Platform-Specific Code
How platform code works
Tape abstracts platform differences through the runtime library, tapert100. Each target has its own implementation of it (rt/win64/, rt/linux/, rt/macos/, rt/uusi/). Standard library modules like io are written in tape and call into the runtime where they need the operating system, so user code imports them without knowing which platform backs them.
import io from "io"; // the same module on every target
io.println("works everywhere");@link directive
Declares a native library dependency. A library of one platform is linked
in that target’s branch of a @tape block, with the extern fns it
provides:
@tape {
if (@target == "win64") {
@emit @link("user32");
@emit extern fn MessageBoxA(hwnd: *u8, text: *const u8, caption: *const u8, flags: u32) -> i32;
}
}On Windows, this links against user32.dll; on the other targets the
module has neither the library nor the function.
In a t1 or t2 module a bare @link (outside a target branch) is for a
library every target has under one name, written without an extension
(tapert100, uwm100, mylib): each target gives it its own. A platform
library, one whose name ties it to one platform (a path, a .dll, .so,
.dylib, .sto or framework file name, or an operating system’s own
library such as user32 or libc: the list is in Linking &
FFI), linked bare is a compile
error, so a module compiled for another target never asks for a library
that target lacks:
error: 'user32' is a platform library: link it inside `@tape { if (@target == "win64") { @emit @link("user32"); } }` and @emit the extern fns it binds there too; a bare @link in t1 or t2 is for a library every target has, named without an extensiont0 code is single-platform by nature (a kernel, a boot loader) and links any library bare.
Available targets
| Target | Description |
|---|---|
win64 | Windows x86-64 (PE64) |
uusi | Uusi kernel (ELF64) |
linux | Linux x86-64 (ELF64) |
linux-arm64 | Linux ARM64 (ELF64) |
macos | macOS x86-64 |
macos-arm64 | macOS ARM64 |
uefi | UEFI application (PE32+, entry efi_main; see tape build) |
Conditional compilation
For code that must differ per platform within a single module, use if inside a @tape {} block:
@tape {
if (@target == "win64") {
@emit fn get_ticks() -> u64 { return GetTickCount() as u64; }
} else {
@emit fn get_ticks() -> u64 { return clock_ticks(); }
}
}A C function that exists on one target only is declared the same way, with @emit extern fn (no body). It binds to the last @emit @link("...") the block ran before it (the branch taken decides), else to the last @link written before the block (Linking & FFI):
@tape {
if (@target == "macos" || @target == "macos-arm64") {
@emit @link("/usr/lib/libobjc.A.dylib");
if (@target == "macos") {
@emit extern("objc_msgSend_stret") fn msg_rect(obj: *u8, sel: *u8) -> NSRect;
} else {
@emit extern("objc_msgSend") fn msg_rect(obj: *u8, sel: *u8) -> NSRect;
}
}
}@target is the name of the target being compiled for, from the table above:
--target, or the machine you compile on when there is none. tape run and
tape test use the host, so the VM sees the same branch as a native build
on the same machine.
@program is its sibling: the name of the program being compiled. tape build takes it from the output, without directory or extension (-o build/notes.exe is "notes"); tape run and tape test from the source
file (app.tape is "app"). Make it a value the program can use with
@tape const:
@tape const NAME: []const u8 = @program;Both are written with @, like everything the compiler provides, so
nothing in your code can hide them: a const of your own named target is
just a const. The old names TARGET and PROGRAM are not defined (unless
you declare them): the error says to write @target or @program.
@target is checked against the targets there are. It is compared only
with a string literal (==, != or a match arm), and the literal names
one of the targets in the table above:
error: no target 'win65'; the targets are win64, linux, linux-arm64, macos, macos-arm64, uusi, uefiComparing it with anything else (a const, a variable, a call) is an error
too, so a misspelt name can’t hide behind one. A match (@target) covers
every target or has _, so a target added to tape shows each match that
hasn’t decided for it:
match (@target) {
"win64" => { /* ... */ }
"macos", "macos-arm64" => { /* ... */ }
_ => { /* everything else */ }
}@target can’t be assigned, and "{@target}" writes its name. A program’s
own build-time choices (a release or a dev build, a server address) are
build settings.
Link modules
Every extern fn lives in a dedicated link module: a module with @link
is an FFI binding, and the compiler holds it to that. It declares the C
functions and wraps them in pub fns for the rest of the program. (The
one exception is the system-call intrinsics __syscall1..__syscall6,
which the compiler implements for the target rather than imports: a
module that makes raw system calls on Uusi declares them without
@link.)
// gl.tape
@tape {
if (@target == "win64") {
@emit @link("opengl32");
} else if (@target == "linux" || @target == "linux-arm64") {
@emit @link("libGL.so.1");
} else {
@emit @link("/System/Library/Frameworks/OpenGL.framework/OpenGL");
}
@emit extern fn glClear(mask: u32);
@emit extern fn glViewport(x: i32, y: i32, w: i32, h: i32);
}
pub fn clear(mask: u32) { glClear(mask); }
pub fn viewport(x: i32, y: i32, w: i32, h: i32) { glViewport(x, y, w, h); }Other modules import gl from "./gl.tape"; and call gl.clear(...). A link
module may hold extern fn, fn wrappers, struct, enum, tagged and
const declarations, and import ordinary modules. These are compile errors:
- an
extern fnin a module without@link; - in t1 and t2, a bare
@linkof a platform library (link it in a target branch, as above); - a
var, acomponentor a#view/#style/#localizationregion in a link module (keep state and UI in the module that imports it); - a
@tapeblock in a link module, other thanif (@target == "...")branches that@emitthe declarations above; no generators or@tape fn; - a link module importing another link module (link modules are leaves: import both from an ordinary module).
The tape runtime’s own modules are exempt; they say so with @runtime;.
Last modified: