From 3cc750644760f186d50aab9d8e93dfd5021b34fe Mon Sep 17 00:00:00 2001 From: Ashton Meuser Date: Mon, 4 Mar 2024 20:34:52 -0800 Subject: [PATCH] Backport FFI type inference (#64) * Record exported function param types Require type converting Variant to Wasm value * Validate argument count * Record import function return types * Separate import tests * Type inference tests * Fix test dummy import leaking Godot object * Exported function argument count test --- examples/wasm-test/TestGeneral.gd | 64 ++----------- examples/wasm-test/TestImports.gd | 81 +++++++++++++++++ examples/wasm-test/TestInference.gd | 13 +++ .../wasm-test/utils/GodotWasmTestSuite.gd | 10 +-- examples/wasm-test/utils/TestSuite.gd | 5 ++ examples/wasm-test/wasm/import.wasm | Bin 104 -> 140 bytes examples/wasm-test/wasm/inference.wasm | Bin 0 -> 271 bytes examples/wasm-test/wasm/memory-import.wasm | Bin 3562 -> 5860 bytes src/godot-wasm.cpp | 84 ++++++++++++------ 9 files changed, 168 insertions(+), 89 deletions(-) create mode 100644 examples/wasm-test/TestImports.gd create mode 100644 examples/wasm-test/TestInference.gd create mode 100644 examples/wasm-test/wasm/inference.wasm diff --git a/examples/wasm-test/TestGeneral.gd b/examples/wasm-test/TestGeneral.gd index 6da04f5..47ed39f 100644 --- a/examples/wasm-test/TestGeneral.gd +++ b/examples/wasm-test/TestGeneral.gd @@ -48,37 +48,6 @@ func test_invalid_binary(): expect_eq(error, ERR_INVALID_DATA) expect_error("Invalid binary") -func test_imports(): - var imports = dummy_imports(["import.test_import"]) - load_wasm("import", imports) - expect_empty() - -func test_invalid_imports(): - var wasm = Wasm.new() - var buffer = read_file("import") - var error = wasm.compile(buffer) - expect_eq(error, OK) - # Missing import - var imports = {} - error = wasm.instantiate(imports) - expect_eq(error, ERR_CANT_CREATE) - expect_error("Missing import function import.test_import") - # Invalid import - imports = { "functions": { "import.test_import": [] } } - error = wasm.instantiate(imports) - expect_eq(error, ERR_CANT_CREATE) - expect_error("Invalid import function import.test_import") - # Invalid import target - imports = { "functions": { "import.test_import": [0, "dummy"] } } - error = wasm.instantiate(imports) - expect_eq(error, ERR_CANT_CREATE) - expect_error("Invalid import target") - # Invalid import method - imports = { "functions": { "import.test_import": [self, 0] } } - error = wasm.instantiate(imports) - expect_eq(error, ERR_CANT_CREATE) - expect_error("Invalid import method") - func test_function(): var wasm = load_wasm("simple") var result = wasm.function("add", [1, 2]) @@ -99,18 +68,18 @@ func test_uninstantiated_function(): expect_eq(result, null) expect_error("Not instantiated") -func test_invalid_function_args(): +func test_invalid_function_arg_type(): var wasm = load_wasm("simple") var result = wasm.function("add", [{}, 2]) expect_eq(result, null) expect_error("Unsupported Godot variant type") expect_error("Invalid argument type") -func test_callback_function(): - var imports = dummy_imports(["import.test_import"]) - var wasm = load_wasm("import", imports) - wasm.function("callback", []) - expect_log("Dummy import 123") +func test_invalid_function_arg_count(): + var wasm = load_wasm("simple") + var result = wasm.function("add", [1]) + expect_eq(result, null) + expect_error("Incorrect number of arguments supplied") func test_global(): var wasm = load_wasm("simple") @@ -158,24 +127,3 @@ func test_inspect(): "memory": {} } expect_eq(inspect, expected) - # Import module post-instantiation - var imports = dummy_imports(["import.test_import"]) - wasm = load_wasm("import", imports) - inspect = wasm.inspect() - expected = { - "import_functions": { - "import.test_import": [[TYPE_INT], []], - }, - "export_globals": {}, - "export_functions": { - "_initialize": [[], []], - "callback": [[], []], - }, - "memory": { - "min": 0, - "max": PAGES_MAX, - "current": 0, - } - } - expect_eq(inspect, expected) - expect_empty() diff --git a/examples/wasm-test/TestImports.gd b/examples/wasm-test/TestImports.gd new file mode 100644 index 0000000..dd730e5 --- /dev/null +++ b/examples/wasm-test/TestImports.gd @@ -0,0 +1,81 @@ +extends GodotWasmTestSuite + +func test_imports(): + var imports = { "functions": { + "import.import_int": dummy_import(), + "import.import_float": dummy_import(), + } } + load_wasm("import", imports) + expect_empty() + +func test_invalid_imports(): + var wasm = Wasm.new() + var buffer = read_file("import") + var error = wasm.compile(buffer) + expect_eq(error, OK) + # Missing import + var imports = {} + error = wasm.instantiate(imports) + expect_eq(error, ERR_CANT_CREATE) + expect_error("Missing import function import.import_(int|float)") + # Invalid import + imports = { "functions": { + "import.import_int": [], + "import.import_float": dummy_import(), + } } + error = wasm.instantiate(imports) + expect_eq(error, ERR_CANT_CREATE) + expect_error("Invalid import function import.import_int") + # Invalid import target + imports = { "functions": { + "import.import_int": [0, "dummy"], + "import.import_float": dummy_import(), + } } + error = wasm.instantiate(imports) + expect_eq(error, ERR_CANT_CREATE) + expect_error("Invalid import target import.import_int") + # Invalid import method + imports = { "functions": { + "import.import_int": [self, 0], + "import.import_float": dummy_import(), + } } + error = wasm.instantiate(imports) + expect_eq(error, ERR_CANT_CREATE) + expect_error("Invalid import method import.import_int") + +func test_callback_function(): + var imports = { "functions": { + "import.import_int": dummy_import(), + "import.import_float": dummy_import(), + } } + var wasm = load_wasm("import", imports) + wasm.function("callback", []) + expect_log("Dummy import -123") + expect_log("Dummy import -12.34") + +func test_inspect(): + # Import module post-instantiation + var imports = { "functions": { + "import.import_int": dummy_import(), + "import.import_float": dummy_import(), + } } + var wasm = load_wasm("import", imports) + var inspect = wasm.inspect() + var expected = { + "import_functions": { + "import.import_float": [[TYPE_FLOAT], []], + "import.import_int": [[TYPE_INT], []], + }, + "export_globals": {}, + "export_functions": { + "_initialize": [[], []], + "callback": [[], []], + }, + "memory": { + "min": 0, + "max": PAGES_MAX, + "current": 0, + } + } + expect_eq(inspect, expected) + expect_empty() diff --git a/examples/wasm-test/TestInference.gd b/examples/wasm-test/TestInference.gd new file mode 100644 index 0000000..2d828b2 --- /dev/null +++ b/examples/wasm-test/TestInference.gd @@ -0,0 +1,13 @@ +extends GodotWasmTestSuite + +func test_param_types(): + var wasm = load_wasm("inference", { "functions": { + "inference.echo_i32": dummy_import(), + "inference.echo_i64": dummy_import(), + "inference.echo_f32": dummy_import(), + "inference.echo_f64": dummy_import(), + } }) + expect_eq(wasm.function("add_i32", [3, -1]), 2) + expect_eq(wasm.function("add_i64", [3, -1]), 2) + expect_approx(wasm.function("add_f32", [3.5, -1.2]), 2.3) + expect_approx(wasm.function("add_f64", [3.5, -1.2]), 2.3) diff --git a/examples/wasm-test/utils/GodotWasmTestSuite.gd b/examples/wasm-test/utils/GodotWasmTestSuite.gd index daae570..8b3fd43 100644 --- a/examples/wasm-test/utils/GodotWasmTestSuite.gd +++ b/examples/wasm-test/utils/GodotWasmTestSuite.gd @@ -25,15 +25,13 @@ func read_file(f: String) -> PoolByteArray: return file.get_buffer(file.get_len()) # Dummy import to supply to Wasm modules -static func dummy(a = "", b = "", c = "", d = ""): +static func _dummy_import(a = "", b = "", c = "", d = ""): var message = "Dummy import %s %s %s %s" % [a, b, c, d] print(message.strip_edges()) + return a -func dummy_imports(functions: Array = []) -> Dictionary: - var imports = { "functions": {} } - for function in functions: - imports.functions[function] = [self, "dummy"] - return imports +func dummy_import() -> Array: + return [self, "_dummy_import"] func make_bytes(data: Array): return PoolByteArray(data) diff --git a/examples/wasm-test/utils/TestSuite.gd b/examples/wasm-test/utils/TestSuite.gd index 97f5073..040e180 100644 --- a/examples/wasm-test/utils/TestSuite.gd +++ b/examples/wasm-test/utils/TestSuite.gd @@ -6,6 +6,8 @@ signal test_error(message) signal test_pass(case) signal test_fail(case) +const EPSILON = 0.00001 # See https://github.com/godotengine/godot/blob/master/core/math/math_defs.h + var _log_file # Log file used to check for output and/or errors var _error: bool = false # If the current test case has failed @@ -62,6 +64,9 @@ func expect_type(a, t): func expect_within(a, b, c): if abs(a - b) > c: _fail("Expect within: %s != %s ± %s" % [a, b, c]) +func expect_approx(a, b): + expect_within(a, b, EPSILON) + func expect_includes(o, v: String): if o is Dictionary: if !o.keys().has(v): _fail("Expect contains: %s ∉ %s" % [v, o.keys()]) diff --git a/examples/wasm-test/wasm/import.wasm b/examples/wasm-test/wasm/import.wasm index 48a9fbb8d93ea80346462328083c31bac176c7e9..30855269c0fd423c1db141572ebcff24a9e1511c 100644 GIT binary patch literal 140 zcmZQbEY4+QU|?Y6VNPIRNMNi3;u;1fZ6>zN+=Bd~5-tc8pP5&}zz7xNfr_Q&8R7+h?zoTzuaA{Ut-R;xZIJpq_XZ$B~1SC>M{E(p{KKUTt5s1L&6t}V-`mM diff --git a/examples/wasm-test/wasm/inference.wasm b/examples/wasm-test/wasm/inference.wasm new file mode 100644 index 0000000000000000000000000000000000000000..f57c18df0f70d78bd0e70e67297c6c4e5be184e0 GIT binary patch literal 271 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 GOYt9-h15m> delta 691 zcmZ8dO=}ZT6utM|HUGp$b7k5tS-5uQaF|mo5ZrDn?8+m1?Q@K_*fV zQe3n==t}$mmu{r{;!4DV_zPSKLKiNi8zBqto3Vl}-Z|&pd$?cUj%bLZ{@ z0oN%gp%7UD>B*b}=nyD()QM4p`UeJmkp?moj@gUVA$7Gi*IH=J-flf?f}cm3`!HNu zzlJvv1G6diQ2@$(l7~!zsvMc17o!{r@vwk0X`HXing+e%=}puHKQP-g&$b)#Y+{ok zYS|7&``nIgE46LN8Mci%ON_6bql{mihe zyP;s(a1R95{bo%}!rLkTsqfhA4|j}9n8q7%`4WXKz(V)L0C>^SdCTGETuM zf)GPHI%HfP8ht+e6PdP` ze~a>A|MNs-J{QNGlH@tfuj1(fg_z9bmu%PdNxz8&vfGeSeIY?VGslK@n~!^b{sy2I BltKUi diff --git a/src/godot-wasm.cpp b/src/godot-wasm.cpp index 4465ceb..afa610b 100644 --- a/src/godot-wasm.cpp +++ b/src/godot-wasm.cpp @@ -15,12 +15,22 @@ namespace godot { struct ContextFuncImport: public ContextExtern { Object* target; // The object from which to invoke callback method String method; // External name; doesn't necessarily match import name - ContextFuncImport(uint16_t i): ContextExtern(i) { } + std::vector results; // Return types + ContextFuncImport(uint16_t i, const wasm_functype_t* func_type): ContextExtern(i) { + const wasm_valtype_vec_t* func_results = wasm_functype_results(func_type); + for (uint16_t i = 0; i < func_results->size; i++) results.push_back(wasm_valtype_kind(func_results->data[i])); + } }; struct ContextFuncExport: public ContextExtern { size_t return_count; // Number of return values - ContextFuncExport(uint16_t i, size_t return_count): ContextExtern(i), return_count(return_count) { } + std::vector params; // Param types + ContextFuncExport(uint16_t i, const wasm_functype_t* func_type): ContextExtern(i) { + const wasm_valtype_vec_t* func_params = wasm_functype_params(func_type); + const wasm_valtype_vec_t* func_results = wasm_functype_results(func_type); + for (uint16_t i = 0; i < func_params->size; i++) params.push_back(wasm_valtype_kind(func_params->data[i])); + return_count = func_results->size; + } }; struct ContextMemory: public ContextExtern { @@ -42,6 +52,14 @@ namespace godot { p = NULL; } + inline wasm_val_t error_value(const char* message) { + PRINT_ERROR(message); + wasm_val_t value; + value.kind = WASM_ANYREF; + value.of.ref = NULL; + return value; + } + Variant decode_variant(wasm_val_t value) { switch (value.kind) { case WASM_I32: return Variant(value.of.i32); @@ -52,23 +70,35 @@ namespace godot { } } - wasm_val_t encode_variant(Variant variant) { + wasm_val_t encode_variant(Variant variant, wasm_valkind_t kind) { wasm_val_t value; + value.kind = kind; switch (variant.get_type()) { case Variant::INT: - value.kind = WASM_I64; - value.of.i64 = (int64_t)variant; - break; + switch (kind) { + case WASM_I32: + value.of.i32 = (int32_t)variant; + return value; + case WASM_I64: + value.of.i64 = (int64_t)variant; + return value; + default: + return error_value("Invalid target type for integer variant"); + } case Variant::FLOAT: - value.kind = WASM_F64; - value.of.f64 = (float64_t)variant; - break; + switch (kind) { + case WASM_F32: + value.of.f32 = (float32_t)variant; + return value; + case WASM_F64: + value.of.f64 = (float64_t)variant; + return value; + default: + return error_value("Invalid target type for float variant"); + } default: - PRINT_ERROR("Unsupported Godot variant type"); - value.kind = WASM_ANYREF; - value.of.ref = NULL; + return error_value("Unsupported Godot variant type"); } - return value; } String decode_name(const wasm_name_t* name) { @@ -83,18 +113,19 @@ namespace godot { return d.has(k) && d[k].get_type() == Variant::OBJECT ? Object::cast_to(d[k]) : NULL; } - godot_error extract_results(Variant variant, wasm_val_vec_t* results) { + godot_error extract_results(Variant variant, const godot_wasm::ContextFuncImport* context, wasm_val_vec_t* results) { + FAIL_IF(results->size != context->results.size(), "Incompatible return value(s)", ERR_INVALID_DATA); if (results->size <= 0) return OK; if (variant.get_type() == Variant::ARRAY) { Array array = variant.operator Array(); if ((size_t)array.size() != results->size) return ERR_PARAMETER_RANGE_ERROR; for (uint16_t i = 0; i < results->size; i++) { - results->data[i] = encode_variant(array[i]); + results->data[i] = encode_variant(array[i], context->results[i]); if (results->data[i].kind == WASM_ANYREF) return ERR_INVALID_DATA; } return OK; } else if (results->size == 1) { - results->data[0] = encode_variant(variant); + results->data[0] = encode_variant(variant, context->results[0]); return results->data[0].kind == WASM_ANYREF ? ERR_INVALID_DATA : OK; } else return ERR_INVALID_DATA; } @@ -175,7 +206,7 @@ namespace godot { for (uint16_t i = 0; i < args->size; i++) params.push_back(decode_variant(args->data[i])); // TODO: Ensure target is valid and has method Variant variant = context->target->callv(context->method, params); - godot_error error = extract_results(variant, results); + godot_error error = extract_results(variant, context, results); if (error) FAIL("Extracting import function results failed", trap("Extracting import function results failed\0")); return NULL; } @@ -302,8 +333,8 @@ namespace godot { } const Array& import = dict_safe_get(functions, it.first, Array()); FAIL_IF(import.size() != 2, "Invalid import function " + it.first, ERR_CANT_CREATE); - FAIL_IF(import[0].get_type() != Variant::OBJECT, "Invalid import target", ERR_CANT_CREATE); - FAIL_IF(import[1].get_type() != Variant::STRING, "Invalid import method", ERR_CANT_CREATE); + FAIL_IF(import[0].get_type() != Variant::OBJECT, "Invalid import target " + it.first, ERR_CANT_CREATE); + FAIL_IF(import[1].get_type() != Variant::STRING, "Invalid import method " + it.first, ERR_CANT_CREATE); godot_wasm::ContextFuncImport* context = (godot_wasm::ContextFuncImport*)&it.second; context->target = import[0]; context->method = import[1]; @@ -406,11 +437,14 @@ namespace godot { const wasm_func_t* func = wasm_extern_as_func(data); FAIL_IF(func == NULL, "Failed to retrieve function export " + name, NULL_VARIANT); + // Validate argument count + FAIL_IF(context.params.size() != args.size(), "Incorrect number of arguments supplied", NULL_VARIANT); + // Construct args std::vector args_vec; for (uint16_t i = 0; i < args.size(); i++) { Variant variant = args[i]; - wasm_val_t value = encode_variant(variant); + wasm_val_t value = encode_variant(variant, context.params[i]); FAIL_IF(value.kind == WASM_ANYREF, "Invalid argument type", NULL_VARIANT); args_vec.push_back(value); } @@ -441,10 +475,11 @@ namespace godot { const wasm_externkind_t kind = wasm_externtype_kind(type); const String key = decode_name(wasm_importtype_module(imports.data[i])) + "." + decode_name(wasm_importtype_name(imports.data[i])); switch (kind) { - case WASM_EXTERN_FUNC: - import_funcs.emplace(key, godot_wasm::ContextFuncImport(i)); + case WASM_EXTERN_FUNC: { + const wasm_functype_t* func_type = wasm_externtype_as_functype((wasm_externtype_t*)type); + import_funcs.emplace(key, godot_wasm::ContextFuncImport(i, func_type)); break; - case WASM_EXTERN_MEMORY: + } case WASM_EXTERN_MEMORY: memory_context = new godot_wasm::ContextMemory(i, true); break; default: FAIL("Import type not implemented", ERR_INVALID_DATA); @@ -462,8 +497,7 @@ namespace godot { switch (kind) { case WASM_EXTERN_FUNC: { const wasm_functype_t* func_type = wasm_externtype_as_functype((wasm_externtype_t*)type); - const wasm_valtype_vec_t* func_results = wasm_functype_results(func_type); - export_funcs.emplace(key, godot_wasm::ContextFuncExport(i, func_results->size)); + export_funcs.emplace(key, godot_wasm::ContextFuncExport(i, func_type)); break; } case WASM_EXTERN_GLOBAL: export_globals.emplace(key, godot_wasm::ContextExtern(i));