26 Foreign function interface

The foreign function interface (FFI) enables Ori programs to call functions implemented in other languages.

Grammar: See grammar.ebnf § DECLARATIONS (extern_block)

26.1 Overview

Ori supports two FFI backends:

BackendSyntaxTarget
Nativeextern "c"C ABI (LLVM targets)
JavaScriptextern "js"WebAssembly/Browser

All FFI calls require the FFI capability.

26.2 Native FFI (C ABI)

26.2.1 Declaration syntax

extern "c" from "m" {
    @_sin (x: float) -> float as "sin"
    @_sqrt (x: float) -> float as "sqrt"
}

The from clause specifies the library name. The as clause maps Ori function names to C function names.

26.2.2 Library specification

SyntaxMeaning
from "m"System library (libm)
from "/usr/lib/libfoo.so"Absolute path
from "./native/libcustom.so"Relative to project
from "libc"Header-only/inline

26.2.3 Name mapping

When C function names differ from desired Ori names:

extern "c" from "m" {
    @abs (value: float) -> float as "fabs"
    @ln (x: float) -> float as "log"
}

Without as, the Ori function name (without @) is used as the C name.

26.2.4 Visibility

External declarations are private by default:

// Private
extern "c" from "m" {
    @sin (x: float) -> float
}

// Public
pub extern "c" from "m" {
    @sin (x: float) -> float
}

26.3 JavaScript FFI (WASM target)

26.3.1 Declaration syntax

extern "js" {
    @_sin (x: float) -> float as "Math.sin"
    @_sqrt (x: float) -> float as "Math.sqrt"
    @_now () -> float as "Date.now"
}

26.3.2 Module imports

extern "js" from "./utils.js" {
    @_formatDate (timestamp: int) -> str as "formatDate"
}

26.3.3 Async functions

Async JavaScript functions return JsPromise<T>:

extern "js" {
    @_fetch (url: str) -> JsPromise<JsValue> as "fetch"
}

See JsPromise Type for resolution semantics.

26.4 Type marshalling

26.4.1 Primitive types

Ori TypeC TypeWASM TypeJS Type
intint64_ti64BigInt or number
floatdoublef64number
boolbooli32boolean
byteuint8_ti32number

JavaScript number has 53-bit integer precision. Large int values use BigInt.

26.4.2 C type aliases

Ori TypeC TypeSize
c_charchar1 byte
c_shortshort2 bytes
c_intint4 bytes
c_longlongplatform
c_longlonglong long8 bytes
c_floatfloat4 bytes
c_doubledouble8 bytes
c_sizesize_tplatform

26.4.3 Strings

Strings are copied at FFI boundaries:

  • Native: Ori string converted to null-terminated C string (allocated, copied)
  • WASM: Ori string converted via TextEncoder/TextDecoder

26.4.4 CPtr type

CPtr represents an opaque pointer to C data structures:

extern "c" from "sqlite3" {
    @sqlite3_open (filename: str, db: CPtr) -> int
    @sqlite3_close (db: CPtr) -> int
}

CPtr cannot be dereferenced in Ori code.

26.4.5 Nullable pointers

Option<CPtr> represents nullable pointers:

extern "c" from "foo" {
    @get_resource (id: int) -> Option<CPtr>
}

Returns None when the C function returns NULL.

26.4.6 Byte arrays

extern "c" from "z" {
    @compress (
        dest: [byte],
        dest_len: int,
        source: [byte],
        source_len: int
    ) -> int
}
  • [byte] as input: Pointer to data, length passed separately
  • [byte] as output: Pre-allocated buffer, modified in place
  • Bounds checking occurs on the Ori side before the call

26.4.7 JsValue type

JsValue represents an opaque handle to a JavaScript object:

extern "js" {
    @_document_query (selector: str) -> JsValue as "document.querySelector"
    @_element_set_text (elem: JsValue, text: str) -> void
    @_drop_js_value (handle: JsValue) -> void
}

JsValue handles are reference counted in a heap slab and shall be explicitly dropped.

26.4.8 JsPromise type

JsPromise<T> represents a JavaScript Promise:

extern "js" {
    @_fetch (url: str) -> JsPromise<JsValue> as "fetch"
    @_response_text (resp: JsValue) -> JsPromise<str>
}

Implicit Resolution:

JsPromise<T> is implicitly resolved at binding sites in functions with uses Suspend:

@fetch_text (url: str) -> str uses Suspend, FFI =
    {
        let response = _fetch(url: url),   // JsPromise<JsValue> resolved
        let text = _response_text(resp: response),  // JsPromise<str> resolved
        text
    }

