Calling Janus from C and Rust
Calling Janus from C and Rust
Section titled “Calling Janus from C and Rust”The FFI boundary runs both ways (SPEC-253).
Exporting a Janus function
Section titled “Exporting a Janus function”export func emits a function as an unmangled, external-linkage symbol with the C calling convention — callable directly from C or Rust:
export func janus_score(x: i64) -> i64 do return x * 2end- The symbol name is exactly
janus_score— no mangling. exportis a contextual identifier (likepub), not a reserved keyword. Identifiers namedexportkeep working elsewhere.exportcomposes withpuborthogonally:pubis module visibility,exportis ABI emission.
The signature must be C-ABI-representable:
| Allowed | Rejected (E2530) |
|---|---|
integer/float scalars, bool | slices, strings |
*T, [*]T | sum types, trait objects |
[N]u8, void | generics |
func(...) (C fn pointer) | non-empty effect rows other than !{} |
From C:
extern long janus_score(long x); // janus_score(21) == 42Function-pointer parameters
Section titled “Function-pointer parameters”In an extern func signature, a func(...) parameter type denotes a C function pointer, not a Janus closure value:
extern func apply_twice(x: i64, f: func(i64) -> i64) -> i64At the call site you can pass:
- a reference to a top-level function, or
- a non-capturing inline function literal — it erases to a bare C fn pointer.
func double(x: i64) -> i64 do return x * 2end
export func run_named() -> i64 do return apply_twice(5, double) // 20end
export func run_literal() -> i64 do return apply_twice(5, func(x: i64) -> i64 do return x + 1 end) // 7endCapturing closures at (fn, void*) APIs
Section titled “Capturing closures at (fn, void*) APIs”Registration-style C APIs that take a callback plus a void *user_data
slot CAN receive a capturing inline literal (SPEC-253 §2.2b, Gap 114b):
extern func register_cb(cb: func(*void, i64) -> i64, user_data: *void) -> i64
export func run_capture() -> i64 do let k: i64 = 10 return register_cb(func(x: i64) -> i64 do return x + k end)endThe shape gate requires all of:
- an adjacent
*void/*opaqueuser_data parameter immediately after the fn-pointer parameter; - the declared
func(...)type takes*void/*opaqueas its first parameter — C passes user_data back through the callback’s own argument list (cb(user_data, x)); - the literal’s visible signature matches the declared func type minus that leading user_data parameter exactly;
- the visible signature uses integer scalars and pointers only (v1 subset).
The captured environment lives on the stack and is valid only for the
duration of the extern call (.call lifetime — synchronous callbacks).
APIs that retain the callback after registration (OpenSSL
SSL_CTX_set_verify-style) are not supported yet; that needs owned
environments (SPEC-058 Phase 5). AOT (janus build) only.
Diagnostics
Section titled “Diagnostics”| Code | Meaning |
|---|---|
| E2530 | export func signature is not C-ABI-representable |
| E2531 | Capturing closure at an extern boundary outside the v1 integer/pointer subset (general capturing callbacks are SPEC-058 Phase 3) |
| E2532 | Function signature mismatch at an extern fn-pointer argument — widths, signedness, and pointer-ness must match exactly |
| E2533 | Capturing closure at an extern boundary requires an adjacent *void user_data parameter (registration-style (fn, void*) API) |
Rust interop
Section titled “Rust interop”There is no graft rust (it remains E2400-banned per SPEC-240). Rust crosses the boundary exclusively through the C-ABI membrane, in both directions.
Rust side: rust_callback/src/lib.rs
Section titled “Rust side: rust_callback/src/lib.rs”Build as a staticlib (crate-type = ["staticlib"] in Cargo.toml):
/// Janus → Rust: receives a Janus function as a C fn pointer.#[no_mangle]pub extern "C" fn rust_apply_twice(x: i64, cb: extern "C" fn(i64) -> i64) -> i64 { cb(cb(x))}
/// Rust → Janus: calls an exported Janus symbol.#[no_mangle]pub extern "C" fn rust_call_export(x: i64) -> i64 { unsafe { janus_score(x) + 1 }}
extern "C" { fn janus_score(x: i64) -> i64;}Janus side
Section titled “Janus side”extern func rust_apply_twice(x: i64, f: func(i64) -> i64) -> i64extern func rust_call_export(x: i64) -> i64
func double(x: i64) -> i64 do return x * 2end
export func janus_score(x: i64) -> i64 do return x * 2end
export func run_rust_named() -> i64 do return rust_apply_twice(5, double) // 20end
export func run_rust_literal() -> i64 do return rust_apply_twice(5, func(x: i64) -> i64 do return x + 1 end) // 7end
export func run_rust_to_janus() -> i64 do return rust_call_export(21) // 43end
func main() -> i32 do return 0endLinking
Section titled “Linking”Link order matters: the Janus object first, then the Rust static library, then the system libs Rust needs:
cargo build --release # target/release/librust_callback.ajanus build rust_smoke.jan /tmp/rust_smoke.o --emit-objzig cc /tmp/rust_smoke.o target/release/librust_callback.a \ -lpthread -ldl -lm -o /tmp/rust_smokeWorking copies of these examples live in the compiler repo: tests/export_smoke.jan, tests/fnptr_smoke.jan, tests/fnptr_capture_smoke.jan (capturing closures), tests/e2e/rust_smoke.jan, and tests/e2e/rust_callback/.