mirror of
https://github.com/odin-lang/Odin.git
synced 2026-07-22 23:42:33 +00:00
316 lines
9.9 KiB
Odin
316 lines
9.9 KiB
Odin
// rexcode · Brendan Punsky (dotbmp@github), original author
|
|
// Ginger Bill (gingerBill@github)
|
|
|
|
package rexcode_wasm
|
|
|
|
import "base:runtime"
|
|
import "core:slice"
|
|
|
|
// =============================================================================
|
|
// SECTION: Container encode (ir Module -> a whole .wasm binary module)
|
|
// =============================================================================
|
|
//
|
|
// The mirror of parse.odin. `encode_ops` (encoder.odin) serializes one WASM
|
|
// `expr` -- a single function's instruction stream. This file is the outer
|
|
// layer parse.odin reads: it wraps those bodies in the CODE section framing and
|
|
// emits the surrounding sections so `encode` produces a complete `.wasm` file
|
|
// (the 8-byte header plus the length-prefixed sections), not a bare run of
|
|
// concatenated bodies.
|
|
//
|
|
// Two shapes of module flow through here, dispatched on whether the module
|
|
// carries the binary framing decode preserved (`sections` + `data`):
|
|
//
|
|
// * A decoded module (round-trip / edit-a-body): every section is re-emitted
|
|
// verbatim from `data` in its original order, EXCEPT CODE, which is
|
|
// regenerated from the ir Operations (so edits to a function body show up).
|
|
// A decode -> encode with no edits reproduces the input byte-for-byte; the
|
|
// sections parse.odin does not model structurally (table / memory / global
|
|
// / element / data, non-func imports, custom sections) are never lost.
|
|
//
|
|
// * A from-scratch module (built via make_module + the builders): there is no
|
|
// `data` to copy, so the sections the ir core *does* model are synthesized
|
|
// in the canonical WASM order -- TYPE, IMPORT (func), FUNCTION, EXPORT,
|
|
// START, CODE. Sections with no structural slot (table/memory/global/...)
|
|
// cannot be synthesized and are simply absent.
|
|
//
|
|
// Emitted relocations (for symbolic index refs inside bodies, e.g. op_label)
|
|
// are offset relative to the produced `code` buffer, and carried out through
|
|
// `relocs` for a linker; body errors surface through `errors` as usual.
|
|
|
|
// encode: serialize the module `m` into `code`, producing a complete `.wasm`
|
|
// binary. Returns the number of bytes written and whether it fully succeeded.
|
|
// Size `code` with `encode_size(m)` (or any larger buffer).
|
|
encode :: proc(m: Module, code: []u8, relocs: ^[dynamic]Relocation, errors: ^[dynamic]Error) -> (byte_count: u32, ok: bool) {
|
|
errors_start := u32(len(errors))
|
|
|
|
out := build_module(m, relocs, errors)
|
|
defer delete(out)
|
|
|
|
if len(out) > len(code) {
|
|
append(errors, Error{location = 0, code = .BUFFER_OVERFLOW})
|
|
return 0, false
|
|
}
|
|
copy(code, out[:])
|
|
byte_count = u32(len(out))
|
|
ok = u32(len(errors)) == errors_start
|
|
return
|
|
}
|
|
|
|
// encode_size: the exact byte length `encode(m, ...)` will produce (a dry run,
|
|
// so callers can size the output buffer precisely). Uses the temp allocator.
|
|
@(require_results)
|
|
encode_size :: proc(m: Module) -> u32 {
|
|
throw_r: [dynamic]Relocation; throw_r.allocator = runtime.nil_allocator()
|
|
throw_e: [dynamic]Error; throw_e.allocator = runtime.nil_allocator()
|
|
out := build_module(m, &throw_r, &throw_e)
|
|
defer delete(out)
|
|
return u32(len(out))
|
|
}
|
|
|
|
// =============================================================================
|
|
// Module builder (into a temp-allocated byte buffer)
|
|
// =============================================================================
|
|
|
|
@(private, require_results)
|
|
build_module :: proc(m: Module, relocs: ^[dynamic]Relocation, errors: ^[dynamic]Error) -> (out: [dynamic]u8) {
|
|
append_u32le :: #force_inline proc(b: ^[dynamic]u8, v: u32) {
|
|
append(b, u8(v), u8(v >> 8), u8(v >> 16), u8(v >> 24))
|
|
}
|
|
|
|
out.allocator = context.temp_allocator
|
|
|
|
// header: `\0asm` magic + version.
|
|
append_u32le(&out, WASM_MAGIC)
|
|
append_u32le(&out, m.version if m.version != 0 else WASM_VERSION)
|
|
|
|
if len(m.sections) > 0 && m.data != nil {
|
|
emit_passthrough(m, &out, relocs, errors)
|
|
} else {
|
|
emit_synth(m, &out, relocs, errors)
|
|
}
|
|
return
|
|
}
|
|
|
|
// Decoded module: re-emit each section verbatim in file order, regenerating
|
|
// only CODE from the ir Operations.
|
|
@(private)
|
|
emit_passthrough :: proc(m: Module, out: ^[dynamic]u8, relocs: ^[dynamic]Relocation, errors: ^[dynamic]Error) {
|
|
for sec in m.sections {
|
|
if sec.id == .CODE {
|
|
emit_code_section(m, out, relocs, errors)
|
|
} else {
|
|
append(out, u8(sec.id))
|
|
append_uleb(out, u64(sec.size))
|
|
append(out, ..m.data[sec.offset:][:sec.size])
|
|
}
|
|
}
|
|
}
|
|
|
|
// From-scratch module: synthesize the modeled sections in canonical order.
|
|
@(private)
|
|
emit_synth :: proc(m: Module, out: ^[dynamic]u8, relocs: ^[dynamic]Relocation, errors: ^[dynamic]Error) {
|
|
emit_section :: proc(out: ^[dynamic]u8, id: Section_Id, contents: []u8) {
|
|
append(out, u8(id))
|
|
append_uleb(out, u64(len(contents)))
|
|
append(out, ..contents)
|
|
}
|
|
|
|
|
|
tmp := context.temp_allocator
|
|
|
|
// TYPE
|
|
if len(m.func_types) > 0 {
|
|
c: [dynamic]u8; c.allocator = tmp
|
|
append_uleb(&c, u64(len(m.func_types)))
|
|
for ft in m.func_types {
|
|
append(&c, 0x60) // functype form
|
|
|
|
#assert(size_of(ft.params[0]) == 1)
|
|
#assert(size_of(ft.results[0]) == 1)
|
|
|
|
append_uleb(&c, u64(len(ft.params)))
|
|
append(&c, ..slice.to_bytes(ft.params[:]))
|
|
|
|
append_uleb(&c, u64(len(ft.results)))
|
|
append(&c, ..slice.to_bytes(ft.results[:]))
|
|
}
|
|
emit_section(out, .TYPE, c[:])
|
|
}
|
|
|
|
// IMPORT (only the func kind is structurally modeled)
|
|
if len(m.imports) > 0 {
|
|
c: [dynamic]u8; c.allocator = tmp
|
|
append_uleb(&c, u64(len(m.imports)))
|
|
for imp in m.imports {
|
|
append_uleb_name(&c, imp.module_name)
|
|
append_uleb_name(&c, imp.field_name)
|
|
append(&c, u8(imp.kind))
|
|
#partial switch imp.kind {
|
|
case .FUNC:
|
|
append_uleb(&c, u64(imp.index)) // typeidx
|
|
case:
|
|
// table/memory/global descriptors are not modeled from scratch.
|
|
append(errors, Error{location = 0, code = .UNSUPPORTED_FEATURE})
|
|
}
|
|
}
|
|
emit_section(out, .IMPORT, c[:])
|
|
}
|
|
|
|
// FUNCTION (typeidx per *defined* function -- those with a body)
|
|
if ndef := defined_function_count(m); ndef > 0 {
|
|
c: [dynamic]u8; c.allocator = tmp
|
|
append_uleb(&c, u64(ndef))
|
|
for f in m.functions {
|
|
if len(f.blocks) == 0 {
|
|
continue // imported: no FUNCTION entry
|
|
}
|
|
append_uleb(&c, u64(u32(f.signature)))
|
|
}
|
|
emit_section(out, .FUNCTION, c[:])
|
|
}
|
|
|
|
// EXPORT
|
|
if len(m.exports) > 0 {
|
|
c: [dynamic]u8; c.allocator = tmp
|
|
append_uleb(&c, u64(len(m.exports)))
|
|
for e in m.exports {
|
|
append_uleb_name(&c, e.name)
|
|
append(&c, u8(e.kind))
|
|
append_uleb(&c, u64(e.index))
|
|
}
|
|
emit_section(out, .EXPORT, c[:])
|
|
}
|
|
|
|
// START
|
|
if m.start >= 0 {
|
|
c: [dynamic]u8; c.allocator = tmp
|
|
append_uleb(&c, u64(u32(m.start)))
|
|
emit_section(out, .START, c[:])
|
|
}
|
|
|
|
// CODE
|
|
emit_code_section(m, out, relocs, errors)
|
|
|
|
// the remaining sections
|
|
for sec in m.sections {
|
|
switch sec.id {
|
|
case .CUSTOM, .TABLE, .MEMORY, .GLOBAL, .ELEMENT, .DATA, .DATA_COUNT:
|
|
emit_section(out, sec.id, m.data[sec.offset:][:sec.size])
|
|
case .TYPE, .IMPORT, .FUNCTION, .EXPORT, .START, .CODE:
|
|
// handled above
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
// =============================================================================
|
|
// CODE section (shared by both modes -- always regenerated from the ir ops)
|
|
// =============================================================================
|
|
|
|
@(private)
|
|
emit_code_section :: proc(m: Module, out: ^[dynamic]u8, relocs: ^[dynamic]Relocation, errors: ^[dynamic]Error) {
|
|
tmp := context.temp_allocator
|
|
|
|
ndef := defined_function_count(m)
|
|
if ndef == 0 {
|
|
return
|
|
}
|
|
|
|
contents: [dynamic]u8; contents.allocator = tmp
|
|
append_uleb(&contents, u64(ndef))
|
|
|
|
// Body relocations, collected with offsets relative to the CODE contents;
|
|
// rebased to buffer-relative once the section is placed.
|
|
code_relocs: [dynamic]Relocation; code_relocs.allocator = tmp
|
|
|
|
for f, fi in m.functions {
|
|
if len(f.blocks) == 0 {
|
|
continue // imported function: no body
|
|
}
|
|
|
|
// A WASM function body is one `expr`. Concatenate all blocks' ops (the
|
|
// decoder produces one block; a hand-built function may use several).
|
|
all_ops := f.blocks[0].ops
|
|
if len(f.blocks) > 1 {
|
|
acc: [dynamic]Operation; acc.allocator = tmp
|
|
for blk in f.blocks {
|
|
append(&acc, ..blk.ops)
|
|
}
|
|
all_ops = acc[:]
|
|
}
|
|
|
|
// body = locals vector ++ expr bytes
|
|
body: [dynamic]u8; body.allocator = tmp
|
|
locals: []Value_Type
|
|
if fi < len(m.function_locals) {
|
|
locals = m.function_locals[fi]
|
|
}
|
|
emit_locals(&body, locals)
|
|
expr_off_in_body := u32(len(body))
|
|
|
|
scratch := make([]u8, encode_max_code_size(len(all_ops)) + 16, tmp)
|
|
body_relocs: [dynamic]Relocation; body_relocs.allocator = tmp
|
|
n, _ := encode_ops(all_ops, scratch, &body_relocs, errors)
|
|
append(&body, ..scratch[:n])
|
|
|
|
// entry = uleb(body_len) ++ body
|
|
append_uleb(&contents, u64(len(body)))
|
|
body_start_in_contents := u32(len(contents))
|
|
append(&contents, ..body[:])
|
|
|
|
expr_off_in_contents := body_start_in_contents + expr_off_in_body
|
|
for rr in body_relocs {
|
|
r2 := rr
|
|
r2.offset += expr_off_in_contents
|
|
append(&code_relocs, r2)
|
|
}
|
|
}
|
|
|
|
// Emit the framed section; learn where its contents landed in the buffer.
|
|
append(out, u8(Section_Id.CODE))
|
|
append_uleb(out, u64(len(contents)))
|
|
code_contents_file_off := u32(len(out))
|
|
append(out, ..contents[:])
|
|
|
|
for rr in code_relocs {
|
|
r2 := rr
|
|
r2.offset += code_contents_file_off
|
|
append(relocs, r2)
|
|
}
|
|
}
|
|
|
|
// A function's declared locals, re-compressed into `count x type` groups (runs
|
|
// of the same value type), exactly as the code section expects.
|
|
@(private)
|
|
emit_locals :: proc(body: ^[dynamic]u8, locals: []Value_Type) {
|
|
group_count := u32(0)
|
|
#no_bounds_check for i := 0; i < len(locals); /**/ {
|
|
j := i+1
|
|
for j < len(locals) && locals[j] == locals[i] {
|
|
j += 1
|
|
}
|
|
group_count += 1
|
|
i = j
|
|
}
|
|
append_uleb(body, u64(group_count))
|
|
#no_bounds_check for i := 0; i < len(locals); /**/ {
|
|
j := i+1
|
|
for j < len(locals) && locals[j] == locals[i] {
|
|
j += 1
|
|
}
|
|
append_uleb(body, u64(j - i))
|
|
append(body, u8(locals[i]))
|
|
i = j
|
|
}
|
|
}
|
|
|
|
@(private, require_results)
|
|
defined_function_count :: proc "contextless" (m: Module) -> int {
|
|
n := 0
|
|
for f in m.functions {
|
|
if len(f.blocks) > 0 {
|
|
n += 1
|
|
}
|
|
}
|
|
return n
|
|
} |