The Burn VM
Native interop
Bytecode is how Burn ships. Programs and libraries talk to each other, and to the program that runs bvm, through functions with typed signatures:
program (bytecode) bytecode library (.bvmc / .bar)
─────────────────── ───────────────────────────────
dist(a, b) ───── call ──────────────────────────▶ @Export dist(a: Point, b: Point): float
@Export hostName() ◀── import ─────────────────── @Native fun hostName(): string
@Inject(target: "describe") ── mixin ──────────▶ describe(p: Point): string (rewritten)
host: HostFn { name: "log", argc: 1, ... } ◀──── @Native("log") fun log(s: string)The translation layer
Both sides describe the functions they share the same way:
- Exports: functions marked
@Export, with a typed signature.pubfunctions of the file you compile are exported automatically. - Imports: host functions a library needs. In Burn:
@Native fun hostName(): string. In assembly:import hostName 0. - Values: both sides use the same runtime, the same heap and the same reference counts, so ints, floats, strings, arrays, maps and records pass between them without conversion. When a library is linked into a program, its types are merged into the program's types: a record that looks the same on both sides (same name, same fields) is the same type.
bvm resolves imports against exports whether the exporter is another module (in a linked program or an archive)
or the program that embeds bvm. That makes a library portable: the same .bvmc runs under bvm, in a .bar and
inside any Burn program that embeds bvm.
Using a library from Burn
import "geometry.bvmc"
fun main() {
var a = Point { x: 0, y: 0 }
print(dist(a, Point { x: 3, y: 4 }))
}import with a .bvmc, .bvm or .bar path makes every exported function of the library callable, with its
real parameter and result types. Record, enum and interface types that appear in those signatures become
available under their names. Build the library from Burn with burnc geometry.bn, or write it in any language
that produces bvm modules.
| Command | What happens to the library |
|---|---|
burni app.bn |
linked with the program and run on bvm |
burnc app.bn |
linked into a single .bvmc |
burnc app.bn --target bar |
stored next to the program in an archive, linked at start |
Calling the program from a library
The library declares what it needs:
@Native
fun hostName(): string
pub fun greeting(): string {
return "hosted by " + hostName()
}The program provides it:
@Export
fun hostName(): string {
return "the program"
}The linker turns the call into a direct bytecode call. A library whose imports nobody provides fails to load, with an error that names the missing function. Programs can also apply mixins to the libraries they import.
Host functions from Burn
A Burn program that embeds bvm (embedding bvm) gives modules host functions as HostFn values,
by name and argument count; the module declares them with @Native("name") in Burn or import name <argc> in
assembly. Arguments arrive as raw words, so a string is the address of its object, which __str turns back into a
string in unsafe code:
var log = fun(args: [int]): int {
unsafe {
print("[plugin] " + __str(args[0]))
}
return 0
}
var prog = prepare(module, [HostFn { name: "log", argc: 1, call: log }])
runEntry(prog)The Burn toolchain is built the same way: burn is the command line written in Burn with bvm compiled into it, so
running a program means compiling it to a module and handing it to prepare and runEntry in the same process.
Native executables use the same layer: a program built with burnc app.bn that imports bytecode libraries is
compiled to bytecode, linked with its libraries and run by bvm, which is linked into the executable
(libburn_launcher.a), so the library's calls back into the program and the program's mixins work exactly as they do
on bvm.