From 8b2887d80bfc3f0a8bc42d4a9e497be64af6d9d4 Mon Sep 17 00:00:00 2001 From: l-m Date: Fri, 14 Apr 2023 03:39:55 +1000 Subject: [PATCH] wasm module: globals, constant expressions and function reference types (#17950) --- vlib/wasm/constant.v | 140 +++++++++++++++++++++++++++++++++++ vlib/wasm/encoding.v | 125 ++++++++++++++++++++++++------- vlib/wasm/functions.v | 20 +++-- vlib/wasm/instructions.v | 107 ++++++++++++++++++++------ vlib/wasm/module.v | 71 ++++++++++++++++-- vlib/wasm/tests/block_test.v | 2 +- vlib/wasm/tests/call_test.v | 2 +- vlib/wasm/tests/var_test.v | 55 ++++++++++++++ 8 files changed, 455 insertions(+), 67 deletions(-) create mode 100644 vlib/wasm/constant.v create mode 100644 vlib/wasm/tests/var_test.v diff --git a/vlib/wasm/constant.v b/vlib/wasm/constant.v new file mode 100644 index 0000000000..029eee248c --- /dev/null +++ b/vlib/wasm/constant.v @@ -0,0 +1,140 @@ +module wasm + +import encoding.leb128 + +// constexpr_value returns a constant expression that evaluates to a single value. +pub fn constexpr_value[T](v T) ConstExpression { + mut expr := ConstExpression{} + + $if T is i64 { + expr.i64_const(v) + } $else $if T is $int { + expr.i32_const(v) + } $else $if T is f32 { + expr.f32_const(v) + } $else $if T is f64 { + expr.f64_const(v) + } $else { + $compile_error('values can only be int, i32, i64, f32, f64') + } + + return expr +} + +// constexpr_ref_null returns a constant expression that evaluates to a null reference. +pub fn constexpr_ref_null(rt RefType) ConstExpression { + mut expr := ConstExpression{} + + expr.ref_null(rt) + + return expr +} + +// WebAssembly constant expressions are permitted to use a subset of valid instructions. +pub struct ConstExpression { +mut: + call_patches []CallPatch + code []u8 +} + +// i32_const places a constant i32 value on the stack. +// WebAssembly instruction: `i32.const`. +pub fn (mut expr ConstExpression) i32_const(v i32) { + expr.code << 0x41 // i32.const + expr.code << leb128.encode_i32(v) +} + +// i64_const places a constant i64 value on the stack. +// WebAssembly instruction: `i64.const`. +pub fn (mut expr ConstExpression) i64_const(v i64) { + expr.code << 0x42 // i64.const + expr.code << leb128.encode_i64(v) +} + +// f32_const places a constant f32 value on the stack. +// WebAssembly instruction: `f32.const`. +pub fn (mut expr ConstExpression) f32_const(v f32) { + expr.code << 0x43 // f32.const + push_f32(mut expr.code, v) +} + +// f64_const places a constant f64 value on the stack. +// WebAssembly instruction: `f64.const`. +pub fn (mut expr ConstExpression) f64_const(v f64) { + expr.code << 0x44 // f64.const + push_f64(mut expr.code, v) +} + +// add adds two values on the stack with type `typ`. +// WebAssembly instructions: `i32|i64.add`. +pub fn (mut expr ConstExpression) add(typ NumType) { + assert typ in [.i32_t, .i64_t] + + match typ { + .i32_t { expr.code << 0x6A } // i32.add + .i64_t { expr.code << 0x7C } // i64.add + else {} + } +} + +// sub subtracts two values on the stack with type `typ`. +// WebAssembly instructions: `i32|i64.sub`. +pub fn (mut expr ConstExpression) sub(typ NumType) { + assert typ in [.i32_t, .i64_t] + + match typ { + .i32_t { expr.code << 0x6B } // i32.sub + .i64_t { expr.code << 0x7D } // i64.sub + else {} + } +} + +// mul multiplies two values on the stack with type `typ`. +// WebAssembly instructions: `i32|i64.mul`. +pub fn (mut expr ConstExpression) mul(typ NumType) { + assert typ in [.i32_t, .i64_t] + + match typ { + .i32_t { expr.code << 0x6C } // i32.mul + .i64_t { expr.code << 0x7E } // i64.mul + else {} + } +} + +// global_get places the value of the global at the index `global` on the stack. +// Constant expressions are only allowed to refer to imported globals. +// WebAssembly instruction: `global.get`. +pub fn (mut expr ConstExpression) global_get(global GlobalImportIndex) { + expr.code << 0x23 // global.get + expr.code << leb128.encode_u32(u32(global)) +} + +// ref_null places a null reference on the stack. +// WebAssembly instruction: `ref.null`. +pub fn (mut expr ConstExpression) ref_null(rt RefType) { + expr.code << 0xD0 // ref.null + expr.code << u8(rt) +} + +// ref_func places a reference to a function with `name` on the stack. +// If this function does not exist when calling `compile` on the module, it will panic. +// WebAssembly instruction: `ref.func`. +pub fn (mut expr ConstExpression) ref_func(name string) { + expr.code << 0xD2 // ref.func + expr.call_patches << FunctionCallPatch{ + name: name + pos: expr.code.len + } +} + +// ref_func places a reference to an imported function with `name` on the stack. +// If the imported function does not exist when calling `compile` on the module, it will panic. +// WebAssembly instruction: `ref.func`. +pub fn (mut expr ConstExpression) ref_func_import(mod string, name string) { + expr.code << 0xD2 // ref.func + expr.call_patches << ImportCallPatch{ + mod: mod + name: name + pos: expr.code.len + } +} diff --git a/vlib/wasm/encoding.v b/vlib/wasm/encoding.v index f89c7a82b8..56d7684988 100644 --- a/vlib/wasm/encoding.v +++ b/vlib/wasm/encoding.v @@ -1,6 +1,7 @@ module wasm import encoding.leb128 +import math.bits fn (mut mod Module) u32(v u32) { mod.buf << leb128.encode_u32(v) @@ -34,34 +35,71 @@ fn (mut mod Module) function_type(ft FuncType) { mod.result_type(ft.results) } -fn (mut mod Module) patch_calls(ft Function) { +fn (mut mod Module) global_type(vt ValType, is_mut bool) { + mod.buf << u8(vt) + mod.buf << u8(is_mut) +} + +fn push_f32(mut buf []u8, v f32) { + rv := bits.f32_bits(v) + buf << u8(rv >> u32(0)) + buf << u8(rv >> u32(8)) + buf << u8(rv >> u32(16)) + buf << u8(rv >> u32(24)) +} + +fn push_f64(mut buf []u8, v f64) { + rv := bits.f64_bits(v) + buf << u8(rv >> u32(0)) + buf << u8(rv >> u32(8)) + buf << u8(rv >> u32(16)) + buf << u8(rv >> u32(24)) + buf << u8(rv >> u32(32)) + buf << u8(rv >> u32(40)) + buf << u8(rv >> u32(48)) + buf << u8(rv >> u32(56)) +} + +fn (mod &Module) get_function_idx(patch CallPatch) int { + mut idx := -1 + + match patch { + FunctionCallPatch { + ftt := mod.functions[patch.name] or { + panic('called function ${patch.name} does not exist') + } + idx = ftt.idx + mod.fn_imports.len + } + ImportCallPatch { + for fnidx, c in mod.fn_imports { + if c.mod == patch.mod && c.name == patch.name { + idx = fnidx + break + } + } + if idx == -1 { + panic('called imported function ${patch.mod}.${patch.name} does not exist') + } + } + } + + return idx +} + +fn (mut mod Module) patch(ft Function) { mut ptr := 0 for patch in ft.call_patches { - mut idx := -1 - - match patch { - FunctionCallPatch { - ftt := mod.functions[patch.name] or { - panic('called function ${patch.name} does not exist') - } - idx = ftt.idx + mod.fn_imports.len - } - ImportCallPatch { - for fnidx, c in mod.fn_imports { - if c.mod == patch.mod && c.name == patch.name { - idx = fnidx - break - } - } - if idx == -1 { - panic('called imported function ${patch.mod}.${patch.name} does not exist') - } - } - } + idx := mod.get_function_idx(patch) mod.buf << ft.code[ptr..patch.pos] - mod.buf << 0x10 // call + mod.u32(u32(idx)) + ptr = patch.pos + } + + for patch in ft.global_patches { + idx := mod.global_imports.len + patch.idx + mod.buf << ft.code[ptr..patch.pos] mod.u32(u32(idx)) ptr = patch.pos } @@ -93,12 +131,11 @@ pub fn (mut mod Module) compile() []u8 { } // https://webassembly.github.io/spec/core/binary/modules.html#import-section // - if mod.fn_imports.len > 0 { - // Types + if mod.fn_imports.len > 0 || mod.global_imports.len > 0 { mod.buf << u8(Section.import_section) tpatch := mod.patch_start() { - mod.u32(u32(mod.fn_imports.len)) + mod.u32(u32(mod.fn_imports.len + mod.global_imports.len)) for ft in mod.fn_imports { mod.u32(u32(ft.mod.len)) mod.buf << ft.mod.bytes() @@ -107,6 +144,14 @@ pub fn (mut mod Module) compile() []u8 { mod.buf << 0x00 // function mod.u32(u32(ft.tidx)) } + for gt in mod.global_imports { + mod.u32(u32(gt.mod.len)) + mod.buf << gt.mod.bytes() + mod.u32(u32(gt.name.len)) + mod.buf << gt.name.bytes() + mod.buf << 0x03 // global + mod.global_type(gt.typ, gt.is_mut) + } } mod.patch_len(tpatch) } @@ -141,6 +186,32 @@ pub fn (mut mod Module) compile() []u8 { } mod.patch_len(tpatch) } + // https://webassembly.github.io/spec/core/binary/modules.html#global-section + // + if mod.globals.len > 0 { + mod.buf << u8(Section.global_section) + tpatch := mod.patch_start() + { + mod.u32(u32(mod.globals.len)) + for gt in mod.globals { + mod.global_type(gt.typ, gt.is_mut) + + { + mut ptr := 0 + for patch in gt.init.call_patches { + idx := mod.get_function_idx(patch) + + mod.buf << gt.init.code[ptr..patch.pos] + mod.u32(u32(idx)) + ptr = patch.pos + } + mod.buf << gt.init.code[ptr..] + } + mod.buf << 0x0B // END expression opcode + } + } + mod.patch_len(tpatch) + } // https://webassembly.github.io/spec/core/binary/modules.html#export-section // if mod.functions.len > 0 { @@ -208,7 +279,7 @@ pub fn (mut mod Module) compile() []u8 { mod.u32(1) mod.buf << u8(lt) } - mod.patch_calls(ft) + mod.patch(ft) mod.buf << 0x0B // END expression opcode } mod.patch_len(fpatch) diff --git a/vlib/wasm/functions.v b/vlib/wasm/functions.v index 814a3e0896..93de97850f 100644 --- a/vlib/wasm/functions.v +++ b/vlib/wasm/functions.v @@ -13,16 +13,22 @@ struct FunctionCallPatch { type CallPatch = FunctionCallPatch | ImportCallPatch -struct Function { +struct FunctionGlobalPatch { + idx GlobalIndex + pos int +} + +pub struct Function { tidx int idx int mut: - call_patches []CallPatch - label int - export bool - mod &Module - code []u8 - locals []ValType + call_patches []CallPatch + global_patches []FunctionGlobalPatch + label int + export bool + mod &Module = unsafe { nil } + code []u8 + locals []ValType pub: name string } diff --git a/vlib/wasm/instructions.v b/vlib/wasm/instructions.v index 40b9ae94b0..f0a4ef6ed5 100644 --- a/vlib/wasm/instructions.v +++ b/vlib/wasm/instructions.v @@ -1,7 +1,6 @@ module wasm import encoding.leb128 -import math.bits fn (mut func Function) u32(v u32) { func.code << leb128.encode_u32(v) @@ -25,7 +24,7 @@ fn (mut func Function) blocktype(typ FuncType) { // new_local creates a function local and returns it's index. // See `local_get`, `local_set`, `local_tee`. -pub fn (mut func Function) new_local(v ValType) int { +pub fn (mut func Function) new_local(v ValType) LocalIndex { ldiff := func.mod.functypes[func.tidx].parameters.len ret := func.locals.len + ldiff @@ -51,49 +50,73 @@ pub fn (mut func Function) i64_const(v i64) { // WebAssembly instruction: `f32.const`. pub fn (mut func Function) f32_const(v f32) { func.code << 0x43 // f32.const - rv := bits.f32_bits(v) - func.code << u8(rv >> u32(0)) - func.code << u8(rv >> u32(8)) - func.code << u8(rv >> u32(16)) - func.code << u8(rv >> u32(24)) + push_f32(mut func.code, v) } // f64_const places a constant f64 value on the stack. // WebAssembly instruction: `f64.const`. pub fn (mut func Function) f64_const(v f64) { func.code << 0x44 // f64.const - rv := bits.f64_bits(v) - func.code << u8(rv >> u32(0)) - func.code << u8(rv >> u32(8)) - func.code << u8(rv >> u32(16)) - func.code << u8(rv >> u32(24)) - func.code << u8(rv >> u32(32)) - func.code << u8(rv >> u32(40)) - func.code << u8(rv >> u32(48)) - func.code << u8(rv >> u32(56)) + push_f64(mut func.code, v) } // local_get places the value of the local at the index `local` on the stack. // WebAssembly instruction: `local.get`. -pub fn (mut func Function) local_get(local int) { +pub fn (mut func Function) local_get(local LocalIndex) { func.code << 0x20 // local.get func.u32(u32(local)) } // local_get sets the local at the index `local` to the value on the stack. // WebAssembly instruction: `local.set`. -pub fn (mut func Function) local_set(local int) { +pub fn (mut func Function) local_set(local LocalIndex) { func.code << 0x21 // local.set func.u32(u32(local)) } // local_tee sets the local at the index `local` to the value on the stack, then places it's value on the stack. // WebAssembly instruction: `local.tee`. -pub fn (mut func Function) local_tee(local int) { +pub fn (mut func Function) local_tee(local LocalIndex) { func.code << 0x22 // local.tee func.u32(u32(local)) } +type GlobalIndices = GlobalImportIndex | GlobalIndex + +// global_get places the value of the global at the index `global` on the stack. +// WebAssembly instruction: `global.get`. +pub fn (mut func Function) global_get(global GlobalIndices) { + func.code << 0x23 // global.get + match global { + GlobalIndex { + func.global_patches << FunctionGlobalPatch{ + idx: global + pos: func.code.len + } + } + GlobalImportIndex { + func.u32(u32(global)) + } + } +} + +// global_set sets the global at the index `global` to the value on the stack. +// WebAssembly instruction: `global.set`. +pub fn (mut func Function) global_set(global GlobalIndices) { + func.code << 0x24 // global.set + match global { + GlobalIndex { + func.global_patches << FunctionGlobalPatch{ + idx: global + pos: func.code.len + } + } + GlobalImportIndex { + func.u32(u32(global)) + } + } +} + // drop drops the value on the stack // WebAssembly instruction: `drop`. pub fn (mut func Function) drop() { @@ -847,9 +870,10 @@ pub fn (mut func Function) c_end_if() { } // call calls a locally defined function. -// If this function does not exist when calling `compile` on the module, it panic. +// If this function does not exist when calling `compile` on the module, it will panic. // WebAssembly instruction: `call`. pub fn (mut func Function) call(name string) { + func.code << 0x10 // call func.call_patches << FunctionCallPatch{ name: name pos: func.code.len @@ -857,9 +881,10 @@ pub fn (mut func Function) call(name string) { } // call calls an imported function. -// If the imported function does not exist when calling `compile` on the module, it panic. +// If the imported function does not exist when calling `compile` on the module, it will panic. // WebAssembly instruction: `call`. pub fn (mut func Function) call_import(mod string, name string) { + func.code << 0x10 // call func.call_patches << ImportCallPatch{ mod: mod name: name @@ -1009,7 +1034,7 @@ pub fn (mut func Function) memory_grow() { // memory_init copies from a passive memory segment to the memory instance. // WebAssembly instruction: `memory.init`. -pub fn (mut func Function) memory_init(idx int) { +pub fn (mut func Function) memory_init(idx DataSegmentIndex) { func.code << 0xFC func.code << 0x08 func.u32(u32(idx)) @@ -1018,7 +1043,7 @@ pub fn (mut func Function) memory_init(idx int) { // data_drop prevents further use of a passive memory segment. // WebAssembly instruction: `data.drop`. -pub fn (mut func Function) data_drop(idx int) { +pub fn (mut func Function) data_drop(idx DataSegmentIndex) { func.code << 0xFC func.code << 0x09 func.u32(u32(idx)) @@ -1037,3 +1062,39 @@ pub fn (mut func Function) memory_copy() { pub fn (mut func Function) memory_fill() { func.code << [u8(0xFC), 0x0B, 0x00] } + +// ref_null places a null reference on the stack. +// WebAssembly instruction: `ref.null`. +pub fn (mut func Function) ref_null(rt RefType) { + func.code << 0xD0 // ref.null + func.code << u8(rt) +} + +// ref_is_null checks if the reference value on the stack is null, places an i32 boolean value on the stack. +// WebAssembly instruction: `ref_is_null`. +pub fn (mut func Function) ref_is_null(rt RefType) { + func.code << 0xD1 // ref_is_null +} + +// ref_func places a reference to a function with `name` on the stack. +// If this function does not exist when calling `compile` on the module, it will panic. +// WebAssembly instruction: `ref.func`. +pub fn (mut func Function) ref_func(name string) { + func.code << 0xD2 // ref.func + func.call_patches << FunctionCallPatch{ + name: name + pos: func.code.len + } +} + +// ref_func places a reference to an imported function with `name` on the stack. +// If the imported function does not exist when calling `compile` on the module, it will panic. +// WebAssembly instruction: `ref.func`. +pub fn (mut func Function) ref_func_import(mod string, name string) { + func.code << 0xD2 // ref.func + func.call_patches << ImportCallPatch{ + mod: mod + name: name + pos: func.code.len + } +} diff --git a/vlib/wasm/module.v b/vlib/wasm/module.v index 49fcbb41fb..25705b11f3 100644 --- a/vlib/wasm/module.v +++ b/vlib/wasm/module.v @@ -33,18 +33,39 @@ pub enum ValType as u8 { externref_t = 0x6f } +pub enum RefType as u8 { + funcref_t = 0x70 + externref_t = 0x6f +} + // Module contains the WebAssembly module. // Use the `compile` method to compile the module into a pure byte array. [heap] pub struct Module { mut: - buf []u8 - functypes []FuncType - functions map[string]Function - memory ?Memory - start ?string - fn_imports []FunctionImport - segments []DataSegment + buf []u8 + functypes []FuncType + functions map[string]Function + globals []Global + memory ?Memory + start ?string + fn_imports []FunctionImport + global_imports []GlobalImport + segments []DataSegment +} + +struct Global { + typ ValType + is_mut bool + export_name ?string + init ConstExpression +} + +struct GlobalImport { + mod string + name string + typ ValType + is_mut bool } struct FunctionImport { @@ -65,6 +86,11 @@ struct DataSegment { data []u8 } +pub type LocalIndex = int +pub type GlobalIndex = int +pub type GlobalImportIndex = int +pub type DataSegmentIndex = int + [params] pub struct FuncType { pub: @@ -138,7 +164,7 @@ pub fn (mut mod Module) commit(func Function, export bool) { } // new_data_segment inserts a new data segment at the memory index `pos`. -pub fn (mut mod Module) new_data_segment(pos int, data []u8) int { +pub fn (mut mod Module) new_data_segment(pos int, data []u8) DataSegmentIndex { len := mod.segments.len mod.segments << DataSegment{ idx: pos @@ -153,3 +179,32 @@ pub fn (mut mod Module) new_passive_data_segment(data []u8) { data: data } } + +// new_global creates a global and returns it's index. +// If `export_name` is none, the global will not be exported. +// See `global_get`, `global_set`. +pub fn (mut mod Module) new_global(export_name ?string, typ ValType, is_mut bool, init ConstExpression) GlobalIndex { + len := mod.globals.len + mod.globals << Global{ + typ: typ + is_mut: is_mut + export_name: export_name + init: init + } + return len +} + +// new_global_import imports a new global into the current module and returns it's index. +// See `global_import_get`, `global_import_set`. +pub fn (mut mod Module) new_global_import(modn string, name string, typ ValType, is_mut bool) GlobalImportIndex { + assert !mod.fn_imports.any(it.mod == modn && it.name == name) + + len := mod.global_imports.len + mod.global_imports << GlobalImport{ + mod: modn + name: name + typ: typ + is_mut: is_mut + } + return len +} diff --git a/vlib/wasm/tests/block_test.v b/vlib/wasm/tests/block_test.v index 743e974163..b478d48f1c 100644 --- a/vlib/wasm/tests/block_test.v +++ b/vlib/wasm/tests/block_test.v @@ -22,7 +22,7 @@ fn validate(mod []u8) ! { proc.close() } -fn test_add() { +fn test_block() { mut m := wasm.Module{} mut a1 := m.new_function('param', [], [.i32_t]) { diff --git a/vlib/wasm/tests/call_test.v b/vlib/wasm/tests/call_test.v index 7ee31dcd0e..00ada214de 100644 --- a/vlib/wasm/tests/call_test.v +++ b/vlib/wasm/tests/call_test.v @@ -22,7 +22,7 @@ fn validate(mod []u8) ! { proc.close() } -fn test_add() { +fn test_call() { mut m := wasm.Module{} mut a1 := m.new_function('const-i32', [], [.i32_t]) { diff --git a/vlib/wasm/tests/var_test.v b/vlib/wasm/tests/var_test.v new file mode 100644 index 0000000000..055a6b9710 --- /dev/null +++ b/vlib/wasm/tests/var_test.v @@ -0,0 +1,55 @@ +import wasm +import os + +const exe = os.find_abs_path_of_executable('wasm-validate') or { exit(0) } + +fn validate(mod []u8) ! { + mut proc := os.new_process(exe) + proc.set_args(['-']) + proc.set_redirect_stdio() + proc.run() + { + os.fd_write(proc.stdio_fd[0], mod.bytestr()) + os.fd_close(proc.stdio_fd[0]) + } + proc.wait() + if proc.status != .exited { + return error('wasm-validate exited abormally') + } + if proc.code != 0 { + return error('wasm-validate exited with a non zero exit code') + } + proc.close() +} + +fn test_globals() { + mut m := wasm.Module{} + + vsp := m.new_global('__vsp', .i32_t, true, wasm.constexpr_value(10)) + mut func := m.new_function('vsp', [], [.i32_t]) + { + func.global_get(vsp) + func.i32_const(20) + func.add(.i32_t) + func.global_set(vsp) + func.global_get(vsp) + } + m.commit(func, true) + + fref := m.new_global('__ref', .funcref_t, true, wasm.constexpr_ref_null(.funcref_t)) + mut func1 := m.new_function('ref', [], []) + { + func1.ref_func('vsp') + func1.global_set(fref) + } + m.commit(func1, true) + + gimport := m.new_global_import('env', '__import', .f64_t, false) + mut func2 := m.new_function('import', [], [.f64_t]) + { + func2.global_get(gimport) + } + m.commit(func2, true) + + validate(m.compile()) or { panic(err) } +}