Mixing Scheme 48 and C

This chapter describes the foreign-function interface for calling C functions from Scheme, calling Scheme functions from C, and allocating storage in the Scheme heap. Scheme 48 manages stub functions in C that negotiate between the calling conventions of Scheme and C and the memory allocation policies of both worlds. No stub generator is available yet, but writing stubs is a straightforward task.

The foreign-function interface is modeled after the Java Native Interface (JNI), more information can be found at Sun’s JNI Page.

Currently, Scheme 48 supports two foreign-function interfaces: The old GCPROTECT-style and the new JNI-style interface (this chapter) live side by side. The old interface is deprecated and will go away in a future release. Section 8.12 gives a recipe how to convert external code from the old to the new interface.

8.1  Available facilities

The following facilities are available for interfacing between Scheme 48 and C:

8.1.1  Scheme structures

The structure external-calls has most of the Scheme functions described here. The others are in load-dynamic-externals, which has the functions for dynamic loading and name lookup from Section 8.4, and shared-bindings, which has the additional shared-binding functions described in section 8.2.3.

8.1.2  C naming conventions

The names of all of Scheme 48’s visible C bindings begin with ‘s48_’ (for procedures, variables, and macros). Note that the new foreign-function interface does not distinguish between procedures and macros. Whenever a C name is derived from a Scheme identifier, we replace ‘-’ with ‘_’ and convert letters to lowercase. A final ‘?’ converted to ‘_p’, a final ‘!’ is dropped. As a naming convention, all functions and macros of the new foreign-function interface end in ‘_2’ (for now) to make them distinguishable from the old interface’s functions and macros. Thus the C macro for Scheme’s pair? is s48_pair_p_2 and the one for set-car! is s48_set_car_2. Procedures and macros that do not check the types of their arguments have ‘unsafe’ in their names.

All of the C functions and macros described have prototypes or definitions in the file c/scheme48.h.

8.1.3  Garbage collection and reference objects

Scheme 48 uses a precise, copying garbage collector. The garbage collector may run whenever an object is allocated in the heap. The collector must be able to locate all references to objects allocated in the Scheme 48 heap in order to ensure that storage is not reclaimed prematurely and to update references to objects moved by the collector. This interface takes care of communicating to the garbage collector what objects it uses in most situations. It relieves the programmer from having to think about garbage collector interactions in the common case.

This interface does not give external code direct access to Scheme objects. It introduces one level of indirection as external code never accepts or returns Scheme values directly. Instead, external code accepts or returns reference objects of type s48_ref_t that refer to Scheme values (their C type is defined to be s48_value). This indirection is only needed as an interface to external code, interior pointers in Scheme objects are unaffected.

There are two types of reference objects:

local references
A local reference is valid for the duration of a function call from Scheme to external code and is automatically freed after the external function returns to the virtual machine.

global references
A global reference remains valid until external code explicitly frees it.

Scheme objects that are passed to external functions are passed as local references. External functions return Scheme objects as local references. External code has to manually manage Scheme objects that outlive a function call as global references. Scheme objects outlive a function call if they are assigned to a global variable of the external code or stored in long-living external objects, see section 8.7.1.

A local reference is valid only within the dynamic context of the native method that creates it. Therefore, a local reference behaves exactly like a local variable in the external code: It is live as long as external code can access it. To achieve this, every external function in the interface that accepts or returns reference objects takes a call object of type s48_call_t as its first argument. A call object corresponds to a particular call from Scheme to C. The call object holds all the references that belong to a call (like the call’s arguments and return value) to external code from Scheme. External code may pass a local reference through multiple external functions. The foreign-function interface automatically frees all the local references a call object owns, along with the call object itself, when an external call returns to Scheme.

This means that in the common case of Scheme calling an external function that does some work on its arguments and returns without stashing any Scheme objects in global variables or global data structures, the external code does not need to do any bookkeeping, since all the reference objects the external code accumulates are local references. Once the call returns, the foreign-function interface frees all the local references.

For example, the functions to construct and access pairs are declared like this:

