diff --git a/src/binaryen-c.cpp b/src/binaryen-c.cpp index b4271f5663e..a398655416d 100644 --- a/src/binaryen-c.cpp +++ b/src/binaryen-c.cpp @@ -508,6 +508,9 @@ BinaryenFeatures BinaryenFeatureMultibyte(void) { BinaryenFeatures BinaryenFeatureCustomPageSizes(void) { return static_cast(FeatureSet::CustomPageSizes); } +BinaryenFeatures BinaryenFeatureCompactImports(void) { + return static_cast(FeatureSet::CompactImports); +} BinaryenFeatures BinaryenFeatureAll(void) { return static_cast(FeatureSet::All); } diff --git a/src/binaryen-c.h b/src/binaryen-c.h index 80d0451796d..1b6c3d337d8 100644 --- a/src/binaryen-c.h +++ b/src/binaryen-c.h @@ -246,6 +246,7 @@ BINARYEN_API BinaryenFeatures BinaryenFeatureCallIndirectOverlong(void); BINARYEN_API BinaryenFeatures BinaryenFeatureRelaxedAtomics(void); BINARYEN_API BinaryenFeatures BinaryenFeatureMultibyte(void); BINARYEN_API BinaryenFeatures BinaryenFeatureCustomPageSizes(void); +BINARYEN_API BinaryenFeatures BinaryenFeatureCompactImports(void); BINARYEN_API BinaryenFeatures BinaryenFeatureAll(void); // Modules diff --git a/src/js/binaryen.js-post.js b/src/js/binaryen.js-post.js index 395eca4d1da..8a6ca1f3670 100644 --- a/src/js/binaryen.js-post.js +++ b/src/js/binaryen.js-post.js @@ -194,6 +194,7 @@ function initializeConstants() { 'CallIndirectOverlong', 'RelaxedAtomics', 'CustomPageSizes', + 'CompactImports', 'All' ].forEach(name => { Module['Features'][name] = Module['_BinaryenFeature' + name](); diff --git a/src/tools/tool-options.h b/src/tools/tool-options.h index cc39daad4fc..6bb85753029 100644 --- a/src/tools/tool-options.h +++ b/src/tools/tool-options.h @@ -112,6 +112,7 @@ struct ToolOptions : public Options { .addFeature(FeatureSet::RelaxedAtomics, "acquire/release atomic memory operations") .addFeature(FeatureSet::CustomPageSizes, "custom page sizes") + .addFeature(FeatureSet::CompactImports, "compact import section") .add("--enable-typed-function-references", "", "Deprecated compatibility flag", diff --git a/src/wasm-binary.h b/src/wasm-binary.h index af031496e9a..b581f42f6c5 100644 --- a/src/wasm-binary.h +++ b/src/wasm-binary.h @@ -471,6 +471,7 @@ extern const char* CustomDescriptorsFeature; extern const char* RelaxedAtomicsFeature; extern const char* MultibyteFeature; extern const char* CustomPageSizesFeature; +extern const char* CompactImportsFeature; enum Subsection { NameModule = 0, @@ -1663,6 +1664,12 @@ class WasmBinaryReader { Address defaultIfNoMax); void readImports(); + void addImport(uint32_t kind, std::unique_ptr importable); + std::unique_ptr + readImportDetails(Name module, Name field, uint32_t kind); + std::unique_ptr copyImportable(uint32_t kind, + Importable& details); + // The signatures of each function, including imported functions, given in the // import and function sections. Store HeapTypes instead of Signatures because // reconstructing the HeapTypes from the Signatures is expensive. diff --git a/src/wasm-features.h b/src/wasm-features.h index 056e6a4ab55..ade92658ac4 100644 --- a/src/wasm-features.h +++ b/src/wasm-features.h @@ -58,11 +58,12 @@ struct FeatureSet { RelaxedAtomics = 1 << 22, CustomPageSizes = 1 << 23, Multibyte = 1 << 24, + CompactImports = 1 << 25, MVP = None, // Keep in sync with llvm default features: // https://github.com/llvm/llvm-project/blob/c7576cb89d6c95f03968076e902d3adfd1996577/clang/lib/Basic/Targets/WebAssembly.cpp#L150-L153 Default = SignExt | MutableGlobals, - All = (1 << 25) - 1, + All = (1 << 26) - 1, }; static std::string toString(Feature f) { @@ -117,6 +118,8 @@ struct FeatureSet { return "custom-page-sizes"; case Multibyte: return "multibyte"; + case CompactImports: + return "compact-imports"; case MVP: case Default: case All: @@ -180,6 +183,7 @@ struct FeatureSet { bool hasRelaxedAtomics() const { return (features & RelaxedAtomics) != 0; } bool hasCustomPageSizes() const { return (features & CustomPageSizes) != 0; } bool hasMultibyte() const { return (features & Multibyte) != 0; } + bool hasCompactImports() const { return (features & CompactImports) != 0; } bool hasAll() const { return (features & All) != 0; } void set(FeatureSet f, bool v = true) { @@ -208,6 +212,7 @@ struct FeatureSet { void setCustomDescriptors(bool v = true) { set(CustomDescriptors, v); } void setRelaxedAtomics(bool v = true) { set(RelaxedAtomics, v); } void setMultibyte(bool v = true) { set(Multibyte, v); } + void setCompactImports(bool v = true) { set(CompactImports, v); } void setMVP() { features = MVP; } void setAll() { features = All; } diff --git a/src/wasm/wasm-binary.cpp b/src/wasm/wasm-binary.cpp index f5a141ea0fb..2b9d4025283 100644 --- a/src/wasm/wasm-binary.cpp +++ b/src/wasm/wasm-binary.cpp @@ -1478,6 +1478,8 @@ void WasmBinaryWriter::writeFeaturesSection() { return BinaryConsts::CustomSections::RelaxedAtomicsFeature; case FeatureSet::CustomPageSizes: return BinaryConsts::CustomSections::CustomPageSizesFeature; + case FeatureSet::CompactImports: + return BinaryConsts::CustomSections::CompactImportsFeature; case FeatureSet::None: case FeatureSet::Default: case FeatureSet::All: @@ -2986,129 +2988,215 @@ void WasmBinaryReader::getResizableLimits(Address& initial, } } +void WasmBinaryReader::addImport(uint32_t kind, + std::unique_ptr details) { + switch (kind) { + case ExternalKind::Function: + case ExternalKind::Function | BinaryConsts::ExactImport: { + std::unique_ptr func(static_cast(details.release())); + auto [name, isExplicit] = + getOrMakeName(functionNames, + wasm.functions.size(), + makeName("fimport$", wasm.functions.size()), + usedFunctionNames); + func->name = name; + func->hasExplicitName = isExplicit; + functionTypes.push_back(func->type.getHeapType()); + wasm.addFunction(std::move(func)); + break; + } + case ExternalKind::Table: { + std::unique_ptr table(static_cast(details.release())); + auto [name, isExplicit] = + getOrMakeName(tableNames, + wasm.tables.size(), + makeName("timport$", wasm.tables.size()), + usedTableNames); + table->name = name; + table->hasExplicitName = isExplicit; + wasm.addTable(std::move(table)); + break; + } + case ExternalKind::Memory: { + std::unique_ptr memory(static_cast(details.release())); + auto [name, isExplicit] = + getOrMakeName(memoryNames, + wasm.memories.size(), + makeName("mimport$", wasm.memories.size()), + usedMemoryNames); + memory->name = name; + memory->hasExplicitName = isExplicit; + wasm.addMemory(std::move(memory)); + break; + } + case ExternalKind::Global: { + std::unique_ptr global(static_cast(details.release())); + auto [name, isExplicit] = + getOrMakeName(globalNames, + wasm.globals.size(), + makeName("gimport$", wasm.globals.size()), + usedGlobalNames); + global->name = name; + global->hasExplicitName = isExplicit; + wasm.addGlobal(std::move(global)); + break; + } + case ExternalKind::Tag: { + std::unique_ptr tag(static_cast(details.release())); + auto [name, isExplicit] = + getOrMakeName(tagNames, + wasm.tags.size(), + makeName("eimport$", wasm.tags.size()), + usedTagNames); + tag->name = name; + tag->hasExplicitName = isExplicit; + wasm.addTag(std::move(tag)); + break; + } + default: + WASM_UNREACHABLE("unexpected kind"); + } +} + +template +std::unique_ptr copyImportable(Importable& details) { + auto item = std::make_unique(); + *item = static_cast(details); + return item; +} + +std::unique_ptr +WasmBinaryReader::copyImportable(uint32_t kind, Importable& details) { + switch (kind) { + case ExternalKind::Function: + case ExternalKind::Function | BinaryConsts::ExactImport: + return wasm::copyImportable(details); + case ExternalKind::Table: + return wasm::copyImportable
(details); + case ExternalKind::Memory: + return wasm::copyImportable(details); + case ExternalKind::Global: + return wasm::copyImportable(details); + case ExternalKind::Tag: + return wasm::copyImportable(details); + } + WASM_UNREACHABLE("unexpected kind"); +} + +std::unique_ptr +WasmBinaryReader::readImportDetails(Name module, Name base, uint32_t kind) { + Builder builder(wasm); + // We set a unique prefix for the name based on the kind. This ensures no + // collisions between them, which can't occur here (due to the index i) but + // could occur later due to the names section. + switch (kind) { + case ExternalKind::Function: + case ExternalKind::Function | BinaryConsts::ExactImport: { + auto index = getU32LEB(); + auto type = getTypeByIndex(index); + if (!type.isSignature()) { + throwError(std::string("Imported function ") + module.toString() + '.' + + base.toString() + + "'s type must be a signature. Given: " + type.toString()); + } + auto exact = (kind & BinaryConsts::ExactImport) ? Exact : Inexact; + auto curr = builder.makeFunction("", Type(type, NonNullable, exact), {}); + curr->module = module; + curr->base = base; + setLocalNames(*curr, wasm.functions.size()); + return curr; + } + case ExternalKind::Table: { + auto table = builder.makeTable(""); + table->module = module; + table->base = base; + table->type = getType(); + + bool is_shared; + uint8_t page_size = 0xff; + getResizableLimits(table->initial, + table->max, + is_shared, + table->addressType, + page_size, + Table::kUnlimitedSize); + if (is_shared) { + throwError("Tables may not be shared"); + } + if (page_size != 0xff) { + throwError("Tables may not have a custom page size"); + } + return table; + } + case ExternalKind::Memory: { + auto memory = builder.makeMemory(""); + memory->module = module; + memory->base = base; + getResizableLimits(memory->initial, + memory->max, + memory->shared, + memory->addressType, + memory->pageSizeLog2, + Memory::kUnlimitedSize); + return memory; + } + case ExternalKind::Global: { + auto type = getConcreteType(); + auto mutable_ = getU32LEB(); + if (mutable_ & ~1) { + throwError("Global mutability must be 0 or 1"); + } + auto curr = builder.makeGlobal( + "", type, nullptr, mutable_ ? Builder::Mutable : Builder::Immutable); + curr->module = module; + curr->base = base; + return curr; + } + case ExternalKind::Tag: { + getInt8(); // Reserved 'attribute' field + auto index = getU32LEB(); + auto curr = builder.makeTag("", getSignatureByTypeIndex(index)); + curr->module = module; + curr->base = base; + return curr; + } + default: { + throwError("bad import kind"); + } + } +} + void WasmBinaryReader::readImports() { size_t num = getU32LEB(); - Builder builder(wasm); for (size_t i = 0; i < num; i++) { auto module = getInlineString(); auto base = getInlineString(); - auto kind = getU32LEB(); - // We set a unique prefix for the name based on the kind. This ensures no - // collisions between them, which can't occur here (due to the index i) but - // could occur later due to the names section. - switch (kind) { - case ExternalKind::Function: - case ExternalKind::Function | BinaryConsts::ExactImport: { - auto [name, isExplicit] = - getOrMakeName(functionNames, - wasm.functions.size(), - makeName("fimport$", wasm.functions.size()), - usedFunctionNames); - auto index = getU32LEB(); - functionTypes.push_back(getTypeByIndex(index)); - auto type = getTypeByIndex(index); - if (!type.isSignature()) { - throwError(std::string("Imported function ") + module.toString() + - '.' + base.toString() + - "'s type must be a signature. Given: " + type.toString()); - } - auto exact = (kind & BinaryConsts::ExactImport) ? Exact : Inexact; - auto curr = - builder.makeFunction(name, Type(type, NonNullable, exact), {}); - curr->hasExplicitName = isExplicit; - curr->module = module; - curr->base = base; - setLocalNames(*curr, wasm.functions.size()); - wasm.addFunction(std::move(curr)); - break; + auto kind = getInt8(); + if (base == "" && (kind == 0x7F || kind == 0x7E)) { + if (!wasm.features.hasCompactImports()) { + throwError("compact imports not supported"); } - case ExternalKind::Table: { - auto [name, isExplicit] = - getOrMakeName(tableNames, - wasm.tables.size(), - makeName("timport$", wasm.tables.size()), - usedTableNames); - auto table = builder.makeTable(name); - table->hasExplicitName = isExplicit; - table->module = module; - table->base = base; - table->type = getType(); - bool is_shared; - uint8_t page_size = 0xff; - getResizableLimits(table->initial, - table->max, - is_shared, - table->addressType, - page_size, - Table::kUnlimitedSize); - if (is_shared) { - throwError("Tables may not be shared"); + if (kind == 0x7F) { + size_t numCompactImports = getU32LEB(); + while (numCompactImports--) { + base = getInlineString(); + kind = getInt8(); + auto details = readImportDetails(module, base, kind); + addImport(kind, std::move(details)); } - if (page_size != 0xff) { - throwError("Tables may not have a custom page size"); + } else { + kind = getInt8(); + auto base_details = readImportDetails(module, base, kind); + size_t numCompactImports = getU32LEB(); + while (numCompactImports--) { + auto details = copyImportable(kind, *base_details); + details->base = getInlineString(); + addImport(kind, std::move(details)); } - wasm.addTable(std::move(table)); - break; - } - case ExternalKind::Memory: { - auto [name, isExplicit] = - getOrMakeName(memoryNames, - wasm.memories.size(), - makeName("mimport$", wasm.memories.size()), - usedMemoryNames); - auto memory = builder.makeMemory(name); - memory->hasExplicitName = isExplicit; - memory->module = module; - memory->base = base; - getResizableLimits(memory->initial, - memory->max, - memory->shared, - memory->addressType, - memory->pageSizeLog2, - Memory::kUnlimitedSize); - wasm.addMemory(std::move(memory)); - break; - } - case ExternalKind::Global: { - auto [name, isExplicit] = - getOrMakeName(globalNames, - wasm.globals.size(), - makeName("gimport$", wasm.globals.size()), - usedGlobalNames); - auto type = getConcreteType(); - auto mutable_ = getU32LEB(); - if (mutable_ & ~1) { - throwError("Global mutability must be 0 or 1"); - } - auto curr = - builder.makeGlobal(name, - type, - nullptr, - mutable_ ? Builder::Mutable : Builder::Immutable); - curr->hasExplicitName = isExplicit; - curr->module = module; - curr->base = base; - wasm.addGlobal(std::move(curr)); - break; - } - case ExternalKind::Tag: { - auto [name, isExplicit] = - getOrMakeName(tagNames, - wasm.tags.size(), - makeName("eimport$", wasm.tags.size()), - usedTagNames); - getInt8(); // Reserved 'attribute' field - auto index = getU32LEB(); - auto curr = builder.makeTag(name, getSignatureByTypeIndex(index)); - curr->hasExplicitName = isExplicit; - curr->module = module; - curr->base = base; - wasm.addTag(std::move(curr)); - break; - } - default: { - throwError("bad import kind"); } + } else { + auto details = readImportDetails(module, base, kind); + addImport(kind, std::move(details)); } } numFuncImports = wasm.functions.size(); @@ -5433,6 +5521,8 @@ void WasmBinaryReader::readFeatures(size_t sectionPos, size_t payloadLen) { feature = FeatureSet::RelaxedAtomics; } else if (name == BinaryConsts::CustomSections::CustomPageSizesFeature) { feature = FeatureSet::CustomPageSizes; + } else if (name == BinaryConsts::CustomSections::CompactImportsFeature) { + feature = FeatureSet::CompactImports; } else { // Silently ignore unknown features (this may be and old binaryen running // on a new wasm). diff --git a/src/wasm/wasm.cpp b/src/wasm/wasm.cpp index b9df7a232f6..95a727046ce 100644 --- a/src/wasm/wasm.cpp +++ b/src/wasm/wasm.cpp @@ -64,6 +64,7 @@ const char* CustomDescriptorsFeature = "custom-descriptors"; const char* RelaxedAtomicsFeature = "relaxed-atomics"; const char* MultibyteFeature = "multibyte"; const char* CustomPageSizesFeature = "custom-page-sizes"; +const char* CompactImportsFeature = "compact-imports"; } // namespace BinaryConsts::CustomSections diff --git a/test/binaryen.js/kitchen-sink.js.txt b/test/binaryen.js/kitchen-sink.js.txt index d46df456ba3..876ba4834c3 100644 --- a/test/binaryen.js/kitchen-sink.js.txt +++ b/test/binaryen.js/kitchen-sink.js.txt @@ -35,7 +35,7 @@ Features.Strings: 16384 Features.MultiMemory: 32768 Features.RelaxedAtomics: 4194304 Features.CustomPageSizes: 8388608 -Features.All: 33554431 +Features.All: 67108863 InvalidId: 0 BlockId: 1 IfId: 2 diff --git a/test/example/c-api-kitchen-sink.c b/test/example/c-api-kitchen-sink.c index 1d699965baa..e9f6a5bb2df 100644 --- a/test/example/c-api-kitchen-sink.c +++ b/test/example/c-api-kitchen-sink.c @@ -379,6 +379,7 @@ void test_features() { printf("BinaryenFeatureCustomPageSizes: %d\n", BinaryenFeatureCustomPageSizes()); printf("BinaryenFeatureMultibyte: %d\n", BinaryenFeatureMultibyte()); + printf("BinaryenFeatureCompactImports: %d\n", BinaryenFeatureCompactImports()); printf("BinaryenFeatureAll: %d\n", BinaryenFeatureAll()); } diff --git a/test/passes/strip-target-features_roundtrip_print-features_all-features.txt b/test/passes/strip-target-features_roundtrip_print-features_all-features.txt index 8172cc77f9c..04e2a1ad6b0 100644 --- a/test/passes/strip-target-features_roundtrip_print-features_all-features.txt +++ b/test/passes/strip-target-features_roundtrip_print-features_all-features.txt @@ -23,6 +23,7 @@ --enable-relaxed-atomics --enable-custom-page-sizes --enable-multibyte +--enable-compact-imports (module (type $0 (func (result v128 externref))) (func $foo (type $0) (result v128 externref) diff --git a/test/spec/compact-import-section/binary-compact-imports.wast b/test/spec/compact-import-section/binary-compact-imports.wast new file mode 100644 index 00000000000..bf5d9e02130 --- /dev/null +++ b/test/spec/compact-import-section/binary-compact-imports.wast @@ -0,0 +1,174 @@ +;; Auxiliary modules to import + +(module + (func (export "b") (result i32) (i32.const 0x0f)) + (func (export "c") (result i32) (i32.const 0xf0)) +) +(register "a") +(module + (func (export "") (result i32) (i32.const 0xab)) +) +(register "") + + +;; Valid compact encodings + +(module binary + "\00asm" "\01\00\00\00" + "\01\05\01\60\00\01\7f" ;; Type section: (type (func (result i32))) + "\02\0e" ;; Import section + "\01" ;; 1 group + "\01a" ;; "a" + "\00" "\7f" ;; "" + 0x7f (compact encoding) + "\02" ;; 2 items + "\01b" "\00\00" ;; "b" (func (type 0)) + "\01c" "\00\00" ;; "c" (func (type 0)) + "\03\02" "\01" ;; Function section, 1 func + "\00" ;; func 2: type 0 + "\07\08" "\01" ;; Export section, 1 export + "\04test" "\00\02" ;; "test" func 2 + "\0a\09" "\01" ;; Code section, 1 func + "\07" "\00" ;; len, 0 locals + "\10\00" ;; call 0 + "\10\01" ;; call 1 + "\6a" ;; i32.add + "\0b" ;; end +) +(assert_return (invoke "test") (i32.const 0xff)) + +(module binary + "\00asm" "\01\00\00\00" + "\01\05\01\60\00\01\7f" ;; Type section: (type (func (result i32))) + "\02\0c" ;; Import section + "\01" ;; 1 group + "\01a" ;; "a" + "\00" "\7e" ;; "" + 0x7e (compact encoding) + "\00\00" ;; (func (type 0)) + "\02" ;; 2 items + "\01b" ;; "b" + "\01c" ;; "c" + "\03\02" "\01" ;; Function section, 1 func + "\00" ;; func 2: type 0 + "\07\08" "\01" ;; Export section, 1 export + "\04test" "\00\02" ;; "test" func 2 + "\0a\09" "\01" ;; Code section, 1 func + "\07" "\00" ;; len, 0 locals + "\10\00" ;; call 0 + "\10\01" ;; call 1 + "\6a" ;; i32.add + "\0b" ;; end +) +(assert_return (invoke "test") (i32.const 0xff)) + + +;; Overly-long empty name encodings are valid + +(module binary + "\00asm" "\01\00\00\00" + "\01\05\01\60\00\01\7f" ;; Type section: (type (func (result i32))) + "\02\11" ;; Import section + "\01" ;; 1 group + "\01a" ;; "a" + "\80\80\80\00" "\7f" ;; "" (long encoding) + 0x7f + "\02" ;; 2 items + "\01b" "\00\00" ;; "b" (func (type 0)) + "\01c" "\00\00" ;; "c" (func (type 0)) +) +(module binary + "\00asm" "\01\00\00\00" + "\01\05\01\60\00\01\7f" ;; Type section: (type (func (result i32))) + "\02\0f" ;; Import section + "\01" ;; 1 group + "\01a" ;; "a" + "\80\80\80\00" "\7e" ;; "" (long encoding) + 0x7e + "\00\00" ;; (func (type 0)) + "\02" ;; 2 items + "\01b" ;; "b" + "\01c" ;; "c" +) + + +;; Discriminator is not valid except after empty names + +(assert_malformed + (module binary + "\00asm" "\01\00\00\00" + "\01\05\01\60\00\01\7f" ;; Type section: (type (func (result i32))) + "\02\12" ;; Import section + "\01" ;; 1 group + "\01a" ;; "a" + "\01b" "\7f" ;; "b" + 0x7f + "\02" ;; 2 items + "\01b" "\00\00" ;; "b" (func (type 0)) + "\01c" "\00\00" ;; "c" (func (type 0)) + ) + "malformed import kind" +) +(assert_malformed + (module binary + "\00asm" "\01\00\00\00" + "\01\05\01\60\00\01\7f" ;; Type section: (type (func (result i32))) + "\02\10" ;; Import section + "\01" ;; 1 group + "\01a" ;; "a" + "\01b" "\7e" ;; "" + 0x7e (long encoding) + "\00\00" ;; (func (type 0)) + "\02" ;; 2 items + "\01b" ;; "b" + "\01c" ;; "c" + ) + "malformed import kind" +) + + +;; Discriminator is not to be interpreted as LEB128 + +(assert_malformed + (module binary + "\00asm" "\01\00\00\00" + "\01\05\01\60\00\01\7f" ;; Type section: (type (func (result i32))) + "\02\11" ;; Import section + "\01" ;; 1 group + "\01a" ;; "a" + "\00\ff\80\80\00" ;; "" + 0x7f (long encoding) + "\02" ;; 2 items + "\01b" "\00\00" ;; "b" (func (type 0)) + "\01c" "\00\00" ;; "c" (func (type 0)) + ) + "malformed import kind" +) +(assert_malformed + (module binary + "\00asm" "\01\00\00\00" + "\01\05\01\60\00\01\7f" ;; Type section: (type (func (result i32))) + "\02\0f" ;; Import section + "\01" ;; 1 group + "\01a" ;; "a" + "\00\fe\80\80\00" ;; "" + 0x7e (long encoding) + "\00\00" ;; (func (type 0)) + "\02" ;; 2 items + "\01b" ;; "b" + "\01c" ;; "c" + ) + "malformed import kind" +) + + +;; Empty names are still valid if not followed by a discriminator + +(module binary + "\00asm" "\01\00\00\00" + "\01\05\01\60\00\01\7f" ;; Type section: (type (func (result i32))) + "\02\05" ;; Import section + "\01" ;; 1 group + "\00\00\00\00" ;; "" "" (func (type 0)) + "\03\02" "\01" ;; Function section, 1 func + "\00" ;; func 1: type 0 + "\07\08" "\01" ;; Export section, 1 export + "\04test" "\00\01" ;; "test" func 1 + "\0a\06" "\01" ;; Code section, 1 func + "\04" "\00" ;; len, 0 locals + "\10\00" ;; call 0 + "\0b" ;; end +) +(assert_return (invoke "test") (i32.const 0xab))