How to persist Duktape/C arguments across calls

When a Duktape/C function is called, Duktape places the call arguments on the value stack. While the arguments are on the value stack, they're guaranteed to be reachable and the Duktape/C function can safely work with the arguments.

However, when the Duktape/C function returns, the value stack is unwound and references in the function's value stack frame are lost. If the last reference to a particular value was in the function's value stack frame, the value will be garbage collected when the function return is processed.

Sometimes Duktape/C functions need to store longer term references to argument values. One common example is implementing a setTimeout()-like function, which we'll use as a running example here:

setTimeout(function cb() {
    print('called after 1 second');
}, 1000);

The basic issues in implementing such a function are:

Storing a persistent reference

Global object

If one were to implement setTimeout() in pure ECMAScript, the reference would be stored to the global object. You can do the same in a Duktape/C function.

Stash objects

You can store references in Duktape's "stash" objects. They are similar to the global object but are not (easily) reachable from ECMAScript code:

Any reachable object

You can store a reference as a property of any reachable object; the object may be reachable through the global object, the stashes, the current thread, etc. For example:

Example using a single global variable

The most trivial approach is simply to store the callback as a global variable. In ECMAScript one would simply:

var _callbackFunc;  // single callback function

function setTimeout(cb, timeout) {
    if (typeof cb !== 'function') {
        throw new TypeError('callback not a function');
    }
    _callbackFunc = cb;
}

// Later on, a scheduler would call this
function invokeCallback() {
    var fn = _callbackFunc;
    _callbackFunc = null;
    fn();  // TypeError if not set
}

The C equivalent of this would be something like:

duk_ret_t native_set_timeout(duk_context *ctx) {
    long timeout;
    duk_require_function(ctx, 0);
    timeout = (long) duk_require_uint(ctx, 1);

    duk_dup(ctx, 0);
    duk_put_global_string(ctx, "_callbackFunc");
    return 0;
}

duk_ret_t native_invoke_callback(duk_context *ctx) {
    duk_get_global_string(ctx, "_callbackFunc");
    duk_push_null(ctx);
    duk_put_global_string(ctx, "_callbackFunc");
    duk_call(ctx, 0);
    return 0;
}

In practice it's useful to detect a missing callback and perhaps log callback errors:

duk_ret_t native_invoke_callback(duk_context *ctx) {
    duk_int_t rc;

    /* Get current callback. */
    duk_get_global_string(ctx, "_callbackFunc");

    /* Explicit check for callback existence; log and exit if no callback. */
    if (!duk_is_function(ctx, -1)) {
        printf("No callback registered\n");
        return 0;
    }

    /* Remove registered callback to avoid calling again. */
    duk_push_null(ctx);
    duk_put_global_string(ctx, "_callbackFunc");

    /* Protected call, log callback errors. */
    rc = duk_pcall(ctx, 0);
    if (rc != 0) {
        printf("Callback failed: '%s'\n", duk_safe_to_string(ctx, -1));
    }
    duk_pop(ctx);

    return 0;
}

Changing the callback data structure to hold multiple callbacks, assigning callbacks number or string IDs etc are straightforward extensions of this basic pattern.