External Declarations (ext)
Silk’s external declaration feature lets Silk code call foreign functions and access foreign variables.
- The core construct is the
extdefinition, which declares: - external C functions and their Silk function types, or
- external C variables and their Silk types.
- The compiler and runtime perform marshalling between Silk’s internal representations and the C ABI, following a documented mapping.
Declaring an External Binding#
Example:
ext foo = fn (string) -> void;
ext bar = u32;
Here:
foois a C function namedfoowith the given Silk function type.baris a C variable of typeu32.
Binding a Different External Symbol Name#
Sometimes you want the Silk binding name to differ from the linked external symbol name (for example, when writing wrapper modules that want to expose stable public API names without colliding with imported libc names).
Syntax:
// The binding name is `c_malloc`, but the linked symbol is `malloc`.
ext c_malloc "malloc" = fn (i64) -> u64;
ext c_free "free" = fn (u64) -> void;
Rules:
- The identifier after
extis the Silk binding name (used for imports and calls from Silk code). - The optional string literal is the external symbol name used for linking (native) or as the import name (wasm).
- If the string literal is omitted, the external symbol name is the same as the binding name.
Avoiding Shadowing (Global ::...)#
If an ext binding is declared in the global namespace (a module with no
package ...; or header-form module ...; declaration) and a local declaration
shadows it (for example, a wrapper function named malloc), use the global-name
prefix to force lookup of the global binding:
return ::malloc(bytes);
The global-name prefix is not limited to ext: it also applies to type names
and enum variant paths in expression and type positions (for example, ::Foo,
::Foo{...}, or ::E::Variant), always forcing resolution in the global
(unnamed) package.
Verification and ext (Silk rule)#
External declarations have no body available to the verifier.
Therefore:
- It is a compile-time error to attempt to verify an
extdeclaration. - It is a compile-time error for verified code (code whose compilation requires
proofs) to call an
extfunction or read anextvariable.
This intentionally limits verification across the ext boundary.
Notes#
Silk currently implements this feature under the ext
keyword. The docs treat ext as canonical.
Currently supported:
- parsing
extexternal declarations and representing them in the AST, - optional external symbol aliases (
ext local "extern" = ...;), extfunctions with fixed parameter lists (ext name = fn (T0, T1) -> R;) as callable symbols in Silk (C variadic...is not implemented yet),extfunction parameters of function type (fn(...) -> R) as C-compatible function pointers:- at the ABI level, these are passed as a single
u64code pointer (no closure environment), - arguments must be either:
- a top-level function name, or
- a non-capturing
fn (...) -> ...expression, - capturing closures (and arbitrary function-typed locals) are rejected for
extfunction-pointer parameters in the Supported forms, c_fn (...) -> Rtypes as explicit C callback pointers (Supported forms):c_fnis a code-pointer-only function pointer type intended for FFI,- unlike
fn (...) -> Rfunction values,c_fn (...) -> Rvalues do not carry a closure environment and are safe to store in locals/struct fields and pass through APIs, - a
c_fnvalue may be formed only from: - a top-level function name, or
- a non-capturing
fn (...) -> ...expression, - capturing closures are rejected when a
c_fnis required. extvariables of scalar type (ext name = T;whereTis a supported scalar such asint, fixed-width ints,bool,char, orf32/f64) as readable values in Silk,stringparameters inextfunction calls are lowered as C-string pointers (const char *) in Silk; the compiler-emitted backing bytes include a trailing NUL terminator, while the Silkstringlength excludes it.- borrowed-view types are restricted at the external boundary:
- opaque handle types declared via
struct Name;may be used behind a reference (&Name) inextfunction parameters and results, - ordinary references (
&T) and slices (T[]) are rejected atextboundaries, - the same ordinary-borrow restriction also applies to unnamed-package
C-facing
export fnsignatures because they participate in the external ABI surface, - named-package Silk object exports may use slice parameters inside the
compiler-owned package ABI; those parameters lower as
{ ptr: u64, len: i64 }and are not part of the Cext/header surface. - lowering calls to
extfunctions when building: silk build --kind object, andsilk build --kind static,silk build --kind shared,silk build --kind executable, producing relocations against undefined external symbols in the generated.o/.a, dynamic imports in the generated.so, or dynamic imports in the generated dynamically-linked executable (linux/x86_64).- for shared libraries and dynamically-linked executables, external calls are routed through a GOT slot that is filled by the dynamic loader.
extvariable reads are supported for the same outputs, producing relocations against undefined external data symbols (.o/.a) or dynamic imports (.so/ dynamically-linked executable) routed through the GOT.- for wasm targets (
wasm32-unknown-unknown,wasm32-wasi),extdeclarations map to wasm imports: ext foo = fn (...) -> ...;becomes an imported wasm functionenv.foo,ext bar = T;becomes an imported wasm globalenv.bar(for scalarT),- parameter/result types follow the compiler’s current scalar lowering (for example
int→ wasmi64).
Not implemented yet (documented design, future work):
- writing to
extvariables (they are read-only in the Supported forms), extvariables of non-scalar types (strings, structs, optionals, arrays),- richer string and aggregate marshalling (for example: returning
stringfromextcalls as an owned Silk value, passing/returning user-defined structs by value beyond the current ABI-safe POD subset, and C-facing array/slice bridging). - calling back into Silk from foreign code with capturing closures or richer
closure environments (only plain non-capturing function pointers are
supported as
extparameters in the Supported forms).
Passing Callbacks to C (c_fn)#
Use c_fn (...) -> R to model C callback pointers you want to store and pass
to foreign code.
Example:
type I64BinOp = c_fn (i64, i64) -> i64;
// C provides: int64_t call_i64_binop(int64_t (*cb)(int64_t, int64_t), int64_t a, int64_t b);
ext call_i64_binop = fn (I64BinOp, i64, i64) -> i64;
fn add (a: i64, b: i64) -> i64 {
return a + b;
}
fn main () -> int {
let cb: I64BinOp = add;
let out: i64 = call_i64_binop(cb, 40, 2);
if out != 42 { return 1; }
return 0;
}
Notes:
c_fnvalues are code pointers only; they cannot capture local variables.- A raw dynamic symbol address can be converted explicitly with
addr as raw c_fn (...) -> Ror an alias of that type. This is intended for loader APIs such asstd::dylib; Silk does not validate the symbol signature at runtime, so the declaredc_fntype must match the foreign ABI exactly. - A
c_fnvalue can be called directly from Silk using the same call syntax as ordinary function values. The current value is invoked as a plain C code pointer with no closure environment. - If a C API needs context, pass an explicit context pointer (e.g. a
u64that is avoid *in C) alongside the callback and include that context parameter in the callback signature.
Opaque Struct Handles#
Opaque structs are intended for representing foreign pointers/handles safely. They strengthen type safety at the language boundary by preventing accidental mixups between different handle types and by disallowing invalid operations in Silk.
Declare an opaque handle type with a fieldless struct declaration:
// runtime.slk
struct StringBuilder;
ext sb_new = fn () -> &StringBuilder;
ext sb_append = fn (&StringBuilder, string) -> void;
ext sb_destroy = fn (&StringBuilder) -> void;
Use the handle by importing the type name and the ext functions:
import { StringBuilder, sb_new, sb_append, sb_destroy } from "./runtime.slk";
fn main () -> int {
let sb: &StringBuilder = sb_new();
sb_append(sb, "hello");
sb_destroy(sb);
// Using `sb` after destroy is UNDEFINED BEHAVIOR (dangling foreign pointer).
return 0;
}
Rules:
- The handle type must be used behind
&(&StringBuilder), not by value. - Opaque structs cannot be instantiated and do not support member access.
Safety:
- You are responsible for managing the lifetime of foreign handles. Most C APIs provide explicit create/destroy functions; always call the destruction function when you are done.
- Using a handle after destruction is undefined behavior; the compiler does not currently enforce this at compile time.
Notes on executable ext calls (current linux/x86_64 implementation):
- When an executable uses
extcalls orextvariable reads, the compiler emits a dynamically-linked ELF64 executable (PIE-styleET_DYNwithPT_INTERP,.dynamic,.rela.dyn, and a.got). - External symbols are resolved by the platform dynamic loader. Dependencies
can be declared via the CLI (
silk build --needed <soname> ...) or via the C99 embedding API (silk_compiler_add_needed_library), and runtime search paths can be declared via--runpath/silk_compiler_add_runpath.
ABI Contract (Overview)#
The language defines two closely related views of the ABI:
-
A “fat pointer” internal representation for
stringandregexp: -
conceptually:
struct string { ptr: ptr, len: i64 }whereptris a UTF‑8 pointer. -
conceptually:
struct regexp { ptr: ptr, len: i64 }whereptris an engine-owned bytecode pointer. -
A C ABI contract (e.g. via
silk/silk.h) using an explicit struct:typedef struct { char *ptr; int64_t len; } SilkString; -
A mapping to an LLVM type used internally by the compiler:
%silk.string = type { i8*, i64 }
When calling conventional C APIs, the compiler may pass a const char * derived from this structure, with the guarantee that the underlying data is null‑terminated. This distinction is important:
- Internal/runtime ABI: operates on
{ ptr, len }structs (SilkString). - Compatibility calls to typical C libraries: may expose
const char *for parameters declared asstringin Silkextdeclarations, with the compiler extracting theptr.
Our embedding ABI for libsilk.a will treat SilkString as the canonical C representation; details are further specified in abi libsilk.
Primitive Type Mapping#
The spec includes a table mapping Silk primitive types to C types, for example:
i8,u8→int8_t,uint8_ti16,u16→int16_t,uint16_ti32,u32→int32_t,uint32_ti64,u64→int64_t,uint64_ti128→SilkI128(see abi libsilk;{ lo, hi }lanes)u128→SilkU128(see abi libsilk;{ lo, hi }lanes)int→int64_t(currentlinux/x86_64baseline; do not assume Cint)f32→floatf64→doublef128→SilkF128(see abi libsilk; IEEE binary128 bits in{ lo, hi })bool→bool(or_Bool)char→uint32_t(UTF‑32)string→SilkString({ char *ptr; int64_t len; })regexp→SilkString({ char *ptr; int64_t len; }, opaque bytecode view)void→void
Notes:
- For FFI with APIs that use a C
int(for example many POSIX syscalls), preferi32/u32in yourextdeclarations rather thanint. - The stable C99 ABI does not use compiler-specific
__int128or__float128types for these primitives; it uses explicit{ lo, hi }structs so the ABI is portable and can be expressed in strict C99.
These mappings must be reflected exactly in the C99 ABI.
Strings and Passing Convention#
For strings, the spec makes the following points:
- Silk’s
stringis represented internally as a{ ptr, len }pair. - For
extcalls to typical C APIs: - the compiler can extract
ptrand pass it as aconst char *, - the data is guaranteed to be null‑terminated so standard C string functions are safe.
For regex bytecode values (regexp):
- Silk’s
regexpis represented internally as a{ ptr, len }pair with the same slot layout asstring, but the bytes are not text and are not required to be null‑terminated. - At ABI boundaries,
regexpuses the same C shape asSilkString, but C code must treat it as an opaque(ptr, len)byte span (not a C string). - Runtime regex helpers validate malformed or undersized foreign
regexppayloads before execution and report them as invalid input, but C code must still not fabricate regex bytecode as if it were a stable public format. - Runtime regex helpers also track which bytecode buffers they actually
allocated: only
std::regex::RegExp.compile(...)produces an owned regex allocation, while borrowed/literal/foreignregexpviews are ignored by the regex free/drop path instead of being freed as if they were runtime-owned.
For the embedding ABI (libsilk.a):
- We treat
SilkString({ char *ptr; int64_t len; }) as the primary C representation of Silkstringvalues. - Functions exported by
libsilk.awill useSilkStringin their signatures wherever strings cross the boundary.
This layered design allows:
- idiomatic FFI to existing C libraries using
const char *, - a precise, length‑carrying ABI (
SilkString) for embedding the compiler/runtime.
Safety & Ownership#
The external interface rules must ensure:
- No C code can violate Silk’s invariants about ownership and lifetimes.
- Any shared data representation (strings, structs, arrays, closures) is documented and stable.
Typed Errors and the ext boundary#
Typed errors (error, panic, and T | ErrorType...) must not cross the ext
boundary.
Rules:
extfunction types must not use|in their return types.- Silk-to-C ABI surfaces must not expose
|in exported function signatures. Shims should convert typed errors into explicit error codes, optionals, or domain-specific error types, or terminate in a platform-appropriate way.
Implementation
- The current compiler rejects
extdeclarations that include|, and rejects exporting error-producing functions to C ABI outputs.
The spec also includes a “Structs, Arrays, and Closures (Complex Types)” subsection for FFI. As the implementation proceeds, this document must be extended to:
- describe how user‑defined structs map to C structs (respecting the layout rules in
structs-impls-layout.md), - define how arrays and slices are represented across the boundary,
- document any stable closure representation, if exposed in the C ABI.
Structs#
The full language design includes rich user-defined structs and nested aggregates. The current compiler implementation supports only a small subset of structs in code generation:
- structs with 0+ fields of supported value types (scalar primitives,
string, nested structs, and supported optionals) in function bodies and internal helper calls, - on
linux/x86_64, passing and returning these structs by value at ABI boundaries using a scalar-slot lowering model: - a struct value lowers to N scalar “eightbyte” slots in field order, and
each slot is classified as INTEGER (integer-like scalars such as
int, fixed-width integers,bool,char,Instant,Duration) or SSE (f32/f64), - exported function parameters accept these slots as separate parameters; for 1–2 slot structs this is ABI-compatible with a by-value C struct parameter for the 8-byte-field subset, while for packed structs with smaller fields ABI compatibility with an equivalent C struct layout is not yet implemented/validated; for 3+ slot structs downstream C callers should declare separate parameters for the slots,
- exported function returns support 1+ slot structs; 1–2 slot results
return in
rax/rdxand/orxmm0/xmm1accordingly, while 3+ slot results return indirectly via a hidden sret pointer.
This subset is intended as a stepping stone toward fully general struct layout
and SysV ABI classification (including packed layout for smaller fields such
as f32 and small integers, nested structs, and larger aggregates returned via
hidden sret pointers).
Optionals#
The full language design includes rich optional patterns (?., match, nested
optionals, etc.). The current compiler implementation supports only a limited
optional subset in code generation:
- optionals whose payload type is a supported scalar,
string, or a supported ABI-safestruct(i.e. after slot-flattening, all scalar slots arei64/u64/f64), - construction via
NoneandSome(value), - unwrapping via
??with short-circuit fallback evaluation, - and nested optionals (
T??) for the same supported payload subset, including unwrappingT??toT?via??.
At ABI boundaries in the current linux/x86_64 subset, optionals are lowered
as a Bool tag followed by the payload scalar slots in order:
(tag, payload)for scalar payload optionals,(tag, ptr, len)forstring?,(tag, slot0, slot1, ...)forstruct?where the payload lowers to N scalar slots.
For nested optionals (T??) in this subset, the payload slots are the full
inner optional representation (for example int?? lowers as
(tag0, tag1, i64 payload)).
For exported functions, these slots consume the normal scalar argument and result locations (registers then stack), and 3+ scalar results return via a hidden sret pointer.
Compiler requirements:
- Implement
extdeclarations as specified. - Map Silk types to C types per the ABI document.
- Enforce the documented passing conventions and ownership rules for external-call strings and other bridged types.
- Keep this document and abi libsilk in sync with the actual codegen strategy.
Source repository · Edit this page · View Markdown