This foreign-function interface takes a significant burden off the programmer as it handles most common cases automatically. If all the Scheme objects are live for the extent of the current external call, the programmer does not have to do anything at all. Since the lifetime of the Scheme objects is then identical with the lifetime of the according reference objects. In this case, the systems automatically manages both for the programmer. Using this foreign-function interface does not make the code more complex; the code stays compact and readable. The programmer has to get accustomed to passing the call argument around.

How to manage Scheme objects that outlive the current call is described in section 8.7.1.

Section 8.12 gives a recipe how to convert external code from the old GCPROTECT-style interface to the new JNI-style interface.

8.2  Shared bindings

Shared bindings are the means by which named values are shared between Scheme code and C code. There are two separate tables of shared bindings, one for values defined in Scheme and accessed from C and the other for values going the other way. Shared bindings actually bind names to cells, to allow a name to be looked up before it has been assigned. This is necessary because C initialization code may be run before or after the corresponding Scheme code, depending on whether the Scheme code is in the resumed image or is run in the current session.

8.2.1  Exporting Scheme values to C

Define-exported-binding makes value available to C code under name, which must be a string, creating a new shared binding if necessary. The C function s48_get_imported_binding_2 returns a global reference to the shared binding defined for name, again creating it if necessary, s48_get_imported_binding_local_2 returns a local reference to the shared binding (see section 8.1.3 for details on reference objects). The C macro s48_shared_binding_ref_2 dereferences a shared binding, returning its current value.

8.2.2  Exporting C values to Scheme

Since shared bindings are defined during initialization, i.e. outside an external call, there is no call object. Therefore, exporting shared bindings from C does not use the new foreign-function interfaces specifications.

These are used to define shared bindings from C and to access them from Scheme. Again, if a name is looked up before it has been defined, a new binding is created for it.

The common case of exporting a C function to Scheme can be done using the macro s48_export_function(name). This expands into

s48_define_exported_binding("name",
                               s48_enter_pointer(name))

which boxes the function pointer into a Scheme byte vector and then exports it. Note that s48_enter_pointer allocates space in the Scheme heap and might trigger a garbage collection; see Section 8.7.

These macros simplify importing definitions from C to Scheme. They expand into

(define name (lookup-imported-binding c-name))

where c-name is as supplied for the second form. For the first form c-name is derived from name by replacing ‘-’ with ‘_’ and converting letters to lowercase. For example, (import-definition my-foo) expands into

(define my-foo (lookup-imported-binding "my_foo"))

8.2.3  Complete shared binding interface

There are a number of other Scheme functions related to shared bindings; these are in the structure shared-bindings.

Shared-binding? is the predicate for shared-bindings. Shared-binding-name returns the name of a binding. Shared-binding-is-import? is true if the binding was defined from C. Shared-binding-set! changes the value of a binding. Define-imported-binding and lookup-exported-binding are Scheme versions of s48_define_exported_binding and s48_lookup_imported_binding. The two undefine- procedures remove bindings from the two tables. They do nothing if the name is not found in the table.

The following C macros correspond to the Scheme functions above.

8.3  Calling C functions from Scheme

There are different ways to call C functions from Scheme, depending on how the C function was obtained.

Each of these applies its first argument, a C function that accepts and/or returns objects of type s48_ref_t and has its first argument of type s48_call_t, to the rest of the arguments. For call-imported-binding-2 the function argument must be an imported binding.

For all of these, the interface passes the current call object and the argi values to the C function and the value returned is that returned by C procedure. No automatic representation conversion occurs for either arguments or return values. Up to twelve arguments may be passed. There is no method supplied for returning multiple values to Scheme from C (or vice versa) (mainly because C does not have multiple return values).

Keyboard interrupts that occur during a call to a C function are ignored until the function returns to Scheme (this is clearly a problem; we are working on a solution).

These macros simplify importing functions from C that follow the return value and argument conventions of the foreign-function interface and use s48_call_t and s48_ref_t as their argument and return types. They define name to be a function with the given formals that applies those formals to the corresponding C binding. C-name, if supplied, should be a string. These expand into

