Native interoperability
Use explicit native interfaces when Valk code must call or be called by other languages.
On this page
Native interoperability connects Valk to a library's binary interface. Begin with the external function's exact signature, then decide who owns every pointer and how long that storage remains valid.
Declare an external function#
extern declares a symbol supplied by the native linker rather than a Valk body. For example, the C function abs accepts and returns a C int:
extern fn abs(value: i32) i32;
fn main() {
println(abs(-7)) // 7
}
The declaration must match the native ABI. An incorrect type can be a runtime problem even if the Valk declaration itself compiles. Use concrete numeric widths and pointer types that agree with the library's header.
For strings, native APIs often expect a zero-terminated byte pointer. A Valk String is not automatically that calling convention; use the appropriate representation, such as .data_cstring, when the API expects a C string.
Link a library#
A source declaration can request a native library:
link "mylib"
Or pass a library name and a search directory to the compiler:
valk build src -o app -l mylib -L ./native/lib
The library must be built for the same target architecture and operating system as the program. Use --static when linking dependencies statically is required and suitable libraries are available.
Keep raw pointer work explicit#
Use an @unsafe scope for operations that need unchecked native access. This small example reads one byte through an unbounded pointer:
fn read_first(data: *[u8]) u8 {
@unsafe
return data[0]
}
The caller must supply readable storage with at least one byte and keep it alive for the call. A pointer alone does not establish those conditions. Prefer a bounded &[u8] parameter for ordinary Valk APIs, where the length and read-only access are part of the type.
For manual allocation, pair the library's allocation and release operations according to its ownership rules. Garbage collection does not free an arbitrary native allocation just because its pointer goes out of scope.
Build a library#
Mark functions with export when a native linker should expose them:
export fn multiply(left: i32, right: i32) i32 {
return left * right
}
Save this as arithmetic.valk and build it as a shared library:
valk build arithmetic.valk --lib -o arithmetic
A library does not need a main function. Exported signatures must be concrete and cannot contain GC-managed data. Design a pointer-based protocol when data must cross that boundary, including how the recipient releases it.
Use --static-lib to produce an archive:
valk build arithmetic.valk --lib --static-lib -o arithmetic
--static-lib chooses the output kind; --static controls how dependencies are linked. These are separate choices. Output extensions follow the target platform: shared libraries use .so, .dylib, or .dll, and static archives use .a or .lib.