Docs/Tools & reference

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:

Valk
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.

A source declaration can request a native library:

Valk
link "mylib"

Or pass a library name and a search directory to the compiler:

Terminal
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:

Valk
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:

Valk
export fn multiply(left: i32, right: i32) i32 {
    return left * right
}

Save this as arithmetic.valk and build it as a shared library:

Terminal
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:

Terminal
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.