Blog › ICP guides
Gambit Scheme developer on retainer: c-lambda FFI, char-string vs pointer type conversions, GC-safe memory model, SRFI-18 threads, and Gambit Scheme compiler on monthly retainer
November 26, 2026 · ~15 min read
A Gambit Scheme program calling C library functions through Gambit’s c-lambda FFI was producing three wrong string values per batch of two or more sequential calls. The C library exposed a get_message(int code) -> char* function that returned a pointer to a static character buffer maintained by the library; the buffer was overwritten on each successive call. The initial Gambit binding used c-lambda with the char-string return type: (c-lambda (int) char-string "get_message"). The char-string return type causes Gambit to copy the bytes from the char* pointer into a fresh Scheme string at the moment of the call and return that Scheme string. Because the string is a copy, it is independent of the static C buffer; subsequent calls that overwrite the static buffer do not affect the previously returned Scheme string. This worked correctly. Later, a developer attempting to eliminate the copy overhead changed the return type from char-string to (pointer char), reasoning that the zero-copy variant would be faster for high-throughput message processing. The (pointer char) return type wraps the raw char* pointer directly as a Gambit foreign-pointer object without copying the bytes. The Gambit program now held a foreign-pointer to the static C buffer rather than an independent Scheme string copy. A first call to the wrapped function returned a foreign-pointer p1 to the static buffer. A second call returned a foreign-pointer p2. Because both pointers wrapped the same static buffer address, both p1 and p2 pointed to the same C memory. After the second call, reading the value of p1 produced the message from the second call, not the first: the second call had overwritten the static buffer that p1 pointed to. Three result values per batch were wrong. The Gambit Scheme developer on retainer diagnosed the static buffer aliasing through the (pointer char) wrapper: the correct return type for a C function returning a pointer to a static buffer is char-string, which copies the bytes into a Gambit-owned Scheme string at call time. Restructured all bindings to use char-string (or nonnull-char-string to add null-pointer safety) and required each call’s result to be extracted into a Scheme string before the next call. Wrong string values per batch: 3 → 0.
The work log entry read “fixed FFI message corruption, 9h.” It names the symptom and duration. It cannot explain why (pointer char) was a performance optimization that introduced a correctness hazard — the zero-copy design assumes the pointer remains valid and unmodified for the duration of the Scheme program’s use of the wrapped pointer, which is only true for C library functions that return pointers to heap-allocated memory that the caller owns and must free; for functions returning pointers to static or thread-local buffers, the pointer is only valid until the next call that modifies that buffer. It cannot explain why the char-string return type is safe precisely because it copies at call time: the copy cost is paid once per call, the resulting Scheme string is fully owned by the Gambit GC, and the C static buffer can be overwritten by subsequent calls without affecting previously captured results. It cannot explain the retainer’s decision to use nonnull-char-string rather than char-string for all final bindings — nonnull-char-string adds a null-pointer check before the copy, raising a Scheme exception if the C function returns a null pointer rather than silently producing a crash or empty string; this is the correct production hardening for C functions that may return null on error. The 9 hours of Gambit FFI memory model analysis, static buffer lifetime audit across all c-lambda bindings, and return type selection are invisible in the diff.
Gambit Scheme c-lambda: type conversions, char-string, nonnull-char-string, pointer types, and foreign-pointer
Gambit Scheme’s c-lambda form generates a Gambit procedure that calls a C function, with automatic type conversion between Gambit Scheme values and C types. The form is (c-lambda (param-types...) return-type "c-expression"). The type specifiers in the parameter and return positions determine how Gambit converts values at the Scheme-C boundary. The type conversions are applied at call time and return time respectively; selecting the wrong type specifier produces either incorrect behavior (values corrupted by the wrong conversion) or runtime exceptions (null-pointer dereference, type mismatch).
The most consequential type selection in practice is the return type for C functions that return char*. Three options exist. char-string: Gambit copies the bytes from the char* pointer into a fresh Gambit string and returns the Gambit string; the C pointer is read only at the moment of the call and is not retained by Gambit. nonnull-char-string: same as char-string but raises a Scheme exception if the char* is null, rather than silently producing a crash. (pointer char): Gambit wraps the raw char* pointer in a Gambit foreign-pointer object and returns it without copying; the C pointer is retained in the foreign-pointer wrapper and can be dereferenced later. The retainer pattern: use char-string or nonnull-char-string for C functions that return pointers to static buffers, thread-local buffers, or any memory that the C library owns and may modify; use (pointer char) only for C functions that return pointers to caller-owned heap memory that the Scheme side controls the lifetime of.
The foreign-pointer type in Gambit wraps an opaque C pointer that Gambit does not interpret or copy: the Scheme program holds the pointer as an object and can pass it back to C functions or dereference it through accessor c-lambda wrappers. Foreign-pointer objects do not participate in Gambit’s GC in the sense that the pointed-to C memory is not tracked or freed by the GC; the Scheme program is responsible for managing the C memory lifetime. Gambit provides will-execute! to register a Scheme procedure to be called when a foreign-pointer wrapper is about to be GC’d, enabling automatic C memory cleanup: (will-execute! ptr (lambda (p) (free-c-memory p))) calls the lambda with the foreign-pointer when the Gambit GC determines that no more Scheme references to the pointer exist. This is the Gambit idiom for RAII-like cleanup of C-allocated resources.
Gambit GC-safe memory model: heap boundary, object pinning, and C callback design
Gambit’s garbage collector manages the Gambit heap, which is separate from the C heap. Gambit GC objects (Scheme strings, vectors, pairs, closures) can be moved by the GC during compaction: a Scheme string allocated at address 0x1000 may be relocated to 0x2000 during a GC cycle. This is transparent for normal Scheme code, which uses Gambit’s tagged-pointer representation to find objects regardless of their address. It becomes critical when Gambit objects are passed to C code: the C function receives a raw pointer (the current address) at the moment of the call; if the GC runs during the C function’s execution, the object may be moved, and the C function is now working with a dangling pointer to the old location.
Gambit provides ##still-alive? (and related pinning mechanisms) to prevent the GC from moving or collecting an object while a C call that references it is in progress. The pattern: before passing a Scheme object to a C function that will hold a pointer to its data, pin the object; after the C call returns, release the pin. For simple one-shot calls where the C function returns immediately, Gambit’s c-lambda handles pinning automatically for the duration of the call for standard parameter types like char-string (which converts at call time, so no pin is needed after the conversion). The complexity arises for C functions that retain a pointer after the call returns — for example, a C library that caches a pointer to a Scheme string buffer for use in a callback. For these patterns, explicit pinning with ##still-alive? and a pin-release callback (via will-execute! or an explicit unpin procedure) is required.
C callbacks into Gambit Scheme — C code that calls a Scheme procedure as a function pointer — require that the C thread is registered with the Gambit thread system. Gambit Scheme’s threading model is based on SRFI-18: threads, mutexes, condition variables, and thread-specific variables, implemented by Gambit’s runtime scheduler. A C thread created outside of Gambit’s scheduler that attempts to call a Gambit Scheme procedure (via a c-define callback) is operating on a thread unknown to Gambit. Gambit provides ##make-primordial-thread and thread initialization APIs for registering C threads with the Gambit runtime before they call Scheme code. The retainer pattern: when a C library calls callbacks on its own threads (for example, an event loop library that fires callbacks on an I/O thread), verify that the callback thread is initialized as a Gambit thread or redesign the callback to post work to a Gambit thread via a channel or mailbox. Gambit Scheme was created by Marc Feeley at the Université de Montréal, with continuous development from the early 1990s. It is one of the most production-capable R7RS Scheme implementations, used for high-performance Scheme programs compiled to native code via C and for Scheme-to-JavaScript compilation. Its closest relatives in the retainer ecosystem are Chicken Scheme for the practical Scheme-with-C-FFI positioning and Common Lisp implementations for the systems-programming-with-Lisp positioning, but Gambit’s c-lambda type system, GC-safe memory model, and SRFI-18 threading discipline make the retainer work distinct in FFI memory lifetime engineering and C-boundary aliasing analysis.
How HourTab tracks Gambit Scheme developer retainer hours
Gambit Scheme retainer work carries the invisible-work problem of all FFI-heavy language retainers, compounded by the gap between Scheme’s high-level value semantics and the manual memory lifetime discipline required at the C boundary. Teams embedding Gambit Scheme for high-performance scripting in C applications frequently encounter the (pointer char) static buffer aliasing pattern when a developer optimizes a binding for performance: the original char-string copy was correct but slow; the replacement (pointer char) wrapper is fast but shares a lifetime with the C buffer; when the C buffer is reused on the next call, the previously returned wrapped pointer becomes stale. The three wrong string values per batch described above are three instances of the static buffer being overwritten after (pointer char) wrappers to it were returned to Scheme code; the retainer work is the FFI memory model analysis that identifies the static buffer lifetime hazard and the return type selection that eliminates it by copying at call time.
HourTab gives Gambit Scheme developers a public retainer-hours URL they send to clients — typically C application teams embedding Gambit Scheme for scripting and policy logic, organizations building high-performance Scheme systems with native-code compilation via gsc, and teams using Gambit’s SRFI-18 threading for concurrent Scheme services. For Gambit retainers, each work log entry should name the mechanism (char-string vs (pointer char) c-lambda return type repair; static buffer lifetime aliasing audit; nonnull-char-string null-pointer safety addition; GC-safe ##still-alive? pinning for long-lived C references; foreign-pointer lifetime management; will-execute! cleanup registration; SRFI-18 mutex and condition variable design; thread-specific variable usage; C-thread Gambit initialization for callbacks; gsc compilation flag optimization; GC tuning parameter selection), the specific c-lambda signatures, C function names, static buffer lifetimes, and thread interaction patterns involved in the bug, and the before/after metric. Gambit retainers are often compared to Racket developer retainers for the shared Scheme-family positioning, but Gambit’s c-lambda type system, GC-heap-vs-C-heap boundary discipline, and SRFI-18 threading model make the retainer work distinct in FFI return type selection, pinning discipline, and C-callback thread initialization. HourTab’s work log makes the c-lambda type mapping analysis, static buffer lifetime audit, and return type selection visible to clients who would otherwise see only the symptom — three wrong string values per batch — and not understand why the fix required understanding the difference between (pointer char) C-pointer wrapping and char-string copy-on-return, and why the zero-copy optimization was correct for some C functions but fatal for functions returning static buffers.
Track Gambit Scheme developer retainer hours without the status emails
HourTab gives Gambit Scheme developers a public URL per client retainer. One link, no login, live burn-down. Your clients stop asking “how many hours do I have left?” and your work log becomes the proof of value that gets the retainer renewed.
See HourTab pricing →FAQ: Gambit Scheme developer retainers
What does a Gambit Scheme developer on retainer typically do?
A Gambit Scheme developer on monthly retainer covers four service areas: Gambit FFI design and debugging (c-lambda type mapping: char-string, nonnull-char-string, (pointer char), int, double, bool; static buffer lifetime and aliasing; foreign-pointer for opaque C pointers; c-define for C-callable Scheme); Gambit memory model (GC heap vs C heap boundary; object pinning with ##still-alive?; foreign-pointer lifetime; will-execute! cleanup); SRFI-18 threading (mutex and condition variable design; thread-specific variables; C thread and Gambit thread interaction; callback from C into Scheme); Gambit compiler optimization (gsc flags; C compilation backend; inlining; GC tuning).
What Gambit Scheme work is most commonly underlogged in a retainer?
C-lambda (pointer char) static buffer aliasing repair (C function returned static char* buffer; (pointer char) wrapped pointer without copy; subsequent call overwrote static buffer; first result read through wrapper saw overwritten data; 3 wrong strings per batch; restructured to char-string copy-on-return; wrong strings: 3/batch → 0; 8–14 hrs invisible); GC-safe pinning discipline (Scheme object passed to C; GC moved object during C call; dangling pointer; ##still-alive? pinning required; 7–13 hrs invisible); and SRFI-18 C-thread callback initialization (C library called Scheme callback from C thread outside Gambit thread system; exception or wrong dispatch; C-thread Gambit initialization required; 9–16 hrs invisible).
What are typical Gambit Scheme developer retainer rates?
Entry-level Gambit Scheme developers (1–2 years, Scheme syntax, basic c-lambda bindings, SRFI-18 threading, Gambit standard library) bill at $65–$115/hr. Mid-level Gambit Scheme engineers (2–4 years, c-lambda type mapping, GC-safe pinning, static buffer aliasing debugging, SRFI-18 mutex and condition variable design, Gambit compiler optimization) bill at $110–$185/hr. Senior Gambit Scheme architects (4–8 years, complex C library integration, GC-safe C-callback design, GC tuning, Gambit-to-JavaScript compilation, large-scale SRFI-18 concurrent system design, Gambit macro engineering) bill at $155–$275/hr. Monthly retainer ranges: $1,800–$4,400/mo advisory (15–25 hrs), $6,000–$16,000/mo for full Gambit Scheme systems development engagements.
What should a Gambit Scheme developer retainer agreement include?
A Gambit Scheme developer retainer agreement should specify: FFI design scope (c-lambda type mapping: char-string, nonnull-char-string, (pointer char), int, double, bool; static buffer lifetime and aliasing; foreign-pointer; c-define for C-callable Scheme); GC memory model scope (GC heap vs C heap boundary; ##still-alive? pinning; foreign-pointer lifetime; will-execute! cleanup); SRFI-18 threading scope (mutex and condition variable design; thread-specific variables; C thread and Gambit thread interaction; callback from C); Gambit compiler scope (gsc flags; C compilation backend; inlining; GC tuning); and hour logging format (advisory category, before/after wrong-value or exception metric, Gambit version, whether fix required char-string return type, pinning, SRFI-18 mutex, or gsc flag).
How should Gambit Scheme developer retainer hours be logged?
Log each Gambit Scheme retainer session with: advisory category (c-lambda char-string vs (pointer char) repair; static buffer aliasing audit; nonnull-char-string null-check addition; ##still-alive? GC-safe pinning; foreign-pointer lifetime management; will-execute! cleanup registration; SRFI-18 mutex and condition variable; thread-specific variable; C-thread Gambit initialization; gsc compilation flag; GC tuning parameter); the specific c-lambda signatures, return types, C function names, and buffer lifetimes involved (c-lambda (int) (pointer char) "get_message" wrapped static char* buffer; second call overwrote buffer; first (pointer char) result saw overwritten data; 3 wrong strings per batch; restructured to nonnull-char-string; wrong strings: 3/batch → 0); and the before/after metric. Include Gambit version and whether fix required char-string, nonnull-char-string, pinning, SRFI-18 mutex, or gsc flag change.