Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Foreign Function Interface (FFI) Guide

Overview

Seq’s FFI system enables calling external C libraries from Seq programs. FFI is purely a compiler/linker concern - the runtime remains free of external dependencies, preserving Seq’s minimal footprint.

Quick Start

Built-in FFI: libedit

The compiler includes a BSD-licensed libedit binding for readline-style input:

include ffi:libedit

: main ( -- Int )
  "prompt> " readline
  "You entered: " swap string.concat io.write-line
  0
;

Build with:

seqc build myprogram.seq -o myprogram

External FFI: SQLite

For libraries not bundled with the compiler, use --ffi-manifest:

include ffi:sqlite

: main ( -- Int )
  ":memory:" db-open drop
  "CREATE TABLE test (id INT)" db-exec drop
  db-close drop
  0
;

Build with:

seqc --ffi-manifest sqlite.toml myprogram.seq -o myprogram

Include Syntax

FFI bindings are accessed via the ffi: prefix in include statements:

include ffi:libedit    # Built-in manifest (ships with compiler)
include ffi:sqlite     # Must provide --ffi-manifest sqlite.toml

The library name after ffi: must match the name field in the manifest.

Writing FFI Manifests

FFI bindings are declared in TOML files. Here’s a complete example:

[[library]]
name = "mylib"
link = "mylib"           # Passed to linker as -lmylib

[[library.function]]
c_name = "my_function"   # C function name
seq_name = "my-func"     # Seq word name
stack_effect = "( Int String -- Int )"
args = [
  { type = "int", pass = "int" },
  { type = "string", pass = "c_string" }
]
[library.function.return]
type = "int"

Type Mappings

Manifest TypeC TypeSeq TypeNotes
intint/longInt64-bit on Seq side
stringchar*StringNull-terminated
ptrvoid*IntRaw pointer as integer
voidvoid(nothing)No return value

Pass Modes

The pass field controls how arguments are passed to C:

ModeDescription
c_stringConvert Seq String to null-terminated char*
ptrPass raw pointer value (Int on stack)
intPass as C integer
by_refAllocate storage, pass pointer (for out params)

Memory Ownership

The ownership field on returns controls memory management:

ModeDescriptionCodegen
caller_freesC malloc’d it, we must freeGenerates free() call
staticLibrary owns memory, don’t freeJust copy, no free
borrowedOnly valid during callCopy immediately

Advanced Features

Out Parameters (by_ref)

Some C functions return values via pointer parameters (out params). Use by_ref pass mode:

[[library.function]]
c_name = "sqlite3_open"
seq_name = "db-open"
stack_effect = "( String -- Int Int )"   # db handle + return code
args = [
  { type = "string", pass = "c_string" },
  { type = "ptr", pass = "by_ref" }       # Out parameter
]
[library.function.return]
type = "int"

For by_ref arguments:

  1. Compiler allocates local storage
  2. Passes pointer to that storage to C function
  3. After call, reads value and pushes onto stack

Important: The by_ref value is an opaque handle owned by the C library. You must:

  • Only pass it to functions from the same library
  • Never attempt to free it manually
  • Always use the library’s cleanup function (e.g., db-close)

Fixed Value Arguments

For arguments that should always be a constant (like NULL callbacks):

args = [
  { type = "ptr", pass = "ptr" },
  { type = "string", pass = "c_string" },
  { type = "ptr", value = "null" },    # Always passes NULL
  { type = "ptr", value = "null" },
  { type = "ptr", value = "null" }
]

Fixed value arguments don’t consume stack values - they’re compiled as constants. Supported values: null, NULL, or integer literals.

Multiple Manifests

You can load multiple FFI manifests:

seqc --ffi-manifest db.toml --ffi-manifest crypto.toml program.seq -o program

Safety Model

FFI is inherently unsafe - you’re calling into C code that can do anything. Seq’s approach:

  1. Opt-in boundary: Using include ffi:* is the explicit safety boundary
  2. Stack effects enforced: Type checker validates declared effects
  3. Memory managed by codegen: Ownership annotations prevent leaks
  4. Linker flag validation: Only safe characters allowed in link names

If you don’t use FFI, your Seq program has full memory safety.

Security Considerations

  • Trust your manifests: Malicious manifests could link arbitrary libraries
  • Validate external manifests: Review manifests before using --ffi-manifest
  • Linker injection prevented: Link names can only contain alphanumeric, dash, underscore, and dot characters

Built-in Libraries

ffi:libedit

BSD-licensed readline alternative. Provides:

WordStack EffectDescription
readline( String -- String )Read line with prompt
add-history( String -- )Add line to history
read-history( String -- Int )Load history from file
write-history( String -- Int )Save history to file

Examples

Example: Interactive REPL

include ffi:libedit

: repl ( -- )
  "seq> " readline
  dup string.length 0 i.> if
    dup add-history
    process-input      # Your processing here
    repl
  else
    drop
  then
;

: main ( -- Int )
  "Welcome to Seq REPL" io.write-line
  repl
  0
;

Example: Persistent History

include ffi:libedit

: repl ( -- )
  "seq> " readline
  dup string.length 0 i.> if
    dup add-history
    process-input
    repl
  else
    drop
  then
;

: main ( -- Int )
  # Load history at startup (ignore error if file doesn't exist)
  "/tmp/.myapp_history" read-history drop

  "Welcome to Seq REPL" io.write-line
  repl

  # Save history on exit
  "/tmp/.myapp_history" write-history drop
  0
;

Note: File paths are passed directly to the C library. Shell expansions like ~ are not performed. Use os.home-dir and os.path-join to build paths from the home directory rather than hardcoding /tmp paths.

Example: SQLite Database

See examples/ffi/sqlite/ for a complete SQLite example demonstrating:

  • by_ref out parameters for database handles
  • Fixed null values for unused callbacks
  • Proper handle cleanup with db-close

Troubleshooting

“Unknown word: readline”

Ensure you have include ffi:libedit at the top of your file.

“Unknown FFI library: sqlite”

You need to provide the manifest: --ffi-manifest path/to/sqlite.toml

Linker errors

Install the C library’s development package:

  • macOS: brew install <library>
  • Ubuntu: apt install lib<library>-dev
  • Fedora: dnf install <library>-devel

“Invalid character in linker flag”

Link names can only contain: a-z, A-Z, 0-9, -, _, .

This prevents command injection attacks via malicious manifests.

Design Notes

Callbacks (Shelved)

FFI callbacks (C functions calling back into Seq) were explored but shelved for now.

Why shelved:

  • Most useful callback patterns (qsort comparators, iteration handlers) pass pointers to the callback, requiring low-level memory operations to interpret them
  • Those low-level operations (ptr-read-i64, etc.) are too invasive for Seq’s design
  • Many C APIs have non-callback alternatives (e.g., SQLite’s prepared statement API works without callbacks)

Callbacks may be revisited when there’s a concrete use case that justifies the complexity.

See ROADMAP.md for the full FFI roadmap.