Skip to content

Commit 7c68bbe

Browse files
committed
Implement JavaScriptCallable for Signal & slot
1 parent 98f6b3b commit 7c68bbe

11 files changed

+184
-7
lines changed

javascript_binder.h

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,12 +88,15 @@ class JavaScriptBinder {
8888
virtual const JavaScriptClassInfo *parse_javascript_class(const Vector<uint8_t> &p_bytecode, const String &p_path, bool ignore_cacehe, JavaScriptError *r_error) = 0;
8989

9090
virtual JavaScriptGCHandler create_js_instance_for_godot_object(const JavaScriptClassInfo *p_class, Object *p_object) = 0;
91-
virtual Variant call_method(const JavaScriptGCHandler &p_object, const StringName &p_method, const Variant **p_args, int p_argcount, Callable::CallError &r_error) = 0;
9291
virtual bool get_instance_property(const JavaScriptGCHandler &p_object, const StringName &p_name, Variant &r_ret) = 0;
9392
virtual bool set_instance_property(const JavaScriptGCHandler &p_object, const StringName &p_name, const Variant &p_value) = 0;
9493
virtual bool has_method(const JavaScriptGCHandler &p_object, const StringName &p_name) = 0;
9594
virtual bool has_signal(const JavaScriptClassInfo *p_class, const StringName &p_signal) = 0;
9695
virtual bool validate(const String &p_code, const String &p_path, JavaScriptError *r_error) = 0;
96+
97+
virtual Variant call_method(const JavaScriptGCHandler &p_object, const StringName &p_method, const Variant **p_args, int p_argcount, Callable::CallError &r_error) = 0;
98+
virtual Variant call(const JavaScriptGCHandler &p_fuction, const JavaScriptGCHandler &p_target, const Variant **p_args, int p_argcount, Callable::CallError &r_error) = 0;
99+
97100
#ifdef TOOLS_ENABLED
98101
virtual const Dictionary &get_modified_api() const = 0;
99102
#endif

javascript_callable.h

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
#ifndef JAVASCRIPT_CALLABLE_H
2+
#define JAVASCRIPT_CALLABLE_H
3+
4+
#include "core/variant/callable.h"
5+
#include "javascript_gc_handler.h"
6+
7+
class JavaScriptCallable : public CallableCustom {
8+
protected:
9+
JavaScriptGCHandler js_function;
10+
11+
public:
12+
JavaScriptCallable() {}
13+
JavaScriptCallable(const JavaScriptGCHandler &p_function) : js_function(p_function) {}
14+
virtual ~JavaScriptCallable() {}
15+
};
16+
17+
#endif // JAVASCRIPT_CALLABLE_H

javascript_gc_handler.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@ struct JavaScriptGCHandler {
128128
_FORCE_INLINE_ bool is_finalized() const {
129129
return flags & FLAG_FINALIZED;
130130
}
131+
131132
_FORCE_INLINE_ bool is_valid_javascript_object() const {
132133
return context != NULL && javascript_object != NULL && !is_finalized();
133134
}

javascript_language.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,13 +36,15 @@ JavaScriptLanguage::JavaScriptLanguage() {
3636
ERR_FAIL_COND(singleton);
3737
singleton = this;
3838
main_binder = memnew(QuickJSBinder);
39+
callable_middleman = memnew(CallableMiddleman);
3940
instance_binding_callbacks.create_callback = JavaScriptInstanceBindingCallbacks::create_callback;
4041
instance_binding_callbacks.free_callback = JavaScriptInstanceBindingCallbacks::free_callback;
4142
instance_binding_callbacks.reference_callback = JavaScriptInstanceBindingCallbacks::reference_callback;
4243
}
4344

4445
JavaScriptLanguage::~JavaScriptLanguage() {
4546
memdelete(main_binder);
47+
memdelete(callable_middleman);
4648
}
4749

4850
void JavaScriptLanguage::init() {

javascript_language.h

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@
55
#include "javascript.h"
66
#include "quickjs/quickjs_binder.h"
77

8+
class CallableMiddleman : public Object {
9+
GDCLASS(CallableMiddleman, Object);
10+
};
11+
812
class JavaScriptBinder;
913
class JavaScriptLanguage : public ScriptLanguage {
1014
friend class JavaScriptBinder;
@@ -18,6 +22,8 @@ class JavaScriptLanguage : public ScriptLanguage {
1822
JavaScriptBinder *main_binder;
1923
HashMap<Thread::ID, JavaScriptBinder *> thread_binder_map;
2024
GDNativeInstanceBindingCallbacks instance_binding_callbacks;
25+
26+
CallableMiddleman *callable_middleman;
2127
#ifdef TOOLS_ENABLED
2228
HashSet<Ref<JavaScript>> scripts;
2329
#endif
@@ -32,6 +38,10 @@ class JavaScriptLanguage : public ScriptLanguage {
3238
return nullptr;
3339
}
3440

41+
_FORCE_INLINE_ CallableMiddleman *get_callable_middleman() const {
42+
return callable_middleman;
43+
}
44+
3545
_FORCE_INLINE_ virtual String get_name() const override { return "JavaScript"; }
3646
const GDNativeInstanceBindingCallbacks *get_instance_binding_callbacks() const { return &instance_binding_callbacks; }
3747
#ifdef TOOLS_ENABLED

quickjs/quickjs_binder.cpp

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
#ifdef TOOLS_ENABLED
1616
#include "editor/editor_settings.h"
1717
#endif
18+
#include "quickjs_callable.h"
1819
#include <cstring>
1920

2021
SafeNumeric<uint32_t> QuickJSBinder::global_context_id;
@@ -547,6 +548,19 @@ JSValue QuickJSBinder::godot_instance_from_id(JSContext *ctx, JSValue this_val,
547548
return variant_to_var(ctx, obj);
548549
}
549550

551+
JSValue QuickJSBinder::godot_object_method_connect(JSContext *ctx, JSValue this_val, int argc, JSValue *argv) {
552+
#ifdef DEBUG_METHODS_ENABLED
553+
ERR_FAIL_COND_V(argc < 2 || !JS_IsString(argv[0]) || !JS_IsFunction(ctx, argv[1]), JS_ThrowTypeError(ctx, "string and function expected for %s.%s", "Object", "connect"));
554+
#endif
555+
JavaScriptGCHandler *bind = BINDING_DATA_FROM_JS(ctx, this_val);
556+
QuickJSBinder *binder = QuickJSBinder::get_context_binder(ctx);
557+
Object *obj = bind->get_godot_object();
558+
StringName signal = binder->js_to_string(ctx, argv[0]);
559+
Callable callable = memnew(QuickJSCallable(ctx, argv[1]));
560+
obj->connect(signal, callable);
561+
return JS_UNDEFINED;
562+
}
563+
550564
void QuickJSBinder::add_debug_binding_info(JSContext *ctx, JSValueConst p_obj, const JavaScriptGCHandler *p_bind) {
551565
if (!p_bind)
552566
return;
@@ -831,6 +845,15 @@ JSClassID QuickJSBinder::register_class(const ClassDB::ClassInfo *p_cls) {
831845
{
832846
godot_methods.resize(internal_godot_method_id + p_cls->method_map.size());
833847
for (const KeyValue<StringName, MethodBind *> &pair : p_cls->method_map) {
848+
if (p_cls->name == "Object") {
849+
const char *connect = "connect";
850+
if (pair.key == connect) {
851+
JSValue func = JS_NewCFunction(ctx, godot_object_method_connect, connect, 3);
852+
JS_DefinePropertyValueStr(ctx, data.prototype, connect, func, PROP_DEF_DEFAULT);
853+
continue;
854+
}
855+
}
856+
834857
MethodBind *mb = pair.value;
835858
godot_methods.set(internal_godot_method_id, mb);
836859
CharString name = String(pair.key).ascii();
@@ -2052,9 +2075,18 @@ Variant QuickJSBinder::call_method(const JavaScriptGCHandler &p_object, const St
20522075
JSAtom atom = get_atom(ctx, p_method);
20532076
JSValue method = JS_GetProperty(ctx, object, atom);
20542077
JS_FreeAtom(ctx, atom);
2078+
JavaScriptGCHandler func;
2079+
func.javascript_object = JS_VALUE_GET_PTR(method);
2080+
Variant ret = call(func, p_object, p_args, p_argcount, r_error);
2081+
JS_FreeValue(ctx, method);
2082+
return ret;
2083+
}
20552084

2085+
Variant QuickJSBinder::call(const JavaScriptGCHandler &p_fuction, const JavaScriptGCHandler &p_target, const Variant **p_args, int p_argcount, Callable::CallError &r_error) {
20562086
JSValue return_val = JS_UNDEFINED;
20572087
JSValue *argv = NULL;
2088+
JSValue method = JS_MKPTR(JS_TAG_OBJECT, p_fuction.javascript_object);
2089+
JSValue object = p_target.is_valid_javascript_object() ? JS_MKPTR(JS_TAG_OBJECT, p_target.javascript_object) : JS_UNDEFINED;
20582090

20592091
if (!JS_IsFunction(ctx, method) || JS_IsPureCFunction(ctx, method)) {
20602092
r_error.error = Callable::CallError::CALL_ERROR_INVALID_METHOD;
@@ -2086,7 +2118,6 @@ Variant QuickJSBinder::call_method(const JavaScriptGCHandler &p_object, const St
20862118
memdelete_arr(argv);
20872119
}
20882120
JS_FreeValue(ctx, return_val);
2089-
JS_FreeValue(ctx, method);
20902121
return ret;
20912122
}
20922123

quickjs/quickjs_binder.h

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,8 @@ class QuickJSBinder : public JavaScriptBinder {
178178
static JSValue godot_load(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv);
179179
static JSValue godot_instance_from_id(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv);
180180

181+
static JSValue godot_object_method_connect(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv);
182+
181183
static void add_debug_binding_info(JSContext *ctx, JSValueConst p_obj, const JavaScriptGCHandler *p_bind);
182184

183185
const JavaScriptClassInfo *register_javascript_class(const JSValueConst &p_constructor, const String &p_path);
@@ -330,13 +332,16 @@ class QuickJSBinder : public JavaScriptBinder {
330332
const JavaScriptClassInfo *parse_javascript_class_from_module(ModuleCache *p_module, const String &p_path, JavaScriptError *r_error);
331333

332334
virtual JavaScriptGCHandler create_js_instance_for_godot_object(const JavaScriptClassInfo *p_class, Object *p_object) override;
333-
virtual Variant call_method(const JavaScriptGCHandler &p_object, const StringName &p_method, const Variant **p_args, int p_argcount, Callable::CallError &r_error) override;
335+
334336
virtual bool get_instance_property(const JavaScriptGCHandler &p_object, const StringName &p_name, Variant &r_ret) override;
335337
virtual bool set_instance_property(const JavaScriptGCHandler &p_object, const StringName &p_name, const Variant &p_value) override;
336338
virtual bool has_method(const JavaScriptGCHandler &p_object, const StringName &p_name) override;
337339
virtual bool has_signal(const JavaScriptClassInfo *p_class, const StringName &p_signal) override;
338340
virtual bool validate(const String &p_code, const String &p_path, JavaScriptError *r_error) override;
339341

342+
virtual Variant call_method(const JavaScriptGCHandler &p_object, const StringName &p_method, const Variant **p_args, int p_argcount, Callable::CallError &r_error) override;
343+
virtual Variant call(const JavaScriptGCHandler &p_fuction, const JavaScriptGCHandler &p_target, const Variant **p_args, int p_argcount, Callable::CallError &r_error) override;
344+
340345
#ifdef TOOLS_ENABLED
341346
virtual const Dictionary &get_modified_api() const override { return modified_api; }
342347
#endif

quickjs/quickjs_callable.cpp

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
#include "quickjs_callable.h"
2+
#include "../javascript_language.h"
3+
#include "quickjs/quickjs.h"
4+
#include "quickjs_binder.h"
5+
6+
bool QuickJSCallable::compare_equal(const CallableCustom *p_a, const CallableCustom *p_b) {
7+
const QuickJSCallable *a = static_cast<const QuickJSCallable *>(p_a);
8+
const QuickJSCallable *b = static_cast<const QuickJSCallable *>(p_b);
9+
return a->js_function.javascript_object == b->js_function.javascript_object;
10+
}
11+
12+
bool QuickJSCallable::compare_less(const CallableCustom *p_a, const CallableCustom *p_b) {
13+
if (compare_equal(p_a, p_b)) {
14+
return false;
15+
}
16+
return p_a < p_b;
17+
}
18+
19+
QuickJSCallable::QuickJSCallable(JSContext *ctx, const JSValue &p_value) {
20+
ERR_FAIL_COND(JS_IsFunction(ctx, p_value));
21+
JSValue v = JS_DupValue(ctx, p_value);
22+
js_function.context = ctx;
23+
js_function.javascript_object = JS_VALUE_GET_PTR(v);
24+
}
25+
26+
QuickJSCallable::QuickJSCallable(const JavaScriptGCHandler &p_function) : JavaScriptCallable(p_function) {
27+
JSValue js_func = JS_MKPTR(JS_TAG_OBJECT, p_function.javascript_object);
28+
JSContext *ctx = static_cast<JSContext *>(p_function.context);
29+
ERR_FAIL_COND(JS_IsFunction(ctx, js_func));
30+
JS_DupValue(ctx, js_func);
31+
}
32+
33+
QuickJSCallable::~QuickJSCallable() {
34+
if (js_function.is_valid_javascript_object()) {
35+
JSContext *ctx = static_cast<JSContext *>(js_function.context);
36+
JSValue js_func = JS_MKPTR(JS_TAG_OBJECT, js_function.javascript_object);
37+
JS_FreeValue(ctx, js_func);
38+
}
39+
}
40+
41+
uint32_t QuickJSCallable::hash() const {
42+
JSValue js_func = JS_MKPTR(JS_TAG_OBJECT, js_function.javascript_object);
43+
return hash_murmur3_one_64((uint64_t)JS_VALUE_GET_PTR(js_func));
44+
}
45+
46+
String QuickJSCallable::get_as_text() const {
47+
QuickJSBinder *binder = QuickJSBinder::get_context_binder(static_cast<JSContext *>(js_function.context));
48+
JSValue js_func = JS_MKPTR(JS_TAG_OBJECT, js_function.javascript_object);
49+
JSContext *ctx = static_cast<JSContext *>(js_function.context);
50+
String text = binder->js_to_string(ctx, js_func);
51+
return text;
52+
}
53+
54+
ObjectID QuickJSCallable::get_object() const {
55+
return JavaScriptLanguage::get_singleton()->get_callable_middleman()->get_instance_id();
56+
}
57+
58+
void QuickJSCallable::call(const Variant **p_arguments, int p_argcount, Variant &r_return_value, Callable::CallError &r_call_error) const {
59+
QuickJSBinder *binder = QuickJSBinder::get_context_binder(static_cast<JSContext *>(js_function.context));
60+
JSValue js_func = JS_MKPTR(JS_TAG_OBJECT, js_function.javascript_object);
61+
JavaScriptGCHandler func;
62+
func.javascript_object = JS_VALUE_GET_PTR(js_func);
63+
JavaScriptGCHandler caller;
64+
r_return_value = binder->call(func, caller, p_arguments, p_argcount, r_call_error);
65+
}

quickjs/quickjs_callable.h

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
#ifndef QUICKJS_CALLABLE_H
2+
#define QUICKJS_CALLABLE_H
3+
#include "../javascript_callable.h"
4+
#include "quickjs/quickjs.h"
5+
struct JSValue;
6+
7+
class QuickJSCallable : public JavaScriptCallable {
8+
static bool compare_equal(const CallableCustom *p_a, const CallableCustom *p_b);
9+
static bool compare_less(const CallableCustom *p_a, const CallableCustom *p_b);
10+
11+
public:
12+
QuickJSCallable(JSContext *ctx, const JSValue &p_value);
13+
QuickJSCallable(const JavaScriptGCHandler &p_function);
14+
virtual ~QuickJSCallable();
15+
16+
virtual uint32_t hash() const override;
17+
virtual String get_as_text() const override;
18+
19+
virtual CompareEqualFunc get_compare_equal_func() const override { return QuickJSCallable::compare_equal; }
20+
virtual CompareLessFunc get_compare_less_func() const override { return QuickJSCallable::compare_less; }
21+
22+
virtual ObjectID get_object() const override;
23+
virtual void call(const Variant **p_arguments, int p_argcount, Variant &r_return_value, Callable::CallError &r_call_error) const override;
24+
};
25+
26+
#endif // QUICKJS_CALLABLE_H

tests/UnitTest.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,27 @@ test('new Resource', () => {
1616
return ok;
1717
}, 'core');
1818

19+
test('Object.prototype.get_class', () => {
20+
let obj = new godot.Object();
21+
let ok = obj.get_class() === 'Object';
22+
obj.free();
23+
return ok;
24+
}, 'core')
25+
26+
test('Object.prototype.connect', () => {
27+
let ok = typeof godot.Object.prototype.connect === 'function';
28+
if (!ok) return ok;
29+
30+
let obj = new godot.Object();
31+
obj.connect('script_changed', (...args)=> {
32+
console.log(`signal 'script_changed' emited with:`, ...args);
33+
ok = true;
34+
});
35+
obj.emit_signal('script_changed');
36+
obj.free();
37+
return ok;
38+
}, 'core')
39+
1940

2041
// --------------------------- Unit Test Implementation ------------------------
2142
function test(description, blcok, group = 'default') {

0 commit comments

Comments
 (0)