Rivet embeds Racket CS in the native application process. This document records the assumptions the platform hosts must follow.
The embedding ABI is Racket's own chezscheme.h, racketcs.h, and racketcsboot.h. Rivet does not maintain a parallel copy of those definitions.
A native source file that calls the Racket CS API includes:
#include "chezscheme.h"
#include "racketcs.h"On Windows the native executable links to the versioned Racket CS DLL/import library from the matching Racket installation/runtime bundle. On macOS Rivet uses the matching Racket framework. On Linux the host links an exactly matching static libracketcs.a; RIVET_RACKET_LIBRARY and RIVET_RACKET_BOOT_DIR can point the CLI at a separately built embedding prefix.
The following artifacts are one compatibility unit:
- Racket CS library;
petite.boot;scheme.boot;racket.boot;- the Racket version used to compile/package application modules.
Rivet does not silently select a nearby release. If the requested exact runtime is not available, tooling must either build/provision that exact runtime or stop with a clear diagnostic.
This intentionally differs from development scripts that try racket-X.Y-1 or the nearest branch as a convenience.
The native host zero-initializes racket_boot_arguments_t and sets at least:
racket_boot_arguments_t boot{};
boot.boot1_path = petite.c_str();
boot.boot2_path = scheme.c_str();
boot.boot3_path = racket.c_str();
boot.exec_file = executable.c_str();
racket_boot(&boot);For packaged applications Rivet can additionally provide:
collects_dir;config_dir;dll_diron Windows for collected runtime DLLs.
Paths to boot images should contain a directory separator, as required by the embedding contract.
Rivet's application build step uses raco ctool --mods to create a compiled module bundle. The native worker loads it with:
racket_embedded_load_file(core_zo.c_str(), 1);The bundle includes the application module and its transitive Racket module declarations.
For a source file named backend.rkt, the normal compiled module name used by the scaffold is backend.
The native worker constructs a quoted module path and asks Racket for the configured entry procedure:
ptr mod = Scons(Sstring_to_symbol("quote"),
Scons(Sstring_to_symbol("backend"), Snil));
ptr results = racket_dynamic_require(mod, Sstring_to_symbol("start"));
ptr start = Scar(results);racket_dynamic_require returns a list of result values through the embedding API. For one value, take the car before applying it.
Native code should use racket_apply as the normal entry point for calling a Racket procedure:
ptr args = Scons(Sfixnum(in_fd),
Scons(Sfixnum(out_fd), Snil));
(void)racket_apply(start, args);Rivet passes plain integer file descriptors rather than Racket port objects. rivet/backend converts those descriptors into binary Racket ports through serve-fds.
The application entry procedure must not allow an exception/escape to cross the racket_apply boundary. Rivet's serve-fds installs a top-level exception boundary and the server replaces the inherited exit-handler, so request failures become Error frames and backend-level failures are logged before transport shutdown.
Raw Racket values are not Rivet's cross-thread data model. The embedding API permits garbage collection/object movement around Racket calls, and retaining arbitrary raw values creates subtle lifetime requirements.
Rivet therefore uses this rule:
Convert at the boundary; move protocol bytes/native values between threads, not Racket pointers.
The Racket worker may construct temporary symbols/pairs required to enter the application. The WinUI/SwiftUI layer never sees them.
Packaging should use raco ctool --runtime alongside --mods to collect runtime dependencies. On Windows, collected optional DLLs used by Racket should be made discoverable using racket_boot_arguments_t.dll_dir or an equivalent controlled DLL search path.
The target machine should not need a globally installed Racket distribution.
On macOS, raco ctool --runtime-access runtime records staged foreign-library
paths relative to the application resource root. Configurations returned by
EmbeddedRacketConfiguration.resolvedDefault therefore carry that root as the
backend working directory, and EmbeddedRacketBackend.start selects it before
booting Racket. This is a process-wide directory change, consistent with the
process-scoped, single-start embedded runtime. A manually constructed
configuration can leave workingDirectory unset when it owns an alternative
foreign-library lookup strategy.
On macOS, construct the normal staged or packaged layout without repeating path probes in every application:
let configuration = try EmbeddedRacketConfiguration.resolvedDefault(
moduleName: RivetGeneratedConfig.moduleName,
entryName: RivetGeneratedConfig.entryName
)
let backend = EmbeddedRacketBackend(configuration: configuration)The resolver checks the packaged app's Contents/Resources directory first,
then the directory containing the staged executable used by raco rivet dev.
Applications with an intentionally nonstandard layout can continue to pass all
runtime URLs to the public initializer.
The expected normal sequence is:
- native client writes a Rivet Shutdown frame;
serve-fdsreturns and closes its protocol ports/descriptors;racket_applyreturns to the worker;- the worker deinitializes the Chez/Racket runtime;
- native reader observes EOF and exits;
- application host joins both threads.
Startup/runtime failure paths close their native pipe endpoints so a client waiting for Hello observes EOF instead of waiting indefinitely.