cleancall v0.1.1

0

Monthly downloads

0th

Percentile

C Resource Cleanup via Exit Handlers

Wrapper of .Call() that runs exit handlers to clean up C resources. Helps managing C (non-R) resources while using the R API.

Readme

cleancall

C Resource Cleanup via Exit Handlers

lifecycle Travis build status Windows Build
status CRAN RStudio mirror
downloads Coverage status

Features

  • Add exit handlers to a .Call() to C, via a call_with_cleanup() wrapper.
  • Restrict an exit handler to run only on early exit (error, interrupt, debugger exit, restart invokation, condition caught, etc.). I.e. anything that prevented your function from running its normal course.
  • Exit handlers are executed in reverse order. Last added runs first.
  • Exit handlers can be added from any downstream function, they don't need to be called directly from the function called by call_with_cleanup().

Limitations

We suggest that exit handlers are kept as simple and fast as possible. In particular, errors (and other early exits) triggered from exit handlers are not caught currently. If an exit handler exits early the others do not run. If this is an issue, you can wrap the exit handler in R_tryCatch() (available for R 3.4.0 and later).

Installation

You can install the released version of cleancall from CRAN with:

install.packages("cleancall")

Example

This example is from the processx package. Its processx_wait() function waits for an external process to end, and this wait is interruptible. processx_wait() opens two temporary file descriptors for the wait, and these need to be closed at the end of the function, even on an interrupt, otherwise we have a resource leak.

See this link for the complete function, before fixing.

Here we only include the relevant parts:

SEXP processx_wait(SEXP status, SEXP timeout) {
  processx_handle_t *handle = R_ExternalPtrAddr(status);
  int ctimeout = INTEGER(timeout)[0], timeleft = ctimeout;
  struct pollfd fd;
  int ret = 0;
  pid_t pid;

  [...]

  /* Setup the self-pipe that we can poll */
  if (pipe(handle->waitpipe)) {
    processx__unblock_sigchld();
    error("processx error: %s", strerror(errno));
  }

  [...]

  while (ctimeout < 0 || timeleft > PROCESSX_INTERRUPT_INTERVAL) {
    do {
      ret = poll(&fd, 1, PROCESSX_INTERRUPT_INTERVAL);
    } while (ret == -1 && errno == EINTR);

    /* If not a timeout, then we are done */
    if (ret != 0) break;

    R_CheckUserInterrupt();

    [...]
  }

  [...]

cleanup:
  if (handle->waitpipe[0] >= 0) close(handle->waitpipe[0]);
  if (handle->waitpipe[1] >= 0) close(handle->waitpipe[1]);
  handle->waitpipe[0] = -1;
  handle->waitpipe[1] = -1;

  return ScalarLogical(ret != 0);
}

pipe() allocates two file descriptors, they are saved in handle->waitpipe[0] and handle->waitpipe[1]. The wait is interruptible, so the function calls R_CheckUserInterrupt(). This checks for a CTRL+C or ESC interrupt, and if there was one, it returns directly to the caller of .Call(). This is of course problematic, because processx_wait() has no chance of closing the pipe file descriptors.

Fixing this with cleancall is as follows. First your package needs to depend on cleancall, update DESCRIPTION:

[...]
Imports:
    cleancall,
LinkingTo:
    cleancall
[...]

In the R code calling processx_wait(), replace .Call() with cleancall::call_with_cleanup():

cleancall::call_with_cleanup(c_processx_wait, private$status,
                             as.integer(timeout))

Then include the cleancall.h header in the C code, and use r_call_on_exit() to push a cleanup handler to the stack of the foreign call:

#include <cleancall.h>

[...]

static void processx__close_fd(void *ptr) {
  int *fd = ptr;
  if (*fd >= 0) close(*fd);
}

SEXP processx_wait(SEXP status, SEXP timeout) {
  processx_handle_t *handle = R_ExternalPtrAddr(status);

  [...]

  if (pipe(handle->waitpipe)) {
    processx__unblock_sigchld();
    error("processx error: %s", strerror(errno));
  }
  r_call_on_exit(processx__close_fd, handle->waitpipe);
  r_call_on_exit(processx__close_fd, handle->waitpipe + 1);

  [...]
}

You can see the whole fix as a commit message on GitHub.

See also our blog post at https://www.tidyverse.org/articles/2019/05/resource-cleanup-in-c-and-the-r-api/

Usage

void r_call_on_exit(void (*fn)(void* data), void *data)

Push an exit handler to the stack. This exit handler is always executed, i.e. both on normal and early exits.

Exit handlers are executed right after the function called from call_with_cleanup() exits. (Or the function used in r_with_cleanup_context(), if the cleanup context was established from C.)

Exit handlers are executed in reverse order (last in is first out, LIFO). Exit handlers pushed with r_call_on_exit() and r_call_on_early_exit() share the same stack.

Best practice is to use this function immediately after acquiring a resource, with the appropriate cleanup function for that resource.

void r_call_on_early_exit(void (*fn)(void* data), void *data)

Push an exit handler to the stack. This exit handler is only executed on early exists, not on normal termination.

Exit handlers are executed right after the function called from call_with_cleanup() exits. (Or the function used in r_with_cleanup_context(), if the cleanup context was established from C.)

Exit handlers are executed in reverse order (last in is first out, LIFO). Exit handlers pushed with r_call_on_exit() and r_call_on_early_exit() share the same stack.

Best practice is to use this function immediately after acquiring a resource, with the appropriate cleanup function for that resource.

SEXP r_with_cleanup_context(SEXP (*fn)(void* data), void* data)

Establish a cleanup stack and call fn with data. This function can be used to establish a cleanup stack from C code.

License

MIT @ RStudio

Please note that the 'cleancall' project is released with a Contributor Code of Conduct. By contributing to this project, you agree to abide by its terms.

Functions in cleancall

Name Description
call_with_cleanup Call a native routine within an exit context
cleancall-package cleancall: C Resource Cleanup via Exit Handlers
No Results!

Last month downloads

Details

URL https://github.com/r-lib/cleancall#readme
BugReports https://github.com/r-lib/cleancall/issues
License MIT + file LICENSE
Encoding UTF-8
LazyData true
RoxygenNote 6.1.1
NeedsCompilation yes
Packaged 2020-01-10 14:07:15 UTC; gaborcsardi
Repository CRAN
Date/Publication 2020-01-12 00:30:02 UTC
suggests covr , testthat
depends R (>= 3.1)
Contributors RStudio, Lionel Henry

Include our badge in your README

[![Rdoc](http://www.rdocumentation.org/badges/version/cleancall)](http://www.rdocumentation.org/packages/cleancall)