Declaring externals in PekoScript

Declaring externals in PekoScript

A .peko.h header is the usual way to reach C, because one file serves both sides. But the declarations it generates are ordinary PekoScript, and you can write them yourself.

The syntax

[external] fn strlen(str: cstr) => i64;
[external] fn memcpy(dst: pointer<void>, src: opaque, count: i64);
[external] fn peko_random_int(min: i32, max: i32) => i32;

A declaration has no body: it ends at the semicolon. The name is the exact C symbol, because [external] suppresses mangling.

Variables work the same way:

[external] let current_line: i32;
[external] let current_file: cstr;

The types you may use

Only the raw FFI types cross the boundary. The boxed value types do not.

PekoScript C
i1 _Bool
i8, i16, i32, i64 the sized integers
f16, f32, f64 the sized floats
cstr const char *, unmanaged
opaque void *, unmanaged
pointer<T> a managed pointer the collector may move
pointer<void> a managed opaque pointer
void no return

Pass a string by unwrapping it first, and box a scalar result on the way back:

let length: i64 = strlen(text.to_raw())
let count: number = new number(danger_cast<f64>(length))

Modifiers on an external

[external gcsafe] fn peko_run_loop(handle: opaque);
[external variadic] fn printf(fmt: cstr) => i32;

gcsafe marks a call that can allocate, block, or call back into PekoScript, so the compiler keeps it as a safepoint. Leaving it off declares the function a leaf and is what makes passing a managed buffer safe without pinning. Getting this wrong is a silent correctness bug, not a compile error.

variadic allows trailing arguments.

The extern module

Where the declaration lives depends on how it arrived.

A declaration imported through a .peko.h stays in its own module, so it is reached through the import alias:

import c::random::random as cmwc;

let value: i32 = cmwc::peko_random_int(lo, hi)

A declaration written by hand with [external] moves into a reserved top-level module called extern, and is reached through that prefix:

[external] fn __create_globals();

extern::__create_globals()

extern is always present and auto-imported. Importing it explicitly is an error.

The practical rule: if you wrote the declaration yourself, call it through extern::. If it came from a header, call it through the header's alias.

Which to choose

Prefer a header. It keeps the C prototype and the PekoScript declaration in one place, so they cannot drift, and it gives the C compiler the same prototypes it gives the PekoScript compiler.

Write declarations by hand when there is no header to own them: a handful of libc functions, or a symbol the runtime provides that no package declares. The standard library does both, and reserves the by-hand form for exactly those cases.

Linking an object directly

A prebuilt object can be pulled into the link from source rather than through the manifest:

link vendor::libfoo as archive

The form is link path::to::file as object|lib|archive, resolved relative to the current source file. [native.libs] in the manifest is the maintained path and the one to prefer; this exists for one-off cases.