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.

tape
import io from "io";   // the same module on every target
io.println("works everywhere");

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

plaintext
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 extension

t0 code is single-platform by nature (a kernel, a boot loader) and links any library bare.

Available targets

TargetDescription
win64Windows x86-64 (PE64)
uusiUusi kernel (ELF64)
linuxLinux x86-64 (ELF64)
linux-arm64Linux ARM64 (ELF64)
macosmacOS x86-64
macos-arm64macOS ARM64
uefiUEFI 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
@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
@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
@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:

plaintext
error: no target 'win65'; the targets are win64, linux, linux-arm64, macos, macos-arm64, uusi, uefi

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

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

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

tape
// 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 fn in a module without @link;
  • in t1 and t2, a bare @link of a platform library (link it in a target branch, as above);
  • a var, a component or a #view/#style/#localization region in a link module (keep state and UI in the module that imports it);
  • a @tape block in a link module, other than if (@target == "...") branches that @emit the 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: