Skip to content

Latest commit

 

History

History
228 lines (186 loc) · 7.07 KB

HOWTO-callback-scheme.adoc

File metadata and controls

228 lines (186 loc) · 7.07 KB

Calling Scheme procedure from C code

A quick starter

A typical code to call a Scheme procedure from C looks like the following. (We assume you want to call a Scheme procedure my-procedure in a module my-module):

{
    ...
    static ScmObj x_proc = SCM_UNDEFINED;
    SCM_BIND_PROC(x_proc, "my-procedure",
                  Scm_FindModule(SCM_SYMBOL(SCM_INTERN("my-module")), 0));
    ...

    ScmObj result = Scm_ApplyRec(x_proc, args);
    ...

}

The macro SCM_BIND_PROC checks whether x_proc is SCM_UNDEFINED, and if so, fetches the value of my-procedure in my-module and stores it in x_proc. You don’t need a lock, for the result is idempotent.

The module my-module must be loaded before this code. If you’re not sure it’s the case, you want to call Scm_Require before the above code; it loads the library if it hasn’t been loaded, and it’s thread-safe.

Once you obtain the Scheme procedure in x_proc, there’s a few ways to call it.

The easiest way is to use Scm_ApplyRec. It takes the Scheme procedure and a list of arguments, and receives a single result. For the zero to five arguments case, we have Scm_ApplyRec0 to Scm_ApplyRec5, for your convenience.

The Scm_ApplyRec family won’t trap Scheme errors; that is, if a Scheme procedure throws an error, it `longjmp`s to somewhere outside of your routine. If you allocate some resource in the above scope, it will leak. Furthermore, the above code assumes there’s someone outside of this frame that are waiting to catch the error. If your code is called from Gauche runtime, there’s a Scheme VM running outside of your routine that calls you---so the Scheme VM will handle the error.

Capturing error

If you need to stop the error (either for resource handling or there’s no VM running outside of your routine), or you need to receive more than one results, you have to use Scm_Apply:

    ...
    static ScmObj x_proc = SCM_UNDEFINED;
    SCM_BIND_PROC(x_proc, "my-procedure",
                  Scm_FindModule(SCM_SYMBOL(SCM_INTERN("my-module")), 0));
    ...
    ScmEvalPacket packet;
    int nresults = Scm_Apply(x_proc, args, &packet);

Scm_Apply always returns even the Scheme routine throws an error. The return value is the number of results, or -1 if there’s an error. The packet structure contains the result(s) or the error thrown:

typedef struct ScmEvalPacketRec {
    ScmObj results[SCM_VM_MAX_VALUES];
    int    numResults;
    ScmObj exception;
    ScmModule *module;          /* 'Current module' after evaluation */
} ScmEvalPacket;

If the return value is nonnegative, results[] contains the return value(s) of the procedure call. If the return value is negative, exception contains the exception thrown during the procedure call.

Continuation-safety

Scheme procedures can return more than once, but C doesn’t expect its functions to return one than once. So we have a checkpoint at the Scheme-C boundary, and if a Scheme function tries to return to C function in second time and later, it throws an error. Typically, when such error occurs, we already finished executing the caller function (in C), so even Scm_Apply can’t capture it. Such error is generally handled by the Gauche VM.

If you’re implementing a C function to be called from Scheme program, which in turn needs to call back to Scheme (think, for example, you’re writing map-like function in C), this limitation is too restricting.

Suppose you have a C function which is callable from Scheme, and it takes a callback argument you need to call within the C function:

ScmObj func(ScmObj callback)
{
    ....
    ScmObj args = ...

    ScmObj r = Scm_ApplyRec(callback, args);
    ....

    return something;
}

If a continuation is captured during calling callback, and it is re-invoked after Scm_ApplyRec returns, and callback tries to return to the C world the second time, an error is thrown. (Note that it is ok that the continuation is re-invoked after Scm_ApplyRec returns, as far as the control transfers out before returning from callback again.)

To allow callback to return more than once, you need to split func into two parts.

static ScmObj func_cc(ScmObj result, void **data);

/* The first half */
ScmObj func(ScmObj callback)
{
    ScmObj args = ...
    ....
    Scm_VMPushCC(func_cc, NULL, 0);
    return Scm_VMApply(callback, args);
}


/* The latter half.  The result argument holds the return value
   of callback. */
ScmObj func_cc(ScmObj result, void **data)
{
    ...
    return something;
}

You see the C function func is split to two parts, one before calling callback and another after calling callback. The call Scm_VMPushCC arranges that the latter half func_cc to be called after callback returns. Scm_VMApply arranges callback to be called after func returns to VM.

You should treat what Scm_VMApply returns as an opaque object. It is important that the value will be returned to the Gauche VM. What Scm_VMApply and Scm_VMPushCC do is to set up the VM state so that callback and func_cc will be called. If you do something to change the VM state between returning Scm_VMApply and the VM takes back control, unexpected things can occur.

If you call callback in this way, and a continuation is captured within callback, it does not involve C calling stack, so that the callback can return as many times as it wish. It’s simply that func_cc will be called more than once.

What if you need to retain a local variable across the call of callback?

ScmObj func(ScmObj callback)
{
    ....
    ScmObj args = ...
    ScmObj some_var = ...
    ....
    ScmObj r = Scm_ApplyRec(callback, args);
    ....
    do_something_with(some_var);
    ....
    return something;
}

You can pass a small number of variables via void **data array.

static ScmObj func_cc(ScmObj result, void **data);

/* The first half */
ScmObj func(ScmObj callback)
{
    ScmObj args = ...
    ScmObj some_var = ...
    ....
    void *data[1];
    data[0] = some_var;
    Scm_VMPushCC(func_cc, data, 1);
    return Scm_VMApply(callback, args);
}


/* The latter half.  The result argument holds the return value
   of callback. */
ScmObj func_cc(ScmObj result, void **data)
{
    ScmObj some_var = SCM_OBJ(data[0]);
    ....
    do_something_with(some_var);
    ...
    return something;
}

Here, the content of data array passed to Scm_VMPushCC is carried over to the data argument of func_cc. The array itself is copied into the area managed by Gauche VM, so the data variable in func can be local. The data variable in func_cc is valid only until Gauche VM state changes, so it is better to extract its content before calling any Gauche API.

The maximum size of the data array is SCM_CCONT_DATA_SIZE, which is at least 6. If you need to retain more local state, you should allocate a struct by SCM_NEW and pass its pointer via data. Be aware that func_cc can be called more than once You shouldn’t deallocate resources explicitly in func_cc, but leave that work to the garbage collector.