(define temp (lookup-imported-binding c-name))
(define name
  (lambda (formal ...)
    (call-imported-binding-2 temp formal ...)))

If c-name is not supplied, it is derived from name by converting all letters to lowercase and replacing ‘-’ with ‘_’.

8.4  Dynamic loading

External code can be loaded into a running Scheme 48---at least on most variants of Unix and on Windows. The required Scheme functions are in the structure load-dynamic-externals.

To be suitable for dynamic loading, the externals code must reside in a shared object. The shared object must define a function:

The s48_on_load is run upon loading the shared objects. It typically contains invocations of S48_EXPORT_FUNCTION to make the functionality defined by the shared object known to Scheme 48.

The shared object may also define either or both of the following functions:

Scheme 48 calls s48_on_unload just before it unloads the shared object. If s48_on_reload is present, Scheme 48 calls it when it loads the shared object for the second time, or some new version thereof. If it is not present, Scheme 48 calls s48_on_load instead. (More on that later.)

For Linux, the following commands compile foo.c into a file foo.so that can be loaded dynamically.

% gcc -c -o foo.o foo.c
% ld -shared -o foo.so foo.o

The following procedures provide the basic functionality for loading shared objects containing dynamic externals:

Load-dynamic-externals loads the named shared objects. The plete? argument determines whether Scheme 48 appends the OS-specific suffix (typically .so for Unix, and .dll for Windows) to the name. The rrepeat? argument determines how load-dynamic-externals behaves if it is called again with the same argument: If this is true, it reloads the shared object (and calls its s48_on_unload on unloading if present, and, after reloading, s48_on_reload if present or s48_on_load if not), otherwise, it will not do anything. The rresume? argument determines if an image subsequently dumped will try to load the shared object again automatically. (The shared objects will be loaded before any record resumers run.) Load-dynamic-externals returns a handle identifying the shared object just loaded.

Unload-dynamic-externals unloads the shared object associated with the handle passed as its argument, previously calling its s48_on_unload function if present. Note that this invalidates all external bindings associated with the shared object; referring to any of them will probably crash the program.

Reload-dynamic-externals will reload the shared object named by its argument and call its s48_on_unload function before unloading, and, after reloading, s48_on_reload if present or s48_on_load if not.

This procedure represents the expected most usage for loading dynamic-externals. It is best explained by its definition:

