// rexcode ยท Brendan Punsky (dotbmp@github), original author // Ginger Bill (gingerBill@github) package rexcode_wasm import "base:runtime" import "core:strings" // ============================================================================= // SECTION: Container decode (a whole .wasm binary module -> ir Module) // ============================================================================= // // `decode_ops` / `decode_expr` (decoder.odin) turn a single WASM `expr` -- one // instruction stream -- into ir.Operations. That is only the *code* of one // function; a real `.wasm` file is a container: an 8-byte header (`\0asm`, a // version) followed by a sequence of length-prefixed sections (type / import / // function / table / memory / global / export / start / element / code / data, // plus custom sections). This file is the missing outer layer: `decode` reads // that whole container and populates the full `wasm.Module`. // // How the container maps onto the ir core: // // * TYPE -> `func_types`. // * FUNCTION -> the typeidx per defined function (folded into signatures). // * CODE -> for *each* defined function, the locals go to // `function_locals` and the body `expr` is decoded (via the // shared `decode_ops`) into a single `ir.Block` of Operations // under an `ir.Function`. This is the plural fix: the old // `decode` produced one function; a module has many. // * IMPORT -> `imports`; imported functions also occupy the low function // indices (with empty bodies) so the funcidx space is intact. // * EXPORT -> `exports`, and names attached to the referenced functions. // * START -> `start`. // * reloc.* -> `relocations`; the CODE group is threaded into body decode // so relocatable index fields decode as symbolic refs. // // TABLE / MEMORY / GLOBAL / ELEMENT / DATA are recorded as `sections` and left // re-readable from `data` (the shared core has no structural slot for them and // there is, as yet, no container *emitter* -- the codec's symmetric unit is the // instruction stream, mirroring the original which only parsed the container). // // Structural problems surface as an `ir.Error` (BUFFER_TOO_SHORT for a // truncation, MALFORMED_MODULE otherwise) and `ok = false`; per-instruction // body errors are pushed by `decode_ops` as usual. // decode: parse a whole `.wasm` binary module into `m`. Returns the number of // bytes consumed (the file length on success) and whether it fully succeeded. decode :: proc( data: []u8, m: ^Module, errors: ^[dynamic]Error, allocator := context.allocator, ) -> (byte_count: u32, ok: bool) { context.allocator = allocator errors_start := u32(len(errors)) m.base.dataflow = .STACK m.version = WASM_VERSION m.start = -1 m.data = data if err := parse_container(data, m, errors, allocator); err != nil { append(errors, Error{location = 0, code = parse_error_code(err)}) return 0, false } byte_count = u32(len(data)) ok = u32(len(errors)) == errors_start return } @(private, require_results) parse_error_code :: proc "contextless" (err: Reader_Error) -> Error_Code { #partial switch e in err { case Parse_Error: if e == .TRUNCATED { return .BUFFER_TOO_SHORT } } return .MALFORMED_MODULE // allocation failure / anything else } // ============================================================================= // Container walk // ============================================================================= parse_container :: proc(data: []u8, m: ^Module, errors: ^[dynamic]Error, allocator: runtime.Allocator) -> Reader_Error { r := reader(data, 0) if (rd_u32le_block(&r) or_else 0) != WASM_MAGIC { return .BAD_MAGIC } m.version = rd_u32le_block(&r) or_return // Pass 1: collect every section header (id, content offset, size, count). secs: [dynamic]Section secs.allocator = allocator for r.off < u32(len(data)) { id := Section_Id(rd_byte(&r) or_return) size := rd_u32(&r) or_return content := r.off if content + size > u32(len(data)) { return .BAD_SECTION } sec := Section{id = id, offset = content, size = size} switch id { case .CUSTOM: sub := reader(data, content) sec.name = rd_name(&sub) or_return case .START: // funcidx only, no vector count case .TYPE, .IMPORT, .FUNCTION, .TABLE, .MEMORY, .GLOBAL, .EXPORT, .ELEMENT, .CODE, .DATA, .DATA_COUNT: sub := reader(data, content) sec.count = rd_u32(&sub) or_return } append(&secs, sec) or_return r.off = content + size } m.sections = secs[:] // Pass 2: parse the structured sections. func_typeidx: []u32 codes: []Code_Body for &sec in m.sections { s := reader(data, sec.offset) #partial switch sec.id { case .TYPE: m.func_types = parse_types (&s, allocator) or_return case .IMPORT: m.imports = parse_imports (&s, allocator) or_return case .FUNCTION: func_typeidx = parse_function_section(&s, allocator) or_return case .EXPORT: m.exports = parse_exports (&s, allocator) or_return case .CODE: codes = parse_code (&s, allocator) or_return case .START: m.start = i64(rd_u32(&s) or_return) } } parse_custom_sections(m, allocator) or_return // Relocations must be parsed before the bodies: the CODE group is threaded // into body decode so relocatable index fields become symbolic refs. m.relocations = parse_relocations(m^, allocator) or_return build_functions(m, func_typeidx, codes, errors, allocator) or_return apply_name_section(m) return nil } // ============================================================================= // Byte reader (container-level, distinct from leb.odin's stream primitives: // it tracks position and reports structural errors -- TRUNCATED / BAD_*) // ============================================================================= Parse_Error :: enum u8 { NONE = 0, TRUNCATED, // read past the end of the buffer BAD_MAGIC, // not a `\0asm` module BAD_TYPE_FORM, // a functype did not start with 0x60 BAD_SECTION, // section contents extend past the buffer BAD_ULEB, // a ULEB did not terminate within 10 bytes } Reader_Error :: union #shared_nil { Parse_Error, runtime.Allocator_Error, } Reader :: struct { data: []u8, off: u32, } @(private, require_results) reader :: proc "contextless" (data: []u8, off: u32) -> Reader { return Reader{data = data, off = off} } @(private, require_results) rd_byte :: proc "contextless" (r: ^Reader) -> (u8, Parse_Error) { if r.off >= u32(len(r.data)) { return 0, .TRUNCATED } b := r.data[r.off] r.off += 1 return b, .NONE } @(private, require_results) rd_u32le_block :: proc "contextless" (r: ^Reader) -> (u32, Parse_Error) { if r.off + 4 > u32(len(r.data)) { return 0, .TRUNCATED } v := u32(r.data[r.off]) | u32(r.data[r.off+1]) << 8 | u32(r.data[r.off+2]) << 16 | u32(r.data[r.off+3]) << 24 r.off += 4 return v, .NONE } @(private, require_results) rd_uleb :: proc "contextless" (r: ^Reader) -> (u64, Parse_Error) { shift: uint = 0 value: u64 = 0 for _ in 0..<10 { if r.off >= u32(len(r.data)) { return 0, .TRUNCATED } b := r.data[r.off] r.off += 1 value |= u64(b & 0x7F) << shift if b & 0x80 == 0 { return value, .NONE } shift += 7 } return 0, .BAD_ULEB } @(private, require_results) rd_sleb :: proc "contextless" (r: ^Reader) -> (i64, Parse_Error) { shift: uint = 0 value: i64 = 0 b: u8 = 0 for _ in 0..<10 { if r.off >= u32(len(r.data)) { return 0, .TRUNCATED } b = r.data[r.off] r.off += 1 value |= i64(b & 0x7F) << shift shift += 7 if b & 0x80 == 0 { break } } if shift < 64 && (b & 0x40) != 0 { value |= -(i64(1) << shift) } return value, .NONE } @(private, require_results) rd_u32 :: proc "contextless" (r: ^Reader) -> (u32, Parse_Error) { v, err := rd_uleb(r) return u32(v), err } @(private, require_results) rd_name :: proc "contextless" (r: ^Reader) -> (val: string, err: Parse_Error) { n := rd_u32(r) or_return if r.off + n > u32(len(r.data)) { err = .TRUNCATED; return } val = string(r.data[r.off:][:n]) r.off += n return } @(private, require_results) rd_valtype_vec :: proc(r: ^Reader, allocator: runtime.Allocator) -> (out: []Value_Type, err: Reader_Error) { n := rd_u32(r) or_return out = make([]Value_Type, int(n), allocator) or_return for &v in out { v = Value_Type(rd_byte(r) or_return) } return } @(private, require_results) rd_limits :: proc "contextless" (r: ^Reader) -> (min: u64, max: Maybe(u64), err: Parse_Error) { flags := rd_byte(r) or_return min = rd_uleb(r) or_return if flags & 0x01 != 0 { max = rd_uleb(r) or_return } return } // ============================================================================= // Structured section parsers // ============================================================================= @(private, require_results) parse_types :: proc(r: ^Reader, allocator: runtime.Allocator) -> (out: []Func_Type, err: Reader_Error) { n := rd_u32(r) or_return out = make([]Func_Type, int(n), allocator) or_return for &ft in out { form := rd_byte(r) or_return if form != 0x60 { return out, .BAD_TYPE_FORM } ft.params = rd_valtype_vec(r, allocator) or_return ft.results = rd_valtype_vec(r, allocator) or_return } return } @(private, require_results) parse_imports :: proc(r: ^Reader, allocator: runtime.Allocator) -> (out: []Import, err: Reader_Error) { n := rd_u32(r) or_return out = make([]Import, int(n), allocator) or_return for &imp in out { imp.module_name = rd_name(r) or_return imp.field_name = rd_name(r) or_return imp.kind = External_Kind(rd_byte(r) or_return) switch imp.kind { case .FUNC: imp.index = rd_u32(r) or_return case .TABLE: _ = rd_byte(r) or_return // reftype _, _ = rd_limits(r) or_return case .MEMORY: _, _ = rd_limits(r) or_return case .GLOBAL: _ = rd_byte(r) or_return // valtype _ = rd_byte(r) or_return // mutability } } return } @(private, require_results) parse_function_section :: proc(r: ^Reader, allocator: runtime.Allocator) -> (out: []u32, err: Reader_Error) { n := rd_u32(r) or_return out = make([]u32, int(n), allocator) or_return for &idx in out { idx = rd_u32(r) or_return } return } @(private, require_results) parse_exports :: proc(r: ^Reader, allocator: runtime.Allocator) -> (out: []Export, err: Reader_Error) { n := rd_u32(r) or_return out = make([]Export, int(n), allocator) or_return for &e in out { e.name = rd_name(r) or_return e.kind = External_Kind(rd_byte(r) or_return) e.index = rd_u32(r) or_return } return } // One code entry's locals (still compressed as `count x type` groups) and the // file span of its body `expr`. @(private) Code_Body :: struct { locals: []Local_Group, body_offset: u32, // file offset of the instruction stream body_size: u32, // instruction-stream length in bytes } @(private) Local_Group :: struct { count: u32, type: Value_Type, } @(private, require_results) parse_code :: proc(r: ^Reader, allocator: runtime.Allocator) -> (out: []Code_Body, err: Reader_Error) { n := rd_u32(r) or_return out = make([]Code_Body, int(n), allocator) or_return for &cb in out { total := rd_u32(r) or_return body_end := r.off + total nl := rd_u32(r) or_return locals := make([]Local_Group, int(nl), allocator) or_return for &g in locals { g.count = rd_u32(r) or_return g.type = Value_Type(rd_byte(r) or_return) } cb = Code_Body{ locals = locals, body_offset = r.off, body_size = body_end > r.off ? body_end - r.off : 0, } r.off = body_end // jump past the expr to the next entry } return } @(private, require_results) parse_custom_sections :: proc(m: ^Module, allocator: runtime.Allocator) -> Reader_Error { custom_count := 0 for &sec in m.sections { if sec.id == .CUSTOM { custom_count += 1 } } m.customs = make([]Custom_Section, custom_count, allocator) or_return custom_index := 0 for &sec in m.sections { if sec.id != .CUSTOM { continue } custom := &m.customs[custom_index] custom_index += 1 custom.section = sec custom.payload = m.data[sec.offset:][:sec.size] r := reader(custom.payload, 0) sec_name := rd_name(&r) or_continue assert(sec_name == sec.name) if strings.has_prefix(sec.name, ".debug_") { // DWARF debug stuff } else if strings.has_prefix(sec.name, "reloc.") { // other relocation stuff } custom_block: switch sec.name { case "linking": // not yet handled case "producers": // not yet handled case "dynlink.0": // not yet handled case "external_debug_info", "sourceMappingURL": // not yet handled, but debugging related case "metadata.code.branch_hint": // not yet handled case "name": cname: Custom_Section_Name defer custom.variant = cname for r.off < u32(len(r.data)) { id := rd_byte(&r) or_return size := rd_u32(&r) or_return end_off := r.off+size defer r.off = end_off switch id { case 0: // module cname.module_name = rd_name(&r) or_return case 1: // functions count := rd_u32(&r) or_return cname.functions = make([]Custom_Section_Name_Function, count, allocator) or_return for &func in cname.functions { func.id = rd_u32(&r) or_return func.name = rd_name(&r) or_return } case 2: // locals count := rd_u32(&r) or_return cname.locals = make([]Custom_Section_Name_Function_Locals, count, allocator) or_return for &local_func in cname.locals { local_func.func_idx = rd_u32(&r) or_return local_count := rd_u32(&r) or_return local_func.locals = make([]Custom_Section_Name_Local, local_count, allocator) or_return for &local in local_func.locals { local.idx = rd_u32(&r) or_return local.name = rd_name(&r) or_return } } } } case "target_features": target_features: Custom_Section_Target_Features defer custom.variant = target_features count := rd_u32(&r) or_return target_features.features = make([]Custom_Section_Target_Feature, count, allocator) or_return for &feature in target_features.features { feature.prefix = Custom_Section_Target_Feature_Prefix(rd_byte(&r) or_return) feature.feature = rd_name(&r) or_return } } } return nil } // ============================================================================= // Function index space (imports ++ defined) with eagerly-decoded bodies // ============================================================================= @(private, require_results) build_functions :: proc( m: ^Module, func_typeidx: []u32, codes: []Code_Body, errors: ^[dynamic]Error, allocator: runtime.Allocator, ) -> Reader_Error { num_imports := 0 for imp in m.imports { if imp.kind == .FUNC { num_imports += 1 } } total := num_imports + len(func_typeidx) if total == 0 { return nil } funcs := make([]Function, total, allocator) or_return locals := make([][]Value_Type, total, allocator) or_return // CODE-section file offset, to rebase its relocations to body-relative. code_off: u32 = 0 for s in m.sections { if s.id == .CODE { code_off = s.offset; break } } code_relocs := relocations_for_section(m.relocations, .CODE) // Imported functions: low indices, no body. idx := 0 for imp in m.imports { (imp.kind == .FUNC) or_continue funcs[idx] = Function{ name = imp.field_name, signature = Type_Ref(imp.index), // typeidx into func_types } idx += 1 } // Defined functions: decode each body into a single ir.Block. for tidx, i in func_typeidx { fi := num_imports + i f := Function{signature = Type_Ref(tidx)} if i < len(codes) { cb := codes[i] // Expand the compressed local groups to a flat value-type list. nlocals := 0 for g in cb.locals { nlocals += int(g.count) } flat := make([]Value_Type, nlocals, allocator) or_return w := 0 for g in cb.locals { for _ in 0.. []Relocation { if len(all) == 0 { return nil } lo := body_off_in_sec hi := body_off_in_sec + size out: [dynamic]Relocation out.allocator = allocator for rr in all { if rr.offset >= lo && rr.offset < hi { r2 := rr r2.offset = rr.offset - lo append(&out, r2) } } return out[:] } // ============================================================================= // Relocations (object-file `reloc.*` custom sections) // ============================================================================= @(private, require_results) parse_relocations :: proc(m: Module, allocator: runtime.Allocator) -> (groups_out: []Reloc_Group, err: Reader_Error) { groups: [dynamic]Reloc_Group groups.allocator = allocator for sec in m.sections { if !(sec.id == .CUSTOM && strings.has_prefix(sec.name, "reloc.")) { continue } r := reader(m.data[sec.offset:][:sec.size], 0) _ = rd_name(&r) or_return // step past the custom-section name target := Section_Id(rd_u32(&r) or_return) count := rd_u32(&r) or_return out := make([]Relocation, int(count), allocator) or_return w := 0 for _ in 0..