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 48a9fbb..3085526 100644 Binary files a/examples/wasm-test/wasm/import.wasm and b/examples/wasm-test/wasm/import.wasm differ diff --git a/examples/wasm-test/wasm/inference.wasm b/examples/wasm-test/wasm/inference.wasm new file mode 100644 index 0000000..f57c18d Binary files /dev/null and b/examples/wasm-test/wasm/inference.wasm differ diff --git a/examples/wasm-test/wasm/memory-import.wasm b/examples/wasm-test/wasm/memory-import.wasm index 4e0512b..aaef2f3 100644 Binary files a/examples/wasm-test/wasm/memory-import.wasm and b/examples/wasm-test/wasm/memory-import.wasm differ 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<wasm_valkind_t> 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<wasm_valkind_t> 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<T>(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<wasm_val_t> 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));