(define (import-dynamic-externals name)
  (load-dynamic-externals name #t #f #t))

8.5  Accessing Scheme data from C

The C header file scheme48.h provides access to Scheme 48 data structures. The type s48_ref_t is used for reference objects that refer to Scheme values. When the type of a value is known, such as the integer returned by vector-length or the boolean returned by pair?, the corresponding C procedure returns a C value of the appropriate type, and not a s48_ref_t. Predicates return 1 for true and 0 for false.

8.5.1  Constants

The following macros denote Scheme constants:

8.5.2  Converting values

The following macros and functions convert values between Scheme and C representations. The ‘extract’ ones convert from Scheme to C and the ‘enter’s go the other way.

s48_extract_boolean_2 is false if its argument is #f and true otherwise. s48_enter_boolean_2 is #f if its argument is zero and #t otherwise.

The s48_extract_char_2 function extracts the scalar value from a Scheme character as a C long. Conversely, s48_enter_char_2 creates a Scheme character from a scalar value. (Note that ASCII values are also scalar values.)

The s48_extract_byte_vector_2 function needs to deal with the garbage collector to avoid invalidating the returned pointer. For more details see section 8.7.3.

The second argument to s48_enter_byte_vector_2 is the length of byte vector.

s48_enter_long_2() needs to allocate storage when its argument is too large to fit in a Scheme 48 fixnum. In cases where the number is known to fit within a fixnum (currently 30 bits on a 32-bits architecture and 62 bit on a 64-bits architecture including the sign), the following procedures can be used. These have the disadvantage of only having a limited range, but the advantage of never causing a garbage collection. s48_fixnum_p_2(s48_call_t) is a macro that true if its argument is a fixnum and false otherwise.

An error is signaled if the argument to s48_enter_fixnum is less than S48_MIN_FIXNUM_VALUE or greater than S48_MAX_FIXNUM_VALUE (−229 and 229−1 on a 32-bits architecture and −261 and 262−1 on a 64-bits architecture).

s48_true_p is true if its argument is s48_true and s48_false_p is true if its argument is s48_false.

The s48_enter_string_latin_1_2 function creates a Scheme string, initializing its contents from its NUL-terminated, Latin-1-encoded argument. The s48_enter_string_latin_1_n_2 function does the same, but allows specifying the length explicitly---no NUL terminator is necessary.

The s48_string_latin_1_length_2 function computes the length that the Latin-1 encoding of its argument (a Scheme string) would occupy, not including NUL termination. The s48_string_latin_1_length_2 function does the same, but allows specifying a starting index and a count into the input string.

The s48_copy_latin_1_to_string_2 function copies Latin-1-encoded characters from its second NUL-terminated argument to the Scheme string that is its third argument. The s48_copy_latin_1_to_string_n_2 does the same, but allows specifying the number of characters explicitly. The s48_copy_string_to_latin_1_2 function converts the characters of the Scheme string specified as the second argument into Latin-1 and writes them into the string specified as the third argument. (Note that it does not NUL-terminate the result.) The s48_copy_string_to_latin_1_n_2 function does the same, but allows specifying a starting index and a character count into the source string.

The s48_extract_latin_1_from_string_2 function returns a buffer that contains the Latin-1 encoded characters including NUL termination of the Scheme string specified. The buffer that is returned is a local buffer managed by the foreign-function interface and is automatically freed on the return of the current call.

The s48_enter_string_utf_8_2 function creates a Scheme string, initializing its contents from its NUL-terminated, UTF-8-encoded argument. The s48_enter_string_utf_8_n_2 function does the same, but allows specifying the length explicitly---no NUL terminator is necessary.

The s48_string_utf_8_length_2 function computes the length that the UTF-8 encoding of its argument (a Scheme string) would occupy, not including NUL termination. The s48_string_utf_8_length_2 function does the same, but allows specifying a starting index and a count into the input string.

The s48_copy_string_to_utf_8_2 function converts the characters of the Scheme string specified as the second argument into UTF-8 and writes them into the string specified as the third argument. (Note that it does not NUL-terminate the result.) The s48_copy_string_to_utf_8_n_2 function does the same, but allows specifying a starting index and a character count into the source string. Both return the length of the written encodings in bytes.

The s48_extract_utf_8_from_string_2 function returns a buffer that contains the UTF-8 encoded characters including NUL termination of the Scheme string specified. The buffer that is returned is a local buffer managed by the foreign-function interface and is automatically freed on the return of the current call.

The functions with utf_16 in their names work analogously to their utf_8 counterparts, but implement the UTF-16 encodings. The lengths returned be the _length and copy_string_to functions are in terms of UTF-16 code units. The extract function returns a local buffer that contains UTF-16 code units including NUL termination.

8.5.3  C versions of Scheme procedures

The following macros and procedures are C versions of Scheme procedures. The names were derived by replacing ‘-’ with ‘_’, ‘?’ with ‘_p’, and dropping ‘!.

8.6  Calling Scheme functions from C

External code that has been called from Scheme can call back to Scheme procedures using the following function.

This calls the Scheme procedure p on nargs arguments, which are passed as additional arguments to s48_call_scheme_2. There may be at most twelve arguments. The value returned by the Scheme procedure is returned by the C procedure. Invoking any Scheme procedure may potentially cause a garbage collection.

There are some complications that occur when mixing calls from C to Scheme with continuations and threads. C only supports downward continuations (via longjmp()). Scheme continuations that capture a portion of the C stack have to follow the same restriction. For example, suppose Scheme procedure s0 captures continuation a and then calls C procedure c0, which in turn calls Scheme procedure s1. Procedure s1 can safely call the continuation a, because that is a downward use. When a is called Scheme 48 will remove the portion of the C stack used by the call to c0. On the other hand, if s1 captures a continuation, that continuation cannot be used from s0, because by the time control returns to s0 the C stack used by c0 will no longer be valid. An attempt to invoke an upward continuation that is closed over a portion of the C stack will raise an exception.

In Scheme 48 threads are implemented using continuations, so the downward restriction applies to them as well. An attempt to return from Scheme to C at a time when the appropriate C frame is not on top of the C stack will cause the current thread to block until the frame is available. For example, suppose thread t0 calls a C procedure which calls back to Scheme, at which point control switches to thread t1, which also calls C and then back to Scheme. At this point both t0 and t1 have active calls to C on the C stack, with t1’s C frame above t0’s. If thread t0 attempts to return from Scheme to C it will block, as its frame is not accessible. Once t1 has returned to C and from there to Scheme, t0 will be able to resume. The return to Scheme is required because context switches can only occur while Scheme code is running. T0 will also be able to resume if t1 uses a continuation to throw past its call to C.

8.7  Interacting with the Scheme heap

Scheme 48 uses a copying, precise garbage collector. Any procedure that allocates objects within the Scheme 48 heap may trigger a garbage collection.

Local object references refer to values in the Scheme 48 heap and are automatically registered with the garbage collector by the interface for the duration of a function call from Scheme to C so that the values will be retained and the references will be updated if the garbage collector moves the object.

Global object references need to be created and freed explicitly for Scheme values that need to survive one function call, e.g. that are stored in global variables, global data structures or are passed to libraries. See section 8.7.1 for details.

Additionally, the interface provides local buffers that are malloc’d regions of memory valid for the duration of a function call and are freed automatically upon return from the external code. This relieves the programmer from explicitly freeing locally allocated memory. See section 8.7.2 for details.

The interface treats byte vectors in a special way, since the garbage collector has no facility for updating pointers to the interiors of objects, so that such pointers, for example the ones returned by s48_unsafe_extract_byte_vector_2, will likely become invalid when a garbage collection occurs. The interface provides a facility to prevent a garbage collection from invalidating pointers to byte vector’s memory region, see section 8.7.3 for details.

8.7.1  Registering global references

When external code needs a reference object to survive the current call, the external code needs to do explicit bookkeeping. Local references must not be stored in global variables of the external code or passed to other threads, since all local references are freed once the call returns, which leads to a dangling pointer in the global variable or other thread respectively. Instead, promote a local reference to a global reference and store it in a global variable or pass to another thread as global references will survive the current call. Since the foreign-function interface never automatically frees global references, the programmer must free them at the right time.

s48_make_global_ref permanently registers the Scheme value obj as a global reference with the garbage collector. Basic Scheme values are _s48_value_true, _s48_value_false, _s48_value_null, _s48_value_unspecific, _s48_value_undefined, and _s48_value_eof.

To free a global reference an to unregister it with the garbage collector, use s48_free_global_ref. The function s48_local_to_global_ref promotes a local reference object to a global reference object.

For example, to maintain a global list of values, declare a static global variable

  s48_ref_t global_list = NULL;

and initialize it in the external code’s initialization function

  global_list = s48_make_global_ref(_s48_value_null);

Note that you need to use a Scheme value (not a reference object) as the argument for s48_make_global_ref, since there is not yet a call object at the time external code gets initialized. To add new_value to the list, you can use the following snippet:

  temp = global_list;
  global_list =  s48_local_to_global_ref(s48_cons_2(call, new_value, global_list))
  s48_free_global_ref(temp);

You have to remember to always promote reference objects to global references when assigning to a global variable (and later, to free them manually). Note that it is more efficient to free the previous head of the list when adding a new element to the list.

8.7.2  Local buffers

The foreign-function interface supports the programmer with allocating memory in external code: The programmer can request chunks of memory, called local buffers, that are automatically freed on return from the current call.

The function s48_make_local_buf returns a block of memory of the given size in bytes. This memory freed by the foreign-function interface when the current call returns. To free the buffer manually, use s48_free_local_buf.

8.7.3  Special treatment for byte vectors

The interface treats byte vectors in a special way, since the garbage collector has no facility for updating pointers to the interiors of objects, so that such pointers, for example the ones returned by s48_unsafe_extract_byte_vector_2, will likely become invalid when a garbage collection occurs. The interface provides a facility to prevent a garbage collection from invalidating pointers to byte vector’s memory region. It does this by copying byte vectors that are used in external code from and to the Scheme heap.

These functions create byte vectors:

s48_make_byte_vector_2 creates a byte vector of given size, s48_make_unmovable_byte_vector_2 creates a byte vector in that is not moved by the garbage collector (only the Bibop garbage collector supports this). The functions s48_enter_byte_vector_2 and s48_enter_unmovable_byte_vector_2 create and initialize byte vectors.

The following functions copy byte vectors from and to the Scheme heap:

s48_extract_byte_vector_region_2 copies a given section from the given byte vector to its last argument, s48_enter_byte_vector_region_2 copies the contents of its last argument to its first argument to the given index. s48_copy_from_byte_vector_2 copies the whole byte vector to its last argument, s48_copy_to_byte_vector_2 copies the contents of its last argument to the byte vector.

s48_extract_byte_vector_unmanaged_2 returns a local buffer that is valid during the current external call and copies the contents of the given byte vector to the returned buffer. The returned byte vector may be a copy of the Scheme byte vector, changes made to the returned byte vector will not necessarily be reflected in Scheme until s48_release_byte_vector_2 is called.

The following functions to access byte vectors come with the most support from the foreign-function interface. Byte vectors that are accessed via these functions are automatically managed by the interface and are copied back to Scheme on return from the current call:

s48_extract_byte_vector_2 extracts a byte vector from Scheme by making a copy of the byte vectors contents and returning a pointer to that copy. Changes to the byte vector are automatically copied back to the Scheme heap when the function returns, external code raises an exception, or external code calls a Scheme function. s48_extract_byte_vector_readonly_2 should be used for byte vectors that are not modified by external code, since these byte vectors are not copied back to Scheme.

8.7.4  Memory overhead

Each reference object consumes a certain amount of memory itself, in addition to the memory taken by the referred Scheme object itself. Even though local references are eventually freed on return of an external call, there are some situations where it is desirable to free local references explicitly, since waiting until the call returns may be too long or never happen, which could keep unneeded objects live:

8.7.5  Keeping C data structures in the Scheme heap

C data structures can be kept in the Scheme heap by embedding them inside byte vectors. The following macros can be used to create and access embedded C objects.

s48_make_value_2 makes a byte vector large enough to hold an object whose type is type. s48_make_sized_value_2 makes a byte vector large enough to hold an object of size bytes. s48_extract_value_2 returns the contents of a byte vector cast to type, s48_value_size_2 returns its size, and s48_extract_value_pointer_2 returns a pointer to the contents of the byte vector. The value returned by s48_extract_value_pointer_2 is valid only until the next garbage collection. s48_set_value_2 stores value into the byte vector.

Pointers to C data structures can be stored in the Scheme heap:

The function s48_enter_pointer_2 makes a byte vector large enough to hold the pointer value and stores the pointer value in the byte vector. The function s48_extract_pointer_2 extracts the pointer value from the scheme heap.

8.7.6  C code and heap images

Scheme 48 uses dumped heap images to restore a previous system state. The Scheme 48 heap is written into a file in a machine-independent and operating-system-independent format. The procedures described above may be used to create objects in the Scheme heap that contain information specific to the current machine, operating system, or process. A heap image containing such objects may not work correctly when resumed.

To address this problem, a record type may be given a ‘resumer’ procedure. On startup, the resumer procedure for a type is applied to each record of that type in the image being restarted. This procedure can update the record in a manner appropriate to the machine, operating system, or process used to resume the image.

Define-record-resumer defines procedure, which should accept one argument, to be the resumer for record-type. The order in which resumer procedures are called is not specified.

The procedure argument to define-record-resumer may be #f, in which case records of the given type are not written out in heap images. When writing a heap image any reference to such a record is replaced by the value of the record’s first field, and an exception is raised after the image is written.

8.8  Using Scheme records in C code

External modules can create records and access their slots positionally.

The argument to s48_make_record_2 should be a shared binding whose value is a record type. In C the fields of Scheme records are only accessible via offsets, with the first field having offset zero, the second offset one, and so forth. If the order of the fields is changed in the Scheme definition of the record type the C code must be updated as well.

For example, given the following record-type definition

(define-record-type thing :thing
  (make-thing a b)
  thing?
  (a thing-a)
  (b thing-b))

the identifier :thing is bound to the record type and can be exported to C:

(define-exported-binding "thing-record-type" :thing)

Thing records can then be made in C:

static s48_ref_t
  thing_record_type_binding = NULL;

void initialize_things(void)
{
  thing_record_type_binding =
     s48_get_imported_binding_2("thing-record-type");
}

s48_ref_t make_thing(s48_call_t call, s48_ref_t a, s48_ref_t b)
{
  s48_ref_t thing;
  thing = s48_make_record_2(call, thing_record_type_binding);
  s48_record_set_2(call, thing, 0, a);
  s48_record_set_2(call, thing, 1, b);
  return thing;
}

Note that the interface takes care of protecting all local references against the possibility of a garbage collection occurring during the call to s48_make_record_2(); also note that the record type binding is a global reference that is live until explicitly freed.

8.9  Raising exceptions from external code

The following macros explicitly raise certain errors, immediately returning to Scheme 48. Raising an exception performs all necessary clean-up actions to properly return to Scheme 48, including adjusting the stack of protected variables.

The following procedures are available for raising particular types of exceptions. These never return.

An assertion violation signaled via s48_assertion_violation_2 typically means that an invalid argument (or invalid number of arguments) has been passed. An error signaled via s48_error_2 means that an environmental error (like an I/O error) has occurred. In both cases, who indicates the location of the error, typically the name of the function it occurred in. It may be NULL, in which the system guesses a name. The message argument is an error message encoded in UTF-8. Additional arguments may be passed that become part of the condition object that will be raised on the Scheme side: count indicates their number, and the arguments (which must be of type s48_ref_t) follow.

The s48_os_error_2 function is like s48_error_2, except that the error message is inferred from an OS error code (as in strerror). The s48_out_of_memory_error_2 function signals that the system has run out of memory.

The following macros raise assertion violations if their argument does not have the required type. s48_check_boolean_2 raises an error if its argument is neither #t or #f.

8.10  External events

External code can push the occurrence of external events into the main Scheme 48 event loop and Scheme code can wait and act on external events.

On the Scheme side, the external events functionality consists of the following functions from the structure primitives:

And the following functions from the structure external-events:

The function new-external-event-uid returns a fresh event identifier on every call. When called with a shared binding instead of #f, new-external-event-uid returns a named event identifier for permanent use. The function unregister-external-event-uid unregisters the given event identifier.

External events use condition variables to synchronize the occurrence of events, see section 7.5 for more information on condition variables. The function register-condvar-for-external-event registers a condition variable with an event identifier. For convenience, the function new-external-event combines new-external-event-uid and register-condvar-for-external-event and returns a fresh event identifier and the corresponding condition variable.

The function wait-for-external-event blocks the caller (on the condition variable) until the Scheme main event loop receives an event notification (by s48_note_external_event) of the event identifier that is registered with the given condition variable (with register-condvar-for-external-event). There is no guarantee that the caller of wait-for-external-event is unblocked on every event notification, therefore the caller has to be prepared to handle multiple external events that have occurred and external code has to be prepared to store multiple external events.

The following prototype is the interface on the external side:

External code has to collect external events and can use s48_note_external_event to signal the occurrence of an external event to the main event loop. The argument to s48_note_external_event is an event identifier that was previously registered on the Scheme side. Thus, external code has to obtain the event identifier from the Scheme side, either by passing the event identifier as an argument to the external function that calls s48_note_external_event or by exporting the Scheme value to C (see section 8.2.1).

Since the main event loop does not guarantee that every call to s48_note_external_event causes the just occurred event to get handled immediately, external code has to make sure that it can collect multiple external events (i.e. keep them in an appropriate data structure). It is safe for external code to call s48_note_external_event on every collected external event, though, even if older events have not been handled yet.

8.10.1  Collecting external events in external code

External code has to be able to collect multiple events that have occurred. Therefore, external code has to create the needed data structures to store the information that is associated with the occurred event. Usually, external code collects the events in a thread. An separate thread does not have an call argument, though, so it cannot create Scheme data structures. It must use C data structures to collect the events, for example it can create a linked list of events.

Since the events are later handled on the Scheme side, the information associated with the event needs to be visible on the Scheme side, too. Therefore, external code exports a function to Scheme that returns all current events as Scheme objects (the function that returns the events knows about the current call and thus can create Scheme objects). Scheme and external code might need to share Scheme record types that represent the event information. Typically, the function that returns the events converts the C event list into a Scheme event list by preserving the original order in which the events arrived. Note that the external list data structure that holds all events needs to be mutex locked on each access to preserve thread-safe manipulation of the data structure (the Scheme thread that processes events and the external thread that collects events may access the data structures at the same time).

8.10.2  Handling external events in Scheme

If the sole occurrence of an event does not suffice for the program, the Scheme side has to pull the information that is associated with an event from the C side. Then, the Scheme side can handle the event data. For example, a typical event loop on the Scheme side that waits on external events of an permanent event type that an long-running external thread produces may look like this:

(define *external-event-uid* 
  (new-external-event-uid (lookup-imported-binding "my-event")))

(spawn-external-thread *external-event-uid*)

(let loop ()
  (let ((condvar (make-condvar)))
    (register-condvar-for-external-event! *external-event-uid* condvar)
    (wait-for-external-event condvar)
    (process-external-events! (get-external-events))
    (loop)))

In the above example, the variable *external-event-uid* is defined as a permanent event identifier. On every pass through the loop, a fresh condition variable is registered with the event identifier, then wait-for-external-event blocks on the condition variable until external code signals the occurrence of a matching event. Note that process-external-events! and get-external-events need to be defined by the user. The user-written function get-external-events returns all the events that the external code has collected since the last time get-external-events was called; the user-written function process-external-events! handles the events on the Scheme side.

When the Scheme side only waits for one single event, there is no need for an event loop and an permanent event identifier. Then, new-external-event is more convenient to use:

(call-with-values
  (lambda () (new-external-event))
  (lambda (uid condvar)
    (spawn-external-thread uid)
    (wait-for-external-event condvar)
    (unregister-external-event-uid! uid)
    ...))

Here, new-external-event returns a fresh event identifier and a fresh condition variable. The event identifier is passed to spawn-external-thread and the condition variable is used to wait for the occurrence of the external event.

External code uses s48_note_external_event to push the fact that an external event occurred into the main event loop, then the Scheme code needs to pull the actual event data from external code (in this example with get-external-events). The user-written function spawn-external-thread runs the external code that informs the Scheme side about the occurrence of external events. The event identifier is passed as an argument. The external-event-related parts of the implementation of spawn-external-thread in external code could look like this:

s48_ref_t
spawn_external_thread(s48_call_t call, s48_ref_t sch_event_uid) {
  ...
  s48_note_external_event(s48_extract_long_2(call, sch_event_uid));
  ...
}

The event identifier is extracted from its Scheme representation and used to inform the Scheme side about an occurrence of this specific event type.

8.11  Unsafe functions and macros

All of the C procedures and macros described above check that their arguments have the appropriate types and that indexes are in range. The following procedures and macros are identical to those described above, except that they do not perform type and range checks. They are provided for the purpose of writing more efficient code; their general use is not recommended.

Additionally to not performing type checks, the pointer returned by s48_unsafe_extract_byte_vector_2 will likely become invalid when a garbage collection occurs. See section8.7.3 on how the interface deals with byte vectors in a proper way.

8.12  Converting external code to the new foreign-function interface

It is straightforward to convert external code from the old foreign-function interface to the new foreign-function interface:

If you add #define NO_OLD_FFI 1 just above #include <scheme48.h> in your source code file, it will hide all the macros and prototype definitions of the old foreign-function interface. That way you can make sure that you are only using the new interface and the C compiler will remind you if you don’t.