chibi-ocaml#
OCaml bindings for chibi-scheme, a small, embeddable R7RS Scheme implementation.
chibi-ocaml lets you embed one or more sandboxed Scheme virtual machines in your OCaml programs. The chibi-scheme runtime is vendored and compiled automatically, so no system-level installation of chibi-scheme is required. A C compiler capable of building chibi-scheme is still required.
Features#
- Embedded chibi-scheme runtime -- based on vendored chibi-scheme 0.12.0 sources
- Multiple independent VMs -- each
Context.thas its own heap - Configurable memory limits -- set initial and maximum heap sizes
- Capability-based sandboxing -- whitelist file, network, process, and I/O access
- Foreign functions -- register OCaml functions callable from Scheme (arity 0--6)
- Seq-based streaming -- feed OCaml
Seq.tdata into Scheme processors; produce streams with OCaml 5 effects viaStream.from_producer - Bidirectional value conversion -- construct and extract all Scheme types
- Output capture -- redirect and capture Scheme I/O from OCaml
- External dependencies -- C compiler, Ocaml > 5, and dune
Installation#
opam install chibi-ocaml
Or from source:
git clone https://github.com/username/chibi-ocaml.git
cd chibi-ocaml
dune build
dune runtest
Running Upstream chibi Tests#
The repository includes a runner for executing upstream chibi-scheme test
files through the OCaml bindings.
If you have an upstream checkout at ~/data/src/chibi-scheme, run:
dune exec test_runner/run_chibi_tests.exe
The runner looks for tests in this order:
CHIBI_TEST_ROOT~/data/src/chibi-schemevendor/chibi-scheme
To point at a different checkout:
CHIBI_TEST_ROOT=/path/to/chibi-scheme dune exec test_runner/run_chibi_tests.exe
This runner is intended to compare embedded behavior with native
chibi-scheme on the same test files. If a suite fails, check whether it also
fails in the upstream checkout before treating it as a bindings regression.
Quick Start#
Add to your dune file:
(executable
(name my_program)
(libraries chibi-ocaml))
Hello World#
open Chibi_ocaml.Chibi
let () =
with_context (fun ctx ->
let result = Eval.to_int ctx "(+ 2 3)" in
Printf.printf "2 + 3 = %d\n" result)
Define and Call Functions#
open Chibi_ocaml.Chibi
let () =
with_context (fun ctx ->
(* Define a Scheme function *)
let _ = Eval.string ctx "(define (factorial n)
(if (<= n 1) 1 (* n (factorial (- n 1)))))" in
let result = Eval.to_int ctx "(factorial 10)" in
Printf.printf "10! = %d\n" result)
Register OCaml Functions in Scheme#
open Chibi_ocaml.Chibi
let () =
with_context (fun ctx ->
(* OCaml function callable from Scheme *)
Env.define_fn2 ctx "ocaml-add" (fun a b ->
Value.of_int ctx (Sexp.to_int a + Sexp.to_int b));
let result = Eval.to_int ctx "(ocaml-add 100 200)" in
Printf.printf "Result: %d\n" result) (* 300 *)
Multiple Independent VMs#
open Chibi_ocaml.Chibi
let () =
let vm1 = Context.create () in
let vm2 = Context.create () in
let _ = Eval.string vm1 "(define x 42)" in
let _ = Eval.string vm2 "(define x 99)" in
Printf.printf "VM1: x = %d\n" (Eval.to_int vm1 "x"); (* 42 *)
Printf.printf "VM2: x = %d\n" (Eval.to_int vm2 "x"); (* 99 *)
Context.destroy vm1;
Context.destroy vm2
Memory-Limited Sandboxed VM#
open Chibi_ocaml.Chibi
let () =
let config = Context.sandboxed_config
~heap_size:(1024 * 1024) (* 1 MB initial *)
~max_heap_size:(8 * 1024 * 1024) (* 8 MB maximum *)
~capabilities:[Sandbox.Module_import] (* only allow module imports *)
() in
with_context ~config (fun ctx ->
(* Pure computation works *)
let result = Eval.to_int ctx "(apply + '(1 2 3 4 5))" in
Printf.printf "Sum: %d\n" result;
(* File/network/process access is blocked *)
let v = Eval.string ctx "open-input-file" in
assert (Sexp.is_void v)) (* overridden with void *)
Streaming Data from OCaml to Scheme#
open Chibi_ocaml.Chibi
let () =
with_context (fun ctx ->
(* Stream integers from OCaml into a Scheme fold *)
let numbers = List.to_seq [1; 2; 3; 4; 5; 6; 7; 8; 9; 10] in
let stream = Stream.of_int_seq ctx numbers in
let sum = Stream.fold ctx
~expr:"(lambda (x acc) (+ acc x))"
~init:(Value.of_int ctx 0)
stream in
Printf.printf "Sum: %d\n" (Sexp.to_int sum); (* 55 *)
(* Map a Scheme function over an OCaml sequence *)
let words = List.to_seq ["hello"; "world"] in
let stream = Stream.of_string_seq ctx words in
let proc = Eval.string ctx "(lambda (s) (string-length s))" in
let lengths = Stream.map ctx ~proc stream in
let results = Stream.to_int_list lengths in
Printf.printf "Lengths: %d, %d\n" (List.nth results 0) (List.nth results 1))
Producing a Stream with Effects#
Stream.from_producer is the one place effects are used. It lets you write an
imperative producer that calls Effect.perform (Stream.Yield v) and converts it
into a lazy Seq.t:
open Chibi_ocaml.Chibi
let () =
with_context (fun ctx ->
let producer = Stream.from_producer (fun () ->
for i = 1 to 5 do
Effect.perform (Stream.Yield (Value.of_int ctx i))
done) in
let results = Stream.to_int_list producer in
Printf.printf "Produced: %s\n"
(String.concat ", " (List.map string_of_int results)))
Capturing Scheme Output#
open Chibi_ocaml.Chibi
let () =
with_context (fun ctx ->
let (result, stdout, stderr) = Io.capture ctx (fun () ->
Eval.string ctx {|(begin
(display "Hello from Scheme!")
(newline)
42)|}) in
Printf.printf "stdout: %s" stdout; (* "Hello from Scheme!\n" *)
Printf.printf "result: %d\n"
(match result with Ok v -> Sexp.to_int v | Error _ -> -1))
Pattern Matching on Scheme Values#
open Chibi_ocaml.Chibi
let rec scheme_to_string ctx v =
match Sexp.classify v with
| Sexp.Null -> "()"
| Sexp.Boolean b -> if b then "#t" else "#f"
| Sexp.Fixnum n -> string_of_int n
| Sexp.Flonum f -> string_of_float f
| Sexp.String s -> Printf.sprintf "%S" s
| Sexp.Symbol s -> s
| Sexp.Pair ->
let items = Sexp.to_list v in
"(" ^ String.concat " " (List.map (scheme_to_string ctx) items) ^ ")"
| Sexp.Vector ->
let items = Array.to_list (Sexp.to_array v) in
"#(" ^ String.concat " " (List.map (scheme_to_string ctx) items) ^ ")"
| Sexp.Procedure -> "#<procedure>"
| _ -> Eval.write ctx v
API Reference#
The library is accessed through the Chibi_ocaml.Chibi module, which contains
the following submodules:
Exceptions#
| Exception | Raised when |
|---|---|
Chibi_error of string |
Scheme evaluation fails, or a chibi API operation errors |
Context_destroyed |
Operating on a context that has been destroyed |
Type_error of string |
Extracting a value of the wrong type from an sexp |
Sexp -- Scheme Values#
Sexp.t is the opaque type representing any Scheme value.
Classification:
val classify : t -> tag
Returns a tag variant for pattern matching:
type tag =
| Null | Boolean of bool | Fixnum of int | Flonum of float
| Char of int | String of string | Symbol of string
| Pair | Vector | Bytevector | Procedure | Port | Void | Eof | Other
Predicates: is_null, is_pair, is_symbol, is_string, is_fixnum,
is_flonum, is_number, is_boolean, is_char, is_vector,
is_bytevector, is_procedure, is_port, is_void, is_eof, is_true
Pair access: car, cdr, caar, cadr, cdar, cddr
Extraction (raise Type_error on type mismatch):
to_int, to_float, to_string, to_symbol, to_bool, to_char, to_bytes
Collection conversion:
to_list : t -> t list-- Scheme list to OCaml listto_array : t -> t array-- Scheme vector to OCaml arraylist_length : t -> intequal : t -> t -> bool-- pointer equality (eq?)
Context -- Scheme VM Instance#
Each context is an independent Scheme VM with its own heap.
type config = {
heap_size : int; (* initial heap bytes, 0 = default ~2MB *)
max_heap_size : int; (* max heap bytes, 0 = unlimited *)
sandbox : Sandbox.t; (* capability restrictions *)
module_paths : string list; (* additional module search paths *)
}
| Function | Description |
|---|---|
create ?config () |
Create a new VM. Default: full access, default heap. |
destroy t |
Free all resources. Safe to call multiple times. |
is_alive t |
Check if not destroyed. |
heap_size t |
Current heap size in bytes. |
heap_max_size t |
Max allowed heap (0 = unlimited). |
gc t |
Trigger garbage collection. Returns approx. bytes freed. |
default_config |
Full access, default heap, no limits. |
sandboxed_config ?heap_size ?max_heap_size ?capabilities () |
Restricted config. |
Contexts are also cleaned up by the OCaml GC finalizer if you forget to call
destroy, but explicit cleanup is recommended.
Value -- Constructing Scheme Values#
All functions take Context.t as the first argument.
| Function | Creates |
|---|---|
void ctx |
Scheme void |
null ctx |
Empty list '() |
eof ctx |
EOF object |
of_bool ctx b |
#t or #f |
of_int ctx n |
Fixnum |
of_float ctx f |
Flonum |
of_string ctx s |
Scheme string |
of_symbol ctx s |
Symbol |
of_char ctx c |
Character (from OCaml char) |
of_char_code ctx n |
Character (from Unicode code point) |
of_bytes ctx b |
Bytevector |
cons ctx a b |
Pair |
of_list ctx items |
Proper list from OCaml list of sexp |
of_array ctx arr |
Vector from OCaml array of sexp |
of_int_list ctx ns |
List of fixnums |
of_string_list ctx ss |
List of strings |
Eval -- Expression Evaluation#
| Function | Description |
|---|---|
string ctx code |
Evaluate a single Scheme expression from a string. |
sexp ctx s |
Evaluate a parsed s-expression. |
apply ctx proc args |
Apply a procedure to a list of arguments. |
load ctx path |
Load a file (searches module path). |
load_direct ctx path |
Load a file by absolute path. Handles top-level import. |
read ctx code |
Parse a string into an s-expression without evaluating. |
to_string ctx code |
Eval and return write representation. |
to_int ctx code |
Eval and extract integer. |
to_float ctx code |
Eval and extract float. |
to_bool ctx code |
Eval and extract boolean. |
write ctx sexp |
Convert sexp to machine-readable string. |
display ctx sexp |
Convert sexp to human-readable string. |
Env -- Environment Bindings#
| Function | Description |
|---|---|
define ctx name value |
Define a binding in the current environment. |
lookup ctx name |
Look up a binding. Returns Sexp.t option. |
lookup_exn ctx name |
Look up a binding. Raises Chibi_error if unbound. |
define_function ctx name arity f |
Register an OCaml function callable from Scheme. |
define_fn0 .. define_fn6 |
Convenience: typed wrappers for fixed arity. |
Foreign functions receive arguments as Sexp.t list (for define_function) or
as positional Sexp.t arguments (for define_fnN). They must return a
Sexp.t. Maximum arity is 6.
Sandbox -- Capability-Based Security#
The sandbox uses a whitelist model: you specify which capabilities to grant. Everything not explicitly granted is denied.
type capability =
| File_read (* reading files from the filesystem *)
| File_write (* writing/modifying files and directories *)
| Net_access (* sockets, HTTP, DNS *)
| Process_exec (* fork, exec, system, kill, signals, shell *)
| Env_access (* environment variables, user/host info, PIDs *)
| Module_import (* importing Scheme modules via (import ...) *)
| Standard_io (* access to stdin/stdout/stderr *)
| Function | Description |
|---|---|
none |
Maximum sandbox: all capabilities denied. |
full |
No restrictions: all capabilities granted. |
allow caps |
Grant only the listed capabilities. |
has_capability sandbox cap |
Query a capability. |
What each capability controls#
File_read -- When denied, blocks ~50 bindings including:
open-input-file, file-exists?, file->string, directory-files,
file-size, file-regular?, file-directory?, current-directory,
include, load, and all (chibi filesystem) stat/query operations.
File_write -- When denied, blocks ~30 bindings including:
open-output-file, delete-file, rename-file, create-directory,
delete-directory, chmod, chown, link-file, symbolic-link-file,
change-directory, and all POSIX file descriptor write operations.
Net_access -- When denied, blocks ~45 bindings including:
socket, connect, bind, accept, listen, send, receive,
open-net-io, http-get, http-post, http-put, http-delete,
run-net-server, run-http-server, and all (chibi net) operations.
Process_exec -- When denied, blocks ~45 bindings including:
fork, execute, kill, waitpid, system, exit, emergency-exit,
sleep, alarm, call-with-process-io, process->string, all
(chibi shell) operations, signal handling, and privilege escalation
(set-current-user-id!, set-root-directory!, etc.).
Env_access -- When denied, blocks ~25 bindings including:
get-environment-variable, get-environment-variables, command-line,
get-host-name, user-information, current-user-id, current-process-id,
and all (chibi system) user/group query operations.
Standard_io -- When denied, redirects current-input-port,
current-output-port, and current-error-port to null string ports.
Scheme code can still use display/write/read but the data goes nowhere.
Module_import -- When denied, the standard R7RS environment is not
loaded at all. The context starts with only chibi-scheme primitives.
Example: computation-only sandbox#
let config = Context.sandboxed_config
~heap_size:(2 * 1024 * 1024)
~max_heap_size:(16 * 1024 * 1024)
~capabilities:[Sandbox.Module_import] (* only allow importing modules *)
() in
with_context ~config (fun ctx ->
(* Pure computation works *)
let _ = Eval.string ctx "(define (fib n) (if (< n 2) n (+ (fib (- n 1)) (fib (- n 2)))))" in
let result = Eval.to_int ctx "(fib 20)" in
Printf.printf "fib(20) = %d\n" result;
(* File access is blocked *)
let v = Eval.string ctx "open-input-file" in
assert (Sexp.is_void v);
(* Network access is blocked *)
let v = Eval.string ctx "socket" in
assert (Sexp.is_void v))
Security considerations#
The sandbox enforces restrictions by overriding dangerous bindings in the
interaction environment with void after loading the standard library. This
provides strong defense-in-depth:
- 190+ dangerous bindings are blocked across filesystem, network, process, environment, and privilege escalation categories.
- Memory limits are enforced by chibi-scheme's heap allocator at the C level
-- Scheme code cannot bypass
max_heap_size. - Standard I/O is physically redirected to null ports when denied.
However, the sandbox is not a security boundary in the formal sense:
- If
Module_importis enabled, Scheme code could potentially re-import blocked bindings from their source modules. For maximum security, grant onlyModule_importif you need the standard library, and avoid granting it if pure computation suffices. - For the strongest isolation, create a context with no capabilities at all,
then use
Env.defineandEnv.define_functionfrom OCaml to provide only the specific operations your application needs.
Stream -- Seq-Based Streaming#
The Stream module bridges OCaml Seq.t lazy sequences with Scheme processing
functions. Most operations are plain sequence transforms; from_producer is the
one function that uses OCaml 5 effects to turn an imperative yield-style
producer into a lazy Seq.t.
| Function | Description |
|---|---|
from_producer f |
Create a Sexp.t Seq.t from a function using Effect.perform (Yield v). |
feed ctx ~proc ~init seq |
Fold a Scheme procedure over a sequence. |
fold ctx ~expr ~init seq |
Like feed but takes the procedure as a Scheme expression string. |
map ctx ~proc seq |
Map a Scheme procedure over a sequence. |
filter ctx ~pred seq |
Filter a sequence with a Scheme predicate. |
of_channel ctx ic |
Read lines as Scheme strings. |
of_int_seq ctx seq |
Convert int Seq.t to Sexp.t Seq.t. |
of_string_seq ctx seq |
Convert string Seq.t to Sexp.t Seq.t. |
to_scheme_list ctx seq |
Collect into a Scheme list. |
to_int_list seq |
Collect and extract integers. |
to_string_list seq |
Collect and extract strings. |
Io -- Output Capture and Port Redirection#
| Function | Description |
|---|---|
capture ctx f |
Redirect stdout/stderr, run f, return (result, stdout, stderr). Ports are restored afterward. |
set_input_string ctx s |
Set current input port to read from a string. |
redirect_output ctx |
Redirect stdout to string port. Returns thunk to get output. |
with_context#
val with_context : ?config:Context.config -> (Context.t -> 'a) -> 'a
Creates a context, runs the function, and destroys the context when done. The context is always cleaned up, even if the function raises an exception.
Memory Management#
chibi-ocaml coordinates two garbage collectors -- OCaml's GC and chibi-scheme's mark-and-sweep GC:
- Scheme values held by OCaml are protected from chibi's GC via
sexp_preserve_object. When the OCaml wrapper is finalized, the protection is released. - Contexts are reference-counted via the
aliveflag. Destroying a context invalidates all associated sexp wrappers. Using a sexp from a destroyed context raisesContext_destroyed. with_contextprovides bracket-style cleanup. Explicitdestroyis also supported and is idempotent.
You do not need to manually manage Scheme value lifetimes. Values are automatically released when they become unreachable from OCaml.
Thread Safety#
Each Context.t is an independent VM with its own heap. Different contexts can
be used from different OCaml domains (or OS threads) without synchronization.
However, a single context must not be accessed concurrently from multiple threads or domains. chibi-scheme's internal state is not thread-safe.
Building from Source#
Requirements:
- OCaml >= 5.1
- dune >= 3.22
- A C compiler (gcc or clang)
-lmand-ldl(standard on Linux; macOS provides these by default)
dune build # compile
dune runtest # run OCaml binding tests (97 tests)
dune exec bin/main.exe # run demo
dune exec test_runner/run_chibi_tests.exe # run chibi's own test suite
Test Results#
The chibi-scheme test suite passes through our bindings with the same results as the native chibi-scheme binary:
| Suite | Result |
|---|---|
| Basic tests (11) | 11/11 |
| R5RS | 189/189 |
| R7RS | 1224/1225 (1 embedding-specific: command-line) |
| Syntax | 12/12 |
| Unicode | 18/18 |
| Division | 294/304 (chibi bignum bug, same as native) |
| Library tests (all SRFIs + chibi libs) | 6455/6455 |
Total: 8197/8204 (99.9%). All failures are chibi-scheme's own bugs or inherent embedding differences, not binding issues.
License#
BSD-3-Clause. chibi-scheme is also BSD-licensed.