A foreign block is a declaration with no body. You write the Odin shape of a procedure — its name, arguments, return, calling convention — and end it with --- instead of { ... }. There is no code to generate; the body already exists, compiled, inside a precompiled library across the C ABI. At link time the symbol you named gets wired to that real code. Pick a procedure and watch the declaration map across the line.
Level 1 wired the call. Level 2 is what travels across it: strings change shape at the boundary. An Odin string and a cstring are two different memory layouts — and only one of them is what a C-ABI procedure can read.
An Odin string is two machine words: a pointer to bytes plus a stored length. It carries its own size and needs no terminator. A cstring is one pointer; its length is not stored anywhere — it is found by scanning forward until a zero byte. The strip shows the same six letters under each layout:
Because the layouts differ, the type system keeps them apart: a string handed to a cstring parameter is rejected at compile time — one of the few foreign mistakes Odin can catch before the program runs:
libc.puts(s) // s : string Error: Cannot assign value 's' of type 'string' to 'cstring' in a procedure argument
The fix: convert at the boundary. A bare "hello" literal aimed at a cstring parameter is special — the compiler emits a null-terminated byte buffer for you, so cstring("hello") Just Works. A string built at runtime (from fmt.tprintf, slicing, a file read) is not terminated, so you allocate a terminated copy with strings.clone_to_cstring(s, context.temp_allocator) and pass that:
The terminator is the length. Calling a hand-bound strlen shows it directly: strlen walks the bytes until it hits the zero, and returns the count not including it. So a six-glyph literal returns 6, never 7 — the terminator is the signal to stop, not a counted byte:
my_strlen(cstring("abcdef")) // → 6 my_strlen(cstring("hi")) // → 2
Level 2 marshalled the data. Level 3 is the bill: a foreign declaration is a promise the compiler cannot check. It emits no code — only a reference for the linker to resolve — so it has nothing on the other side to verify your shape against.
A foreign block generates not one instruction of a body. It produces a reference: "at this call site, jump to wherever the linker decides this symbol lives." That hands you two failure modes the ordinary compiler errors don't cover — and they surface at different times. Pick one:
Why the renamed symbol needs @(link_name). When you give the Odin proc a different name than the C symbol — my_puts for the library's puts — the link name changes with it. Without an explicit @(link_name="puts"), the linker hunts for a symbol literally called my_puts, which nothing exports, and the build dies with an unresolved external. The attribute is the wire: Odin-side name on the left, real C symbol on the right.
The last level is what the mechanism unlocks at scale: every precompiled library in the world becomes callable through the same four pieces. A vendor: package is not a different feature — it is exactly this foreign block, already written for you.
Once you can name a library, name a calling convention, declare a shape, and marshal a cstring across, the door opens onto every existing C-ABI library — operating-system calls, audio, physics, the graphics stack. Most of them already have the block written: core:c/libc wraps the C standard library, and Odin's vendor: tree ships ready-made bindings of this kind for the big libraries. Calling libc.puts and calling your own hand-bound my_puts produce indistinguishable output — because they are the same four pieces, one just typed by someone else.
Reimplementing the library means rewriting decades of someone else's audited, optimized code — for OpenGL or SQLite that is not a weekend, it is a career, and your copy starts out buggier than the original.
Shelling out to a separate process and parsing its text output pays a process-launch and serialization cost on every call, loses every typed return value to string-scraping, and turns a direct register-level call into inter-process plumbing.
Hand-marshalling bytes through a generic buffer rebuilds, by hand and without checks, exactly what the foreign declaration + cstring conversion already do — more code, the same idea, more bugs. The foreign block is the marshalling, written once and type-checked on the Odin side.
Here is the real program — the four steps end to end, its output captured from a compiled run:
--- step 1: pre-made libc binding --- --- step 2: convert Odin string -> cstring --- --- step 3: hand-written foreign import --- --- step 4: my_strlen round-trip --- strlen of abcdef = 6 from the bundled binding hello, Ada from my own foreign import
Why the order looks scrambled. All four fmt.println headers and the strlen result print first, then the three puts lines. That is not a bug — Odin's fmt writes to its own stdout buffer and the library's puts writes to the C runtime's separate buffer, and the two flush on their own schedules. The instant two runtimes write to one terminal, ordering between them is no longer guaranteed; you'd flush explicitly or route every line through one side if you needed it strict.
That's the arc: L1 a bodyless declaration the linker wires to a real library symbol → L2 data changes shape at the boundary, so cstring and conversion live there → L3 the bill is that the shape is an unchecked promise — a missing symbol stops the build, a wrong signature does not → L4 get those four pieces right and every precompiled library, including all of vendor:, is callable.