Semantics:

  1. When JsPromise<T> is assigned to a binding or used where T is expected, the compiler inserts suspension/resolution
  2. Resolution only occurs in functions with uses Suspend capability
  3. Assigning JsPromise<T> in a non-async context is a compile-time error

26.4.9 C structs

The #repr attribute controls struct memory layout. It applies only to struct types.

AttributeEffect
#repr("c")C-compatible field layout and alignment
#repr("packed")No padding between fields
#repr("transparent")Same layout as single field
#repr("aligned", N)Minimum N-byte alignment (N shall be power of two)
#repr("c")
type CTimeSpec = {
    tv_sec: c_long,
    tv_nsec: c_long
}

#repr("packed")
type PacketHeader = {
    version: byte,
    flags: byte,
    length: c_short
}

#repr("transparent")
type FileHandle = { fd: c_int }

#repr("aligned", 64)
type CacheAligned = { value: int }

Combining attributes:

#repr("c") may combine with #repr("aligned", N). Other combinations are invalid:

// Valid
#repr("c")
#repr("aligned", 16)
type CAligned = { x: int, y: int }

// Invalid - packed and aligned are contradictory
#repr("packed")
#repr("aligned", 16)  // Error
type Invalid = { x: int }

Newtypes:

Newtypes (type T = U) are implicitly transparent — they have identical layout to their underlying type without requiring an explicit attribute.

extern "c" from "libc" {
    @_clock_gettime (clock_id: int, ts: CTimeSpec) -> int as "clock_gettime"
}

26.4.10 Callbacks

Ori functions can be passed to C as callbacks:

extern "c" from "libc" {
    @qsort (
        base: [byte],
        nmemb: int,
        size: int,
        compar: (CPtr, CPtr) -> int
    ) -> void
}

26.4.11 C variadics

C variadic functions are supported with untyped variadic parameters:

extern "c" {
    @printf (fmt: CPtr, ...) -> c_int
}

Calling C variadic functions requires unsafe.

26.5 Unsafe expressions

Proposal: unsafe-semantics-proposal.md

Operations that bypass Ori’s safety guarantees require the Unsafe capability. The unsafe { } block discharges this capability locally:

@raw_memory_access (ptr: CPtr, offset: int) -> byte uses FFI =
    unsafe { ptr_read_byte(ptr: ptr, offset: offset) };

Unsafe is a marker capability — it cannot be bound via with...in (E1203). A function that wraps unsafe operations in unsafe { } blocks does not propagate Unsafe to callers. See Capabilities § Marker Capabilities.

26.5.1 Operations requiring unsafe

  • Dereference raw pointers
  • Pointer arithmetic
  • Access mutable statics
  • Transmute types
  • Call C variadic functions

26.5.2 Safe FFI calls

Regular FFI calls (via extern declarations) are safe to call but require the FFI capability. Only operations Ori cannot verify require unsafe. The FFI capability tracks provenance (foreign code); the Unsafe capability tracks trust (safety bypasses).

26.6 FFI capability

All FFI calls require the FFI capability:

@call_c_function () -> int uses FFI =
    some_c_function();

@manipulate_dom () -> void uses FFI =
    {
        let elem = document_query(selector: "#app");
        element_set_text(elem: elem, text: "Hello");
        drop_js_value(handle: elem)
    }

Standard library functions internally use FFI but expose clean Ori APIs without requiring the FFI capability from callers.

26.7 Compile-time errors

The compile_error built-in triggers a compile-time error:

#target(arch: "wasm32")
compile_error("std.fs is not available for WASM");

#target(not_arch: "wasm32")
pub use "./read" { read, read_bytes };

Semantics:

  • Evaluated during conditional compilation
  • Only triggers if the code path is active
  • Useful for platform availability errors

26.8 Error handling

26.8.1 Native FFI errors

C functions typically return error codes. Deep FFI provides declarative error protocols that automate error checking and Result wrapping:

// Declarative: #error(errno) generates Result wrapping automatically
extern "c" from "libc" #error(errno) {
    @open (path: str, flags: c_int, mode: c_int) -> c_int as "open"
}

// Effective Ori signature: @open (...) -> Result<int, FfiError> uses FFI("libc")
let fd = open(path: "/etc/hostname", flags: O_RDONLY, mode: 0)?

Available error protocols: #error(errno), #error(nonzero), #error(null), #error(negative), #error(success: N), #error(none). See Deep FFI proposal for full specification.

Manual wrapping remains supported for custom error handling:

extern "c" from "libc" {
    @_open (path: str, flags: c_int, mode: c_int) -> c_int as "open"
}

pub @open_file (path: str) -> Result<int, FileError> uses FFI =
    {
        let fd = _open(path: path, flags: 0, mode: 0);
        if fd < 0 then
            Err(errno_to_error())
        else
            Ok(fd)
    }

