burn / docs

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. pub functions 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.