diff --git a/README.md b/README.md index 9d7096a2..089fe797 100644 --- a/README.md +++ b/README.md @@ -63,13 +63,12 @@ static void add_coin(const Node& player) { + std::to_string(coins) + ((coins == 1) ? " coin" : " coins")); } -extern "C" Variant _on_body_entered(Variant arg) { - Node player_node = arg.as_node(); - if (player_node.get_name() != "Player") { +extern "C" Variant _on_body_entered(Node player) { + if (player.get_name() != "Player") { return {}; } - Node(".").queue_free(); // Remove the current coin! - add_coin(player_node); + get_node().queue_free(); // Remove the current coin! + add_coin(player); return {}; } ``` diff --git a/program/cpp/api/string.hpp b/program/cpp/api/string.hpp index 7759ba14..74096285 100644 --- a/program/cpp/api/string.hpp +++ b/program/cpp/api/string.hpp @@ -9,8 +9,10 @@ */ union String { constexpr String() {} // DON'T TOUCH - String(std::string_view value) - : m_idx(Create(value.data(), value.size())) {} + String(std::string_view value); + String &operator =(std::string_view value); + template + String(const char (&value)[N]); // String operations void append(const String &value); @@ -43,6 +45,7 @@ union String { private: unsigned m_idx = -1; }; +using NodePath = String; // NodePath is compatible with String inline Variant::Variant(const String &s) { m_type = Variant::STRING; @@ -59,3 +62,13 @@ inline String Variant::as_string() const { } return String::from_variant_index(v.i); } + +inline String::String(std::string_view value) + : m_idx(Create(value.data(), value.size())) {} +inline String &String::operator =(std::string_view value) { + m_idx = Create(value.data(), value.size()); + return *this; +} +template +inline String::String(const char (&value)[N]) + : m_idx(Create(value, N - 1)) {} diff --git a/src/guest_datatypes.h b/src/guest_datatypes.h index fd3ef9af..395c0bde 100644 --- a/src/guest_datatypes.h +++ b/src/guest_datatypes.h @@ -22,7 +22,18 @@ struct GDNativeVariant { struct { int32_t ivec2_int[2]; }; + struct { + uint64_t object_id; + GodotObject *object_ptr; + }; }; + + godot::Object *to_object() const { + if (object_ptr == nullptr) + return nullptr; + return internal::get_object_instance_binding(object_ptr); + } + } __attribute__((packed)); // -= Guest Data Types =- diff --git a/src/sandbox.cpp b/src/sandbox.cpp index 5a354cf3..1ac75f3a 100644 --- a/src/sandbox.cpp +++ b/src/sandbox.cpp @@ -27,11 +27,13 @@ void Sandbox::_bind_methods() { mi.name = "vmcall"; mi.return_val = PropertyInfo(Variant::OBJECT, "result"); ClassDB::bind_vararg_method(METHOD_FLAGS_DEFAULT, "vmcall", &Sandbox::vmcall, mi, DEFVAL(std::vector{})); + ClassDB::bind_vararg_method(METHOD_FLAGS_DEFAULT, "vmcallv", &Sandbox::vmcallv, mi, DEFVAL(std::vector{})); } ClassDB::bind_method(D_METHOD("vmcallable", "function", "args"), &Sandbox::vmcallable, DEFVAL(Array{})); // Internal testing. ClassDB::bind_method(D_METHOD("assault", "test", "iterations"), &Sandbox::assault); + ClassDB::bind_method(D_METHOD("has_function", "function"), &Sandbox::has_function); // Properties. ClassDB::bind_method(D_METHOD("set_max_refs", "max"), &Sandbox::set_max_refs); @@ -124,7 +126,7 @@ void Sandbox::load(PackedByteArray &&buffer, const std::vector *arg machine_t &m = machine(); m.set_userdata(this); - this->m_current_state = &this->m_states.at(MAX_LEVEL); + this->m_current_state = &this->m_states[0]; // Set the current state to the first state this->initialize_syscalls(); @@ -156,7 +158,8 @@ void Sandbox::load(PackedByteArray &&buffer, const std::vector *arg } Variant Sandbox::vmcall_address(gaddr_t address, const Variant **args, GDExtensionInt arg_count, GDExtensionCallError &error) { - return this->vmcall_internal(address, args, arg_count, error); + error.error = GDEXTENSION_CALL_OK; + return this->vmcall_internal(address, args, arg_count); } Variant Sandbox::vmcall(const Variant **args, GDExtensionInt arg_count, GDExtensionCallError &error) { if (arg_count < 1) { @@ -164,19 +167,40 @@ Variant Sandbox::vmcall(const Variant **args, GDExtensionInt arg_count, GDExtens error.argument = -1; return Variant(); } + error.error = GDEXTENSION_CALL_OK; + + const Variant &function = *args[0]; + args += 1; + arg_count -= 1; + const String function_name = function.operator String(); + return this->vmcall_internal(cached_address_of(function_name.hash(), function_name), args, arg_count); +} +Variant Sandbox::vmcallv(const Variant **args, GDExtensionInt arg_count, GDExtensionCallError &error) { + if (arg_count < 1) { + error.error = GDEXTENSION_CALL_ERROR_TOO_FEW_ARGUMENTS; + error.argument = -1; + return Variant(); + } + error.error = GDEXTENSION_CALL_OK; + const Variant &function = *args[0]; args += 1; arg_count -= 1; const String function_name = function.operator String(); - return this->vmcall_internal(cached_address_of(function_name.hash(), function_name), args, arg_count, error); + // Store use_native_args state and restore it after the call + Variant result; + auto old_use_native_args = this->m_use_native_args; + this->m_use_native_args = false; + result = this->vmcall_internal(cached_address_of(function_name.hash(), function_name), args, arg_count); + this->m_use_native_args = old_use_native_args; + return result; } Variant Sandbox::vmcall_fn(const StringName &function, const Variant **args, GDExtensionInt arg_count) { if (this->m_throttled > 0) { this->m_throttled--; return Variant(); } - GDExtensionCallError error; - Variant result = this->vmcall_internal(cached_address_of(function.hash()), args, arg_count, error); + Variant result = this->vmcall_internal(cached_address_of(function.hash()), args, arg_count); return result; } void Sandbox::setup_arguments_native(gaddr_t arrayDataPtr, GuestVariant *v, const Variant **args, int argc) { @@ -211,12 +235,11 @@ void Sandbox::setup_arguments_native(gaddr_t arrayDataPtr, GuestVariant *v, cons break; } case Variant::VECTOR2I: { // 8- or 16-byte structs can be passed in registers - machine.cpu.reg(index++) = inner->ivec2_int[0]; - machine.cpu.reg(index++) = inner->ivec2_int[1]; + machine.cpu.reg(index++) = inner->value; // 64-bit packed integers break; } case Variant::OBJECT: { // Objects are represented as uintptr_t - godot::Object *obj = arg.operator godot::Object *(); + godot::Object *obj = inner->to_object(); this->add_scoped_object(obj); machine.cpu.reg(index++) = uintptr_t(obj); // Fits in a single register break; @@ -289,11 +312,14 @@ GuestVariant *Sandbox::setup_arguments(gaddr_t &sp, const Variant **args, int ar // A0 is the return value (Variant) of the function return &v[0]; } -Variant Sandbox::vmcall_internal(gaddr_t address, const Variant **args, int argc, GDExtensionCallError &error) { +Variant Sandbox::vmcall_internal(gaddr_t address, const Variant **args, int argc) { //error.error = GDEXTENSION_CALL_OK; //return Variant(); CurrentState &state = this->m_states[m_level]; + const bool is_reentrant_call = m_level > 1; + m_level++; + // Scoped objects and owning tree node state.reset(this->m_max_refs); CurrentState *old_state = this->m_current_state; @@ -306,9 +332,8 @@ Variant Sandbox::vmcall_internal(gaddr_t address, const Variant **args, int argc GuestVariant *retvar = nullptr; riscv::CPU &cpu = m_machine->cpu; auto &sp = cpu.reg(riscv::REG_SP); - m_level++; // execute guest function - if (m_level == 1) { + if (!is_reentrant_call) { cpu.reg(riscv::REG_RA) = m_machine->memory.exit_address(); // reset the stack pointer to its initial location sp = m_machine->memory.stack_initial(); @@ -316,7 +341,7 @@ Variant Sandbox::vmcall_internal(gaddr_t address, const Variant **args, int argc retvar = this->setup_arguments(sp, args, argc); // execute! m_machine->simulate_with(get_instructions_max() << 30, 0u, address); - } else if (m_level > 1 && m_level < MAX_LEVEL) { + } else if (m_level < MAX_LEVEL) { riscv::Registers regs; regs = cpu.registers(); // we are in a recursive call, so wait before setting exit address @@ -336,7 +361,6 @@ Variant Sandbox::vmcall_internal(gaddr_t address, const Variant **args, int argc // Restore the previous state this->m_level--; this->m_current_state = old_state; - error.error = GDEXTENSION_CALL_OK; return result; } catch (const std::exception &e) { @@ -349,8 +373,6 @@ Variant Sandbox::vmcall_internal(gaddr_t address, const Variant **args, int argc // TODO: Free the function arguments and return value? Will help keep guest memory clean this->m_current_state = old_state; - error.error = GDEXTENSION_CALL_ERROR_INVALID_ARGUMENT; - error.argument = -1; return Variant(); } } @@ -384,10 +406,11 @@ void RiscvCallable::call(const Variant **p_arguments, int p_argcount, Variant &r for (int i = 0; i < p_argcount; i++) { m_varargs_ptrs[m_varargs_base_count + i] = p_arguments[i]; } - r_return_value = self->vmcall_internal(address, m_varargs_ptrs.data(), total_args, r_call_error); + r_return_value = self->vmcall_internal(address, m_varargs_ptrs.data(), total_args); } else { - r_return_value = self->vmcall_internal(address, p_arguments, p_argcount, r_call_error); + r_return_value = self->vmcall_internal(address, p_arguments, p_argcount); } + r_call_error.error = GDEXTENSION_CALL_OK; } void Sandbox::print(std::string_view text) { @@ -432,6 +455,11 @@ gaddr_t Sandbox::address_of(std::string_view name) const { return machine().address_of(name); } +bool Sandbox::has_function(const StringName &p_function) const { + const gaddr_t address = cached_address_of(p_function.hash(), p_function); + return address != 0x0; +} + int64_t Sandbox::get_heap_usage() const { if (machine().has_arena()) { return machine().arena().bytes_used(); @@ -466,11 +494,12 @@ std::optional Sandbox::get_scoped_variant(unsigned index) const return state().scoped_variants[index - 1]; } Variant &Sandbox::get_mutable_scoped_variant(unsigned index) { - if (index == 0 || index > state().scoped_variants.size()) { + std::optional var_opt = get_scoped_variant(index); + if (!var_opt.has_value()) { ERR_PRINT("Invalid scoped variant index."); throw std::runtime_error("Invalid scoped variant index."); } - const godot::Variant *var = state().scoped_variants[index - 1]; + const Variant *var = var_opt.value(); // Find the variant in the variants list auto it = std::find_if(state().variants.begin(), state().variants.end(), [var](const Variant &v) { return &v == var; @@ -631,22 +660,20 @@ const SandboxProperty *Sandbox::find_property_or_null(const StringName &name) co void SandboxProperty::set(Sandbox &sandbox, const Variant &value) { if (m_setter_address == 0) { - ERR_PRINT("Sandbox: Setter not found for property: " + m_name); + ERR_PRINT("Sandbox: Setter was invalid for property: " + m_name); return; } const Variant *args[] = { &value }; - GDExtensionCallError error; auto old_use_native_args = sandbox.get_use_native_args(); sandbox.set_use_native_args(false); // Always use Variant for properties - sandbox.vmcall_internal(m_setter_address, args, 1, error); + sandbox.vmcall_internal(m_setter_address, args, 1); sandbox.set_use_native_args(old_use_native_args); } Variant SandboxProperty::get(const Sandbox &sandbox) const { if (m_getter_address == 0) { - ERR_PRINT("Sandbox: Getter not found for property: " + m_name); + ERR_PRINT("Sandbox: Getter was invalid for property: " + m_name); return Variant(); } - GDExtensionCallError error; - return const_cast(sandbox).vmcall_internal(m_getter_address, nullptr, 0, error); + return const_cast(sandbox).vmcall_internal(m_getter_address, nullptr, 0); } diff --git a/src/sandbox.h b/src/sandbox.h index 17c68f5a..098fc484 100644 --- a/src/sandbox.h +++ b/src/sandbox.h @@ -56,6 +56,12 @@ class Sandbox : public Node { /// @param error The error code, if any. /// @return The return value of the function call. Variant vmcall(const Variant **args, GDExtensionInt arg_count, GDExtensionCallError &error); + /// @brief Make a function call to a function in the guest by its name. Always use Variant values for arguments. + /// @param args The arguments to pass to the function, where the first argument is the name of the function. + /// @param arg_count The number of arguments. + /// @param error The error code, if any. + /// @return The return value of the function call. + Variant vmcallv(const Variant **args, GDExtensionInt arg_count, GDExtensionCallError &error); /// @brief Make a function call to a function in the guest by its name. /// @param function The name of the function to call. /// @param args The arguments to pass to the function. @@ -112,6 +118,11 @@ class Sandbox : public Node { gaddr_t cached_address_of(int64_t hash) const; gaddr_t cached_address_of(int64_t hash, const String &name) const; + /// @brief Check if a function exists in the guest program. + /// @param p_function The name of the function to check. + /// @return True if the function exists, false otherwise. + bool has_function(const StringName &p_function) const; + // -= Call State Management =- /// @brief Get the current call state. @@ -222,7 +233,7 @@ class Sandbox : public Node { void assault(const String &test, int64_t iterations); void print(std::string_view text); - Variant vmcall_internal(gaddr_t address, const Variant **args, int argc, GDExtensionCallError &error); + Variant vmcall_internal(gaddr_t address, const Variant **args, int argc); machine_t &machine() { return *m_machine; } const machine_t &machine() const { return *m_machine; } @@ -248,7 +259,7 @@ class Sandbox : public Node { bool m_last_newline = false; uint8_t m_throttled = 0; - uint8_t m_level = 0; + uint8_t m_level = 1; // Current call level (0 is for initialization) bool m_use_native_args = false; // Stats @@ -269,8 +280,10 @@ class Sandbox : public Node { } }; CurrentState *m_current_state = nullptr; - // State stack, with the permanent (initial) state at the end. - std::array m_states; + // State stack, with the permanent (initial) state at index 0. + // That means eg. static Variant values are held stored in the state at index 0, + // so that they can be accessed by future VM calls, and not lost when a call ends. + std::array m_states; // Properties mutable std::vector m_properties; diff --git a/src/sandbox_project_settings.cpp b/src/sandbox_project_settings.cpp index a347daef..6f07eb61 100644 --- a/src/sandbox_project_settings.cpp +++ b/src/sandbox_project_settings.cpp @@ -55,7 +55,7 @@ void SandboxProjectSettings::register_settings() { #else register_setting_plain(DOCKER_PATH, "docker", DOCKER_PATH_HINT, true); #endif - register_setting_plain(NATIVE_TYPES, false, NATIVE_TYPES_HINT, false); + register_setting_plain(NATIVE_TYPES, true, NATIVE_TYPES_HINT, false); } template diff --git a/src/syscalls.cpp b/src/syscalls.cpp index 896b8e69..1277081f 100644 --- a/src/syscalls.cpp +++ b/src/syscalls.cpp @@ -1065,9 +1065,14 @@ APICALL(api_timer_periodic) { timer->set_wait_time(interval); timer->set_one_shot(oneshot); Node *topnode = emu.get_tree_base(); - topnode->add_child(timer); - timer->set_owner(topnode); - timer->start(); + // Add the timer to the top node, as long as the Sandbox is in a tree. + if (topnode != nullptr) { + topnode->add_child(timer); + timer->set_owner(topnode); + timer->start(); + } else { + timer->set_autostart(true); + } // Copy the callback capture storage to the timer timeout callback. PackedByteArray capture_data; capture_data.resize(capture->size()); diff --git a/tests/project.godot b/tests/project.godot index 881db353..25f1ea93 100644 --- a/tests/project.godot +++ b/tests/project.godot @@ -14,6 +14,10 @@ config/name="tests" config/features=PackedStringArray("4.3", "Forward Plus") config/icon="res://icon.svg" +[editor] + +script/native_types=true + [editor_plugins] enabled=PackedStringArray("res://addons/gut/plugin.cfg") diff --git a/tests/tests/test_basic.cpp b/tests/tests/test_basic.cpp index 4863e6a9..65efa2d5 100644 --- a/tests/tests/test_basic.cpp +++ b/tests/tests/test_basic.cpp @@ -8,6 +8,67 @@ extern "C" Variant test_ping_pong(Variant arg) { return arg; } -extern "C" Variant test_dictionary(Variant dict) { +extern "C" Variant test_bool(bool arg) { + return arg; +} + +extern "C" Variant test_int(long arg) { + return arg; +} + +extern "C" Variant test_float(double arg) { + return arg; +} + +extern "C" Variant test_string(String arg) { + return arg; +} + +extern "C" Variant test_nodepath(NodePath arg) { + return arg; +} + +extern "C" Variant test_vec2(Vector2 arg) { + return arg; +} + +extern "C" Variant test_vec2i(Vector2i arg) { + return arg; +} + +extern "C" Variant test_array(Array arg) { + return arg; +} + +extern "C" Variant test_dict(Dictionary arg) { + return arg; +} + +extern "C" Variant test_sub_dictionary(Dictionary dict) { return Dictionary(dict)["1"]; } + + +extern "C" Variant test_object(Object arg) { + return arg; +} + +extern "C" Variant test_exception() { + asm("unimp"); + __builtin_unreachable(); +} + +static bool timer_got_called = false; +extern "C" Variant test_timers() { + long val1 = 11; + float val2 = 22.0f; + return Timer::native_periodic(0.01, [=] (Node timer) -> Variant { + print("Timer with values: ", val1, val2); + timer.queue_free(); + timer_got_called = true; + return {}; + }); +} +extern "C" Variant verify_timers() { + return timer_got_called; +} diff --git a/tests/tests/test_basic.gd b/tests/tests/test_basic.gd index 30dc4300..db61bba9 100644 --- a/tests/tests/test_basic.gd +++ b/tests/tests/test_basic.gd @@ -6,17 +6,40 @@ func test_instantiation(): # Set the test program s.set_program(Sandbox_TestsTests) + # Verify some basic stats + assert_eq(s.get_calls_made(), 0) + assert_eq(s.get_budget_overruns(), 0) + +func test_types(): + # Create a new sandbox + var s = Sandbox.new() + # Set the test program + s.set_program(Sandbox_TestsTests) + + # Verify public functions + assert_eq(s.has_function("test_ping_pong"), true) + assert_eq(s.has_function("test_bool"), true) + assert_eq(s.has_function("test_int"), true) + assert_eq(s.has_function("test_float"), true) + assert_eq(s.has_function("test_string"), true) + assert_eq(s.has_function("test_nodepath"), true) + assert_eq(s.has_function("test_vec2"), true) + assert_eq(s.has_function("test_vec2i"), true) + assert_eq(s.has_function("test_array"), true) + assert_eq(s.has_function("test_dict"), true) + # Verify all basic types can be ping-ponged var nil : Variant assert_eq(s.vmcall("test_ping_pong", nil), nil) # Nil - assert_eq(s.vmcall("test_ping_pong", true), true) # Bool - assert_eq(s.vmcall("test_ping_pong", false), false) # Bool - assert_eq(s.vmcall("test_ping_pong", 1234), 1234) # Int - assert_eq(s.vmcall("test_ping_pong", -1234), -1234) # Int - assert_eq(s.vmcall("test_ping_pong", 9876.0), 9876.0) # Float - assert_eq(s.vmcall("test_ping_pong", "9876.0"), "9876.0") # String - assert_eq(s.vmcall("test_ping_pong", Vector2(1, 2)), Vector2(1, 2)) # Vector2 - assert_eq(s.vmcall("test_ping_pong", Vector2i(1, 2)), Vector2i(1, 2)) # Vector2i + assert_eq(s.vmcall("test_bool", true), true) # Bool + assert_eq(s.vmcall("test_bool", false), false) # Bool + assert_eq(s.vmcall("test_int", 1234), 1234) # Int + assert_eq(s.vmcall("test_int", -1234), -1234) # Int + assert_eq(s.vmcall("test_float", 9876.0), 9876.0) # Float + assert_same(s.vmcall("test_string", "9876.0"), "9876.0") # String + assert_eq(s.vmcall("test_nodepath", NodePath("Node")), NodePath("Node")) # NodePath + assert_eq(s.vmcall("test_vec2", Vector2(1, 2)), Vector2(1, 2)) # Vector2 + assert_eq(s.vmcall("test_vec2i", Vector2i(1, 2)), Vector2i(1, 2)) # Vector2i assert_eq(s.vmcall("test_ping_pong", Vector3(1, 2, 3)), Vector3(1, 2, 3)) # Vector3 assert_eq(s.vmcall("test_ping_pong", Vector3i(1, 2, 3)), Vector3i(1, 2, 3)) # Vector3i assert_eq(s.vmcall("test_ping_pong", Vector4(1, 2, 3, 4)), Vector4(1, 2, 3, 4)) # Vector4 @@ -29,15 +52,14 @@ func test_instantiation(): #assert_eq(s.vmcall("test_ping_pong", Quaternion(1, 2, 3, 4)), Quaternion(1, 2, 3, 4)) # Quat #assert_eq(s.vmcall("test_ping_pong", Basis(Vector3(1, 2, 3), Vector3(4, 5, 6), Vector3(7, 8, 9))), Basis(Vector3(1, 2, 3), Vector3(4, 5, 6), Vector3(7, 8, 9))) # Basis #assert_eq(s.vmcall("test_ping_pong", RID()), RID()) # RID - assert_eq(s.vmcall("test_ping_pong", NodePath("Node")), NodePath("Node")) # NodePath # Array, Dictionary and String as references var a_pp : Array - assert_same(s.vmcall("test_ping_pong", a_pp), a_pp) + assert_same(s.vmcall("test_array", a_pp), a_pp) var d_pp : Dictionary - assert_same(s.vmcall("test_ping_pong", d_pp), d_pp) + assert_same(s.vmcall("test_dict", d_pp), d_pp) var s_pp : String = "12345" - assert_same(s.vmcall("test_ping_pong", s_pp), s_pp) + assert_same(s.vmcall("test_string", s_pp), s_pp) assert_eq(s_pp, "12345") # Packed arrays var pba_pp : PackedByteArray = [1, 2, 3, 4] @@ -52,6 +74,7 @@ func test_instantiation(): assert_same(s.vmcall("test_ping_pong", cb), cb) # Verify that a basic function that returns a String works + assert_eq(s.has_function("public_function"), true) assert_eq(s.vmcall("public_function"), "Hello from the other side") # Verify that the sandbox can receive a Dictionary and return an element @@ -59,7 +82,85 @@ func test_instantiation(): d["1"] = Dictionary() d["1"]["2"] = "3" - assert_eq(s.vmcall("test_dictionary", d), d["1"]) + assert_eq(s.has_function("test_sub_dictionary"), true) + assert_eq(s.vmcall("test_sub_dictionary", d), d["1"]) + +func test_vmcallv(): + # Create a new sandbox + var s = Sandbox.new() + # Set the test program + s.set_program(Sandbox_TestsTests) + assert_eq(s.has_function("test_ping_pong"), true) + + # Verify that the vmcallv function works + # vmcallv always uses Variants for both arguments and the return value + assert_same(s.vmcallv("test_ping_pong", null), null) + assert_same(s.vmcallv("test_ping_pong", true), true) + assert_same(s.vmcallv("test_ping_pong", false), false) + assert_same(s.vmcallv("test_ping_pong", 1234), 1234) + assert_same(s.vmcallv("test_ping_pong", -1234), -1234) + assert_same(s.vmcallv("test_ping_pong", 9876.0), 9876.0) + assert_same(s.vmcallv("test_ping_pong", "9876.0"), "9876.0") + assert_eq(s.vmcallv("test_ping_pong", NodePath("Node")), NodePath("Node")) + assert_same(s.vmcallv("test_ping_pong", Vector2(1, 2)), Vector2(1, 2)) + assert_same(s.vmcallv("test_ping_pong", Vector2i(1, 2)), Vector2i(1, 2)) + assert_same(s.vmcallv("test_ping_pong", Vector3(1, 2, 3)), Vector3(1, 2, 3)) + assert_same(s.vmcallv("test_ping_pong", Vector3i(1, 2, 3)), Vector3i(1, 2, 3)) + assert_same(s.vmcallv("test_ping_pong", Vector4(1, 2, 3, 4)), Vector4(1, 2, 3, 4)) + assert_same(s.vmcallv("test_ping_pong", Vector4i(1, 2, 3, 4)), Vector4i(1, 2, 3, 4)) + assert_same(s.vmcallv("test_ping_pong", Color(1, 2, 3, 4)), Color(1, 2, 3, 4)) + #assert_same(s.vmcallv("test_ping_pong", Rect2(Vector2(1, 2), Vector2(3, 4))), Rect2(Vector2(1, 2), Vector2(3, 4))) + + +func test_objects(): + # Create a new sandbox + var s = Sandbox.new() + # Set the test program + s.set_program(Sandbox_TestsTests) + assert_eq(s.has_function("test_object"), true) + + # Pass a node to the sandbox + var n = Node.new() + n.name = "Node" + assert_same(s.vmcall("test_object", n), n) + + var n2d = Node2D.new() + n2d.name = "Node2D" + assert_same(s.vmcall("test_object", n2d), n2d) + + var n3d = Node3D.new() + n3d.name = "Node3D" + assert_same(s.vmcall("test_object", n3d), n3d) + + +func test_timers(): + # Create a new sandbox + var s = Sandbox.new() + # Set the test program + s.set_program(Sandbox_TestsTests) + assert_eq(s.has_function("test_timers"), true) + assert_eq(s.has_function("verify_timers"), true) + + # Create a timer and verify that it works + var timer = s.vmcall("test_timers") + assert_typeof(timer, TYPE_OBJECT) + await get_tree().create_timer(0.25).timeout + assert_eq(s.get_global_exceptions(), 0) + assert_true(s.vmcall("verify_timers"), "Timers did not work") + + +func test_exceptions(): + # Create a new sandbox + var s = Sandbox.new() + # Set the test program + s.set_program(Sandbox_TestsTests) + + # Verify that an exception is thrown + assert_eq(s.has_function("test_exception"), true) + assert_eq(s.get_global_exceptions(), 0) + s.vmcall("test_exception") + assert_eq(s.get_global_exceptions(), 1) + func callable_function(): return