Skip to content

Commit

Permalink
Backport FFI type inference (#64)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
ashtonmeuser committed Mar 5, 2024
1 parent 5900422 commit 3cc7506
Show file tree
Hide file tree
Showing 9 changed files with 168 additions and 89 deletions.
64 changes: 6 additions & 58 deletions examples/wasm-test/TestGeneral.gd
Original file line number Diff line number Diff line change
Expand Up @@ -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])
Expand All @@ -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")
Expand Down Expand Up @@ -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()
81 changes: 81 additions & 0 deletions examples/wasm-test/TestImports.gd
Original file line number Diff line number Diff line change
@@ -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()
13 changes: 13 additions & 0 deletions examples/wasm-test/TestInference.gd
Original file line number Diff line number Diff line change
@@ -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)
10 changes: 4 additions & 6 deletions examples/wasm-test/utils/GodotWasmTestSuite.gd
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
5 changes: 5 additions & 0 deletions examples/wasm-test/utils/TestSuite.gd
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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()])
Expand Down
Binary file modified examples/wasm-test/wasm/import.wasm
Binary file not shown.
Binary file added examples/wasm-test/wasm/inference.wasm
Binary file not shown.
Binary file modified examples/wasm-test/wasm/memory-import.wasm
Binary file not shown.
84 changes: 59 additions & 25 deletions src/godot-wasm.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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);
Expand All @@ -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) {
Expand All @@ -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;
}
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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];
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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);
Expand All @@ -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));
Expand Down

0 comments on commit 3cc7506

Please sign in to comment.