26.8.2 WASM FFI errors

JavaScript exceptions become Ori errors:

extern "js" {
    @_json_parse (s: str) -> Result<JsValue, str> as "JSON.parse"
}

If JavaScript throws, the function returns Err with the exception message.

26.9 Memory management

26.9.1 Native

Ori’s ARC handles Ori objects. For C objects, Deep FFI provides ownership annotations that integrate with ARC:

  • owned CPtr returns with #free(fn): The compiler generates a Drop impl that calls the specified cleanup function. The value is tracked by ARC like any Ori value.
  • borrowed CPtr returns: Ori does not free the pointer. For borrowed str, Ori copies the string immediately.
  • Unannotated CPtr: Follows C conventions (untracked). Deep FFI phases enforcement from optional to warning to error.

See Deep FFI proposal for the full ownership model.

26.9.2 WASM

  • Linear memory: Ori allocates from WASM linear memory
  • JS object handles: Reference counted in a heap slab; shall be explicitly dropped
@use_js_object () -> void uses FFI =
    {
        let elem = document_query(selector: "#app");
        element_set_text(elem: elem, text: "Hello");
        drop_js_value(handle: elem)
    }

26.10 Platform-specific declarations

Use conditional compilation to provide platform-specific FFI:

#target(not_arch: "wasm32")
extern "c" from "m" {
    @_sin (x: float) -> float as "sin"
}

#target(arch: "wasm32")
extern "js" {
    @_sin (x: float) -> float as "Math.sin"
}

// Public API works on both platforms
pub @sin (angle: float) -> float = _sin(x: angle);

See Conditional Compilation for attribute syntax.

26.11 Deep FFI extensions

Proposal: deep-ffi-proposal.md

Deep FFI is a set of opt-in annotations that layer on top of the extern declaration syntax. All existing FFI code continues to work unchanged.

26.11.1 Parameter modifiers

Grammar: See grammar.ebnf § FFI (param_modifier)

Two modifiers for extern parameters:

ModifierMeaningCompiler Action
outC writes to this address; value folded into return typeAllocate stack slot, pass address, extract value after call
mutC may modify this buffer in placePass pointer to existing buffer
extern "c" from "sqlite3" #error(nonzero) {
    @sqlite3_open (filename: str, db: out owned CPtr) -> c_int
}

// Effective Ori signature:
// @sqlite3_open (filename: str) -> Result<CPtr, FfiError> uses FFI("sqlite3")

26.11.2 Ownership annotations

Grammar: See grammar.ebnf § FFI (ownership)

Two keywords — owned and borrowed — specify memory ownership transfer at the FFI boundary.

AnnotationOn Return TypeOn Parameter
ownedOri takes ownership; cleanup via #freeOri transfers ownership to C
borrowedOri copies immediately (str, [byte]) or non-owning view (CPtr)C borrows temporarily
(none)str: borrowed (safe default). Primitives: by value. CPtr: warning → errorPrimitives: by value. CPtr: borrowed by default

The #free(fn) attribute specifies a cleanup function for owned CPtr returns. The compiler generates a Drop impl that calls this function when the value goes out of scope:

extern "c" from "sqlite3" #free(sqlite3_close) #error(nonzero) {
    @sqlite3_open (filename: str, db: out owned CPtr) -> c_int
    @sqlite3_close (db: owned CPtr) -> c_int
}

26.11.3 Error protocol mapping

A block-level #error(...) attribute specifies how C return values map to Result<T, FfiError>:

ProtocolAttributeSuccess Condition
POSIX errno#error(errno)Return >= 0
Non-zero is error#error(nonzero)Return = 0
Negative is error#error(negative)Return >= 0
NULL is error#error(null)Return != NULL
Specific success#error(success: N)Return = N
No protocol#error(none)(no check)

Per-function #error(...) overrides the block default. FfiError is defined in std.ffi.

26.11.4 Parametric FFI capability

FFI is a parametric capability. Each extern block’s from "..." clause defines a distinct capability namespace:

@query (path: str) -> Result<Row, FfiError> uses FFI("sqlite3") = ...

Unparameterized uses FFI is shorthand for all FFI capabilities (backward compatible).

26.11.5 Capability-gated testability

Each extern block generates a compiler-internal trait. The with FFI("lib") = handler { ... } in construct redirects extern calls to mock implementations:

@test tests query {
    with FFI("sqlite3") = handler {
        sqlite3_open: (filename: str) -> Result<CPtr, FfiError> = Ok(CPtr.null()),
    } in {
        test_database_logic()
    }
}

The stateless handler { ... } form (without state) is syntactic sugar for handler(state: ()) { ... } from the stateful-mock-testing system. Unmocked functions fall through to the real C implementation.