Interop and FFI
The current compiler-visible FFI surface: extern declarations, callbacks, pointers, structs, aliases, arrays, and generated bindings.
Kira's native interop surface is explicit and compiler-visible.
Compiler-Meaning FFI Annotations
Generated and handwritten interop code uses these annotation families today:
@FFI.Extern@FFI.Callback@FFI.Pointer@FFI.Struct@FFI.Alias@FFI.Array
Extern Functions
Extern functions are declarations terminated with ;:
@FFI.Extern { library: callbacks; symbol: kira_invoke_callback; abi: c; }
function kira_invoke_callback(callback: kira_i64_callback, user_data: RawPtr, value: I64): I64;The semantics layer enforces that @FFI.Extern functions must be declarations rather than block-bodied functions.
Native Boundary Rule
Direct FFI usage requires @Native.
That rule applies at the declaration that actually touches the FFI-bound symbol:
@Native
function createPipelineNative(desc: sg_pipeline_desc) -> sg_pipeline {
return sg_make_pipeline(desc)
}The rule is deliberately not transitive. A regular Kira declaration may call a Kira helper that is already @Native:
function createPipeline(desc: sg_pipeline_desc) -> sg_pipeline {
return createPipelineNative(desc)
}The direct native edge stays small and honest, while higher-level Kira code can remain ordinary runtime code when it is only orchestrating Kira helpers or moving handles around.
That rule is deliberately narrow:
- direct FFI usage requires
@Native - indirect usage remains allowed
@Runtimecode may still call@NativeKira helpers, construct native-annotated values, mutate them, and pass them back through hybrid honestly@Nativecode may call back into@Runtimehelpers through the same bridge
Native-annotated structs and classes are still ordinary Kira values. Outside direct FFI edges, they are meant to move through runtime/native code naturally rather than forcing transitive @Native contagion on the surrounding API.
Today, direct FFI usage includes direct calls or references to @FFI.Extern functions. Kira reports KSEM093 when a non-@Native declaration directly uses one. The diagnostic points at the FFI-bound symbol and asks you to mark the declaration @Native or move the direct extern call into a small native helper.
Callback Types
Callback signatures are first-class enough to validate:
@FFI.Callback { abi: c; params: [I64, RawPtr]; result: I64; }
struct kira_i64_callback {}The fail corpus also proves KSEM045 ("invalid callback signature") when a Kira callback does not match the expected FFI callback type.
Pointer, Struct, Alias, and Array Types
The current generated/native examples also prove:
- borrowed pointer wrappers
- C-layout structs
- alias carrier structs for enum-like native values
- array metadata in generated bindings
Examples:
@FFI.Struct { layout: c; }
struct app_state {
var viewport_width: I32
}
@FFI.Pointer { target: app_state; ownership: borrowed; }
struct app_state_ptr {}C-Layout Struct Construction
For @FFI.Struct { layout: c; }, explicit construction is zero-filled before Kira applies the fields you provide:
let desc = sapp_desc {
init_userdata_cb: init
frame_userdata_cb: frame
cleanup_userdata_cb: cleanup
user_data: state
width: 640
height: 480
window_title: "Kira Sokol Triangle"
}sapp_desc() follows the same rule and produces a fully zeroed C-layout value.
This does not change declaration semantics. var desc: sapp_desc is still an uninitialized local declaration, not a zero-filled value.
Where to Go Next
Opaque Callback State
Kira now supports a first-class callback-state flow for native APIs that carry void* or userdata tokens:
struct CounterState {
var count: Int
var total: Int
}
var state = nativeState(CounterState {
count: 0
total: 0
})
var token = nativeUserData(state)
@Native
function onValue(value: I64, user_data: RawPtr) -> I64 {
var state = nativeRecover(user_data)
state.count = state.count + 1
state.total = state.total + value
return value + state.count
}Use each piece for a specific job:
nativeState(value)boxes a Kira-owned value into stable callback-state storagenativeUserData(state)exports the opaqueRawPtrtoken that native code can carry aroundnativeRecover(any)turns the token back into typed mutable Kira access
This is intentionally different from @FFI.Struct { layout: c; }:
@FFI.Struct { layout: c; }is a real native memory-layout contractnativeState/nativeUserData/nativeRecover(any)is opaque userdata transport- native code is not supposed to know the Kira object layout behind that token
- the feature exists so callback-driven interop does not have to fall back to globals
Current lifetime model:
- callback state is Kira-managed heap storage
- it currently lives for the lifetime of the process
- repeated
nativeUserData(state)calls on the same handle return the same token - recovery gives access to the original stored state, not a copy
This chapter explains the language-visible surface. For the workflow side of manifests, autobinding generation, and examples, continue in FFI Workflows.