cleancall

C Resource Cleanup via Exit Handlers

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

Features

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.