ocaml bindings for chibi-scheme VM
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Scheme 71.7%
C 24.8%
OCaml 1.9%
Shell 0.1%
Perl 0.1%
Dune 0.1%
ReScript 0.1%
Other 1.4%
9 1 0

Clone this repository

https://tangled.org/gdiazlo.tngl.sh/chibi-ocaml https://tangled.org/did:plc:g2nj54o5lr36vbtw55n7xktw/chibi-ocaml
git@tangled.org:gdiazlo.tngl.sh/chibi-ocaml git@tangled.org:did:plc:g2nj54o5lr36vbtw55n7xktw/chibi-ocaml

For self-hosted knots, clone URLs may differ based on your setup.

Download tar.gz
README.md

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.t has 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.t data into Scheme processors; produce streams with OCaml 5 effects via Stream.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-scheme
  • vendor/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 list
  • to_array : t -> t array -- Scheme vector to OCaml array
  • list_length : t -> int
  • equal : 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_import is enabled, Scheme code could potentially re-import blocked bindings from their source modules. For maximum security, grant only Module_import if 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.define and Env.define_function from 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 alive flag. Destroying a context invalidates all associated sexp wrappers. Using a sexp from a destroyed context raises Context_destroyed.
  • with_context provides bracket-style cleanup. Explicit destroy is 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)
  • -lm and -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.