@gjsify/node-gi 0.42.0 → 0.44.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/class.cc CHANGED
@@ -645,63 +645,18 @@ static void NodeGiClassInit(gpointer g_class, gpointer class_data) {
645
645
  }
646
646
  }
647
647
 
648
- // callParentVfunc(handle, vfuncName, args?) -> unknown
649
- //
650
- // Chain up to the parent implementation of an overridden vfunc — the engine half
651
- // of `super.vfunc_<name>(...)`. Resolves the NodeGiVFunc record for `vfuncName`
652
- // nearest the instance's type (the record whose trampoline currently owns the
653
- // vtable slot) and ffi_call's its captured parentPtr — the function that was in
654
- // the slot BEFORE the override was installed (the C default, or a JS override
655
- // further up the chain). The same `cif` the override's closure was built from
656
- // describes the call signature (instance + declared args → return), so it is
657
- // reused to call out. `this` (args[0]) goes back in as the instance, keeping the
658
- // canonical toggle-ref wrapper identity. Marshals IN args (JsToGIArgument) +
659
- // the return (gi_type_info_extract_ffi_return_value → GIArgumentToJs); throws a
660
- // GLib.Error for a can-throw vfunc whose parent set the GError.
661
- //
662
- // OUT / INOUT args: routed through per-arg storage slots exactly like the
663
- // function-invoke path (calls.cc InvokeFunctionInfo). The parent's ffi signature
664
- // takes a POINTER for each OUT/INOUT param, so giArgs[1+i] carries &slots[i] (the
665
- // stable storage the C parent writes THROUGH) — INOUT slots are pre-marshalled from
666
- // the JS input, OUT slots start zeroed; a caller-allocates OUT (fixed C array /
667
- // boxed struct) gets a g_malloc0 blob instead. The JS caller passes only IN + INOUT
668
- // args positionally (OUT params are engine-managed), and the result follows the GJS
669
- // return-tuple convention `[returnValue?, ...outArgs]` — one value bare, several as
670
- // an Array, matching exactly what a JS override of that vfunc receives as its call
671
- // args. Read-back reuses ReadOutOrReturn (array-length slots, containers, boxed).
648
+ // ---- the shared vfunc CALL-OUT: marshal, ffi_call, assemble the GJS return ----
672
649
  //
673
- // MULTI-LEVEL chain-up (registered chains, G2): chains to the DEEPEST registered
674
- // override's captured parent (the C-side default below the whole registered chain),
675
- // NOT the nearest. On a multi-level chain the level-to-level `super.vfunc_<name>()`
676
- // hops are resolved by the JS PROTOTYPE chain (each registered ancestor's
677
- // `vfunc_<name>` is a real JS method, invoked directly), so every level's JS impl
678
- // has already run by the time the chain bottoms out at the introspected base's
679
- // prototype and reaches this thunk — at which point the only thing left to call is
680
- // the C default. Using the nearest record would re-enter an intermediate level's
681
- // trampoline (a double-run / infinite loop). See FindDeepestVFuncRecord.
682
- Napi::Value CallParentVfunc(const Napi::CallbackInfo& info) {
683
- Napi::Env env = info.Env();
684
- if (info.Length() < 2 || !info[1].IsString()) {
685
- Napi::TypeError::New(env, "callParentVfunc(handle, vfuncName: string, args?: unknown[])")
686
- .ThrowAsJavaScriptException();
687
- return env.Null();
688
- }
689
- GObject* obj = UnwrapGObject(env, info[0]);
690
- if (obj == nullptr) return env.Null(); // UnwrapGObject threw or it was null
691
- std::string vname = info[1].As<Napi::String>().Utf8Value();
692
- Napi::Array args = (info.Length() >= 3 && info[2].IsArray()) ? info[2].As<Napi::Array>()
693
- : Napi::Array::New(env, 0);
694
-
695
- NodeGiVFunc* vf = FindDeepestVFuncRecord(G_OBJECT_TYPE(obj), vname);
696
- if (vf == nullptr || vf->parentPtr == nullptr || vf->info == nullptr) {
697
- Napi::Error::New(env, "no parent vfunc '" + vname + "' to chain up to on " +
698
- g_type_name(G_OBJECT_TYPE(obj)) +
699
- " (is it overridden by a registerClass subclass?)")
700
- .ThrowAsJavaScriptException();
701
- return env.Null();
702
- }
703
-
704
- GICallableInfo* ci = reinterpret_cast<GICallableInfo*>(vf->info);
650
+ // One body, two callers: `super.vfunc_x(...)` chaining to a captured parent slot
651
+ // (CallParentVfunc) and a plain introspected `inst.vfunc_x(...)` dispatching to the
652
+ // class's own slot (CallClassVfunc). Both have the same three inputs — the vfunc's
653
+ // GICallableInfo, an ffi_cif describing [instance, declared args, GError**] and the
654
+ // function pointer — so the ~300 lines of IN/OUT/INOUT marshalling below are shared
655
+ // rather than copied. `where` prefixes every diagnostic, so each caller names the
656
+ // spelling the program actually used.
657
+ static Napi::Value InvokeVFuncPointer(Napi::Env env, GObject* obj, GICallableInfo* ci,
658
+ ffi_cif* cif, gpointer fnPtr, const std::string& where,
659
+ Napi::Array args) {
705
660
  unsigned int nDeclared = gi_callable_info_get_n_args(ci);
706
661
  bool canThrow = gi_callable_info_can_throw_gerror(ci);
707
662
 
@@ -815,7 +770,7 @@ Napi::Value CallParentVfunc(const Napi::CallbackInfo& info) {
815
770
  }
816
771
  if (size == 0) {
817
772
  Napi::TypeError::New(
818
- env, "super." + vname + ": caller-allocates OUT parameter type is not yet supported")
773
+ env, where + ": caller-allocates OUT parameter type is not yet supported")
819
774
  .ThrowAsJavaScriptException();
820
775
  ok = false;
821
776
  } else {
@@ -825,7 +780,7 @@ Napi::Value CallParentVfunc(const Napi::CallbackInfo& info) {
825
780
  giArgs[1 + i].v_pointer = blob;
826
781
  }
827
782
  } else if (!IsSupportedOutType(ti, &why)) {
828
- Napi::TypeError::New(env, "super." + vname + ": OUT " + why +
783
+ Napi::TypeError::New(env, where + ": OUT " + why +
829
784
  " parameters are not yet supported")
830
785
  .ThrowAsJavaScriptException();
831
786
  ok = false;
@@ -842,7 +797,7 @@ Napi::Value CallParentVfunc(const Napi::CallbackInfo& info) {
842
797
  Napi::Value v = jsCursor < args.Length() ? args.Get(jsCursor) : env.Undefined();
843
798
  jsCursor++;
844
799
  if (!IsSupportedContainerType(ti, &why)) {
845
- Napi::TypeError::New(env, "super." + vname + ": INOUT " + why +
800
+ Napi::TypeError::New(env, where + ": INOUT " + why +
846
801
  " parameters are not yet supported")
847
802
  .ThrowAsJavaScriptException();
848
803
  ok = false;
@@ -880,7 +835,8 @@ Napi::Value CallParentVfunc(const Napi::CallbackInfo& info) {
880
835
  Napi::Value v = jsCursor < args.Length() ? args.Get(jsCursor) : env.Undefined();
881
836
  jsCursor++;
882
837
  GITransfer tr = gi_arg_info_get_ownership_transfer(ai);
883
- if (JsToGIArgument(env, v, ti, &slots[i], &holds[i], tr, &ownedInStrings))
838
+ if (JsToGIArgument(env, v, ti, &slots[i], &holds[i], tr, &ownedInStrings, nullptr,
839
+ nullptr, nullptr, gi_base_info_get_name(reinterpret_cast<GIBaseInfo*>(ai))))
884
840
  giArgs[1 + i].v_pointer = &slots[i];
885
841
  else
886
842
  ok = false; // JsToGIArgument already threw
@@ -895,7 +851,7 @@ Napi::Value CallParentVfunc(const Napi::CallbackInfo& info) {
895
851
  Napi::Value v = jsCursor < args.Length() ? args.Get(jsCursor) : env.Undefined();
896
852
  jsCursor++;
897
853
  if (!IsSupportedContainerType(ti, &why)) {
898
- Napi::TypeError::New(env, "super." + vname + ": IN " + why +
854
+ Napi::TypeError::New(env, where + ": IN " + why +
899
855
  " parameters are not yet supported")
900
856
  .ThrowAsJavaScriptException();
901
857
  ok = false;
@@ -924,7 +880,8 @@ Napi::Value CallParentVfunc(const Napi::CallbackInfo& info) {
924
880
  Napi::Value v = jsCursor < args.Length() ? args.Get(jsCursor) : env.Undefined();
925
881
  jsCursor++;
926
882
  GITransfer tr = gi_arg_info_get_ownership_transfer(ai);
927
- ok = JsToGIArgument(env, v, ti, &giArgs[1 + i], &holds[i], tr, &ownedInStrings);
883
+ ok = JsToGIArgument(env, v, ti, &giArgs[1 + i], &holds[i], tr, &ownedInStrings, nullptr,
884
+ nullptr, nullptr, gi_base_info_get_name(reinterpret_cast<GIBaseInfo*>(ai)));
928
885
  }
929
886
 
930
887
  avalue.push_back(&giArgs[1 + i]);
@@ -951,7 +908,7 @@ Napi::Value CallParentVfunc(const Napi::CallbackInfo& info) {
951
908
  GITypeInfo* retType = gi_callable_info_get_return_type(ci);
952
909
  GIFFIReturnValue ffiRet;
953
910
  ffiRet.v_uint64 = 0;
954
- ffi_call(&vf->cif, reinterpret_cast<void (*)(void)>(vf->parentPtr), &ffiRet, avalue.data());
911
+ ffi_call(cif, reinterpret_cast<void (*)(void)>(fnPtr), &ffiRet, avalue.data());
955
912
 
956
913
  if (canThrow && error != nullptr) {
957
914
  gi_base_info_unref(retType);
@@ -959,7 +916,7 @@ Napi::Value CallParentVfunc(const Napi::CallbackInfo& info) {
959
916
  for (gpointer s : ownedInStrings) g_free(s); // callee did not adopt them on error
960
917
  for (gpointer b : callerAllocBlob)
961
918
  if (b != nullptr) g_free(b);
962
- ThrowGError(env, error, "super." + vname);
919
+ ThrowGError(env, error, where);
963
920
  return env.Null();
964
921
  }
965
922
 
@@ -1028,6 +985,225 @@ Napi::Value CallParentVfunc(const Napi::CallbackInfo& info) {
1028
985
  return arr;
1029
986
  }
1030
987
 
988
+ // callParentVfunc(handle, vfuncName, args?) -> unknown
989
+ //
990
+ // Chain up to the parent implementation of an overridden vfunc — the engine half
991
+ // of `super.vfunc_<name>(...)`. Resolves the NodeGiVFunc record for `vfuncName`
992
+ // nearest the instance's type (the record whose trampoline currently owns the
993
+ // vtable slot) and ffi_call's its captured parentPtr — the function that was in
994
+ // the slot BEFORE the override was installed (the C default, or a JS override
995
+ // further up the chain). The same `cif` the override's closure was built from
996
+ // describes the call signature (instance + declared args → return), so it is
997
+ // reused to call out. `this` (args[0]) goes back in as the instance, keeping the
998
+ // canonical toggle-ref wrapper identity. Marshals IN args (JsToGIArgument) +
999
+ // the return (gi_type_info_extract_ffi_return_value → GIArgumentToJs); throws a
1000
+ // GLib.Error for a can-throw vfunc whose parent set the GError.
1001
+ //
1002
+ // OUT / INOUT args: routed through per-arg storage slots exactly like the
1003
+ // function-invoke path (calls.cc InvokeFunctionInfo). The parent's ffi signature
1004
+ // takes a POINTER for each OUT/INOUT param, so giArgs[1+i] carries &slots[i] (the
1005
+ // stable storage the C parent writes THROUGH) — INOUT slots are pre-marshalled from
1006
+ // the JS input, OUT slots start zeroed; a caller-allocates OUT (fixed C array /
1007
+ // boxed struct) gets a g_malloc0 blob instead. The JS caller passes only IN + INOUT
1008
+ // args positionally (OUT params are engine-managed), and the result follows the GJS
1009
+ // return-tuple convention `[returnValue?, ...outArgs]` — one value bare, several as
1010
+ // an Array, matching exactly what a JS override of that vfunc receives as its call
1011
+ // args. Read-back reuses ReadOutOrReturn (array-length slots, containers, boxed).
1012
+ //
1013
+ // MULTI-LEVEL chain-up (registered chains, G2): chains to the DEEPEST registered
1014
+ // override's captured parent (the C-side default below the whole registered chain),
1015
+ // NOT the nearest. On a multi-level chain the level-to-level `super.vfunc_<name>()`
1016
+ // hops are resolved by the JS PROTOTYPE chain (each registered ancestor's
1017
+ // `vfunc_<name>` is a real JS method, invoked directly), so every level's JS impl
1018
+ // has already run by the time the chain bottoms out at the introspected base's
1019
+ // prototype and reaches this thunk — at which point the only thing left to call is
1020
+ // the C default. Using the nearest record would re-enter an intermediate level's
1021
+ // trampoline (a double-run / infinite loop). See FindDeepestVFuncRecord.
1022
+ Napi::Value CallParentVfunc(const Napi::CallbackInfo& info) {
1023
+ Napi::Env env = info.Env();
1024
+ if (info.Length() < 2 || !info[1].IsString()) {
1025
+ Napi::TypeError::New(env, "callParentVfunc(handle, vfuncName: string, args?: unknown[])")
1026
+ .ThrowAsJavaScriptException();
1027
+ return env.Null();
1028
+ }
1029
+ GObject* obj = UnwrapGObject(env, info[0]);
1030
+ if (obj == nullptr) return env.Null(); // UnwrapGObject threw or it was null
1031
+ std::string vname = info[1].As<Napi::String>().Utf8Value();
1032
+ Napi::Array args = (info.Length() >= 3 && info[2].IsArray()) ? info[2].As<Napi::Array>()
1033
+ : Napi::Array::New(env, 0);
1034
+
1035
+ NodeGiVFunc* vf = FindDeepestVFuncRecord(G_OBJECT_TYPE(obj), vname);
1036
+ if (vf == nullptr || vf->parentPtr == nullptr || vf->info == nullptr) {
1037
+ Napi::Error::New(env, "no parent vfunc '" + vname + "' to chain up to on " +
1038
+ g_type_name(G_OBJECT_TYPE(obj)) +
1039
+ " (is it overridden by a registerClass subclass?)")
1040
+ .ThrowAsJavaScriptException();
1041
+ return env.Null();
1042
+ }
1043
+
1044
+ return InvokeVFuncPointer(env, obj, reinterpret_cast<GICallableInfo*>(vf->info), &vf->cif,
1045
+ vf->parentPtr, "super." + vname, args);
1046
+ }
1047
+
1048
+ // ---- vfunc dispatch on an INTROSPECTED class (no registerClass involved) ----
1049
+ //
1050
+ // `new Gtk.Box().vfunc_add_child(builder, child, null)` works on gjs, and it is the
1051
+ // only route to a GtkBuildable adder — `add_child` is introspected as a vfunc ONLY,
1052
+ // so there is no plain method to call (measured on gjs 1.88.1; ADR 0027 § Context
1053
+ // rests on it, and two React-for-GJS renderers route every insertion through it).
1054
+ // node-gi had ONLY the chain-up thunk, which needs a registerClass override record
1055
+ // to find a captured parent pointer, so every `vfunc_*` on a plain introspected
1056
+ // instance threw "no parent vfunc … to chain up to".
1057
+ //
1058
+ // gjs resolves such a name against the prototype's own GIObjectInfo, its implemented
1059
+ // interfaces and then its parents, and it resolves the ADDRESS at that prototype's
1060
+ // GType (gi/object.cpp find_vfunc_on_parents). This mirrors that: walk the object-info
1061
+ // parent chain, then ask girepository for the vtable slot of the class the prototype
1062
+ // belongs to. A resolution that FAILS is not an error here — it is the signal for
1063
+ // gi.js to fall back to the chain-up thunk, which is what keeps `super.vfunc_dispose()`
1064
+ // working (GObject's own vfuncs report GI_UNKNOWN as their struct offset, so
1065
+ // gi_vfunc_info_get_address cannot locate them).
1066
+
1067
+ // One resolved, immediately callable class vfunc. `usable` false = no such vfunc on
1068
+ // this class, or girepository cannot locate its slot for this GType.
1069
+ struct NodeGiClassVFunc {
1070
+ GIVFuncInfo* info = nullptr; // owned; process lifetime (a GIBaseInfo is immutable)
1071
+ GIFunctionInvoker invoker{}; // cif + native_address, ffi_prep'd once
1072
+ GType gtype = G_TYPE_INVALID; // the implementor whose slot `invoker` points into
1073
+ bool usable = false;
1074
+ };
1075
+
1076
+ // Cached per (namespace, typeName, vfuncName): the ffi cif behind an invoker costs a
1077
+ // malloc to build and a renderer calls vfunc_add_child on every insertion. thread_local
1078
+ // because GIBaseInfo refcounting is not thread-safe and a worker_threads env is its own
1079
+ // JS world with its own repository handles.
1080
+ static thread_local std::map<std::string, NodeGiClassVFunc> g_classVFuncs;
1081
+
1082
+ // Walk the object-info PARENT chain, searching each level's own vfuncs AND the
1083
+ // interfaces that level implements (gi_object_info_find_vfunc_using_interfaces —
1084
+ // `add_child` lives on the GtkBuildable INTERFACE, not on GtkBox). Returns a new
1085
+ // ref, or nullptr when no ancestor declares `name`.
1086
+ static GIVFuncInfo* FindVFuncOnClassOrParents(GIObjectInfo* start, const char* name) {
1087
+ GIObjectInfo* walk =
1088
+ reinterpret_cast<GIObjectInfo*>(gi_base_info_ref(reinterpret_cast<GIBaseInfo*>(start)));
1089
+ GIVFuncInfo* found = nullptr;
1090
+ while (walk != nullptr && found == nullptr) {
1091
+ // The declarer out-param is optional and we do not need it: the ADDRESS is resolved
1092
+ // against the class the PROTOTYPE belongs to, never against the declaring level.
1093
+ found = gi_object_info_find_vfunc_using_interfaces(walk, name, nullptr);
1094
+ GIObjectInfo* parent = found == nullptr ? gi_object_info_get_parent(walk) : nullptr;
1095
+ gi_base_info_unref(reinterpret_cast<GIBaseInfo*>(walk));
1096
+ walk = parent;
1097
+ }
1098
+ return found;
1099
+ }
1100
+
1101
+ static NodeGiClassVFunc* ResolveClassVFunc(const std::string& ns, const std::string& tn,
1102
+ const std::string& vfuncName) {
1103
+ const std::string key = ns + "." + tn + "." + vfuncName;
1104
+ auto it = g_classVFuncs.find(key);
1105
+ if (it != g_classVFuncs.end()) return &it->second;
1106
+
1107
+ NodeGiClassVFunc entry;
1108
+ GIRepository* repo = DupDefaultRepository();
1109
+ GIBaseInfo* base = gi_repository_find_by_name(repo, ns.c_str(), tn.c_str());
1110
+ if (base != nullptr) {
1111
+ if (GI_IS_OBJECT_INFO(base)) {
1112
+ GIObjectInfo* oi = reinterpret_cast<GIObjectInfo*>(base);
1113
+ GType gtype =
1114
+ gi_registered_type_info_get_g_type(reinterpret_cast<GIRegisteredTypeInfo*>(oi));
1115
+ GIVFuncInfo* vi = FindVFuncOnClassOrParents(oi, vfuncName.c_str());
1116
+ if (vi != nullptr && gtype != G_TYPE_INVALID && G_TYPE_IS_CLASSED(gtype)) {
1117
+ // gi_vfunc_info_get_address reads the slot out of the LIVE class struct, which
1118
+ // exists only once the class has been referenced — and the ref is KEPT so the
1119
+ // vtable the cached address points into cannot be torn down under us. gjs holds
1120
+ // classes the same way; an instantiable GObject class lives for the process.
1121
+ g_type_class_ref(gtype);
1122
+ GError* err = nullptr;
1123
+ void* addr = gi_vfunc_info_get_address(vi, gtype, &err);
1124
+ if (addr != nullptr &&
1125
+ gi_function_invoker_new_for_address(addr, reinterpret_cast<GICallableInfo*>(vi),
1126
+ &entry.invoker, &err)) {
1127
+ entry.info = reinterpret_cast<GIVFuncInfo*>(
1128
+ gi_base_info_ref(reinterpret_cast<GIBaseInfo*>(vi)));
1129
+ entry.gtype = gtype;
1130
+ entry.usable = true;
1131
+ }
1132
+ g_clear_error(&err);
1133
+ }
1134
+ if (vi != nullptr) gi_base_info_unref(reinterpret_cast<GIBaseInfo*>(vi));
1135
+ }
1136
+ gi_base_info_unref(base);
1137
+ }
1138
+ g_object_unref(repo);
1139
+ return &g_classVFuncs.emplace(key, entry).first->second;
1140
+ }
1141
+
1142
+ // hasClassVfunc(namespace, typeName, vfuncName) -> boolean
1143
+ //
1144
+ // Whether `Ns.Type.prototype.vfunc_<name>` can be MATERIALIZED as a direct call — the
1145
+ // gate gi.js asks before defining one, so a name that resolves to nothing stays
1146
+ // `undefined` (gjs parity: an unknown vfunc is undefined, never a throw-on-call thunk)
1147
+ // and a name we cannot address falls through to the chain-up thunk.
1148
+ Napi::Value HasClassVfunc(const Napi::CallbackInfo& info) {
1149
+ Napi::Env env = info.Env();
1150
+ if (info.Length() < 3 || !info[0].IsString() || !info[1].IsString() || !info[2].IsString()) {
1151
+ Napi::TypeError::New(env,
1152
+ "hasClassVfunc(namespace: string, typeName: string, vfuncName: string)")
1153
+ .ThrowAsJavaScriptException();
1154
+ return env.Null();
1155
+ }
1156
+ NodeGiClassVFunc* entry = ResolveClassVFunc(info[0].As<Napi::String>().Utf8Value(),
1157
+ info[1].As<Napi::String>().Utf8Value(),
1158
+ info[2].As<Napi::String>().Utf8Value());
1159
+ return Napi::Boolean::New(env, entry->usable);
1160
+ }
1161
+
1162
+ // callClassVfunc(handle, namespace, typeName, vfuncName, args?) -> unknown
1163
+ //
1164
+ // Invoke the vtable entry `Ns.Type` carries for `vfuncName`, with `handle` as the
1165
+ // instance. Same marshalling and same GJS return-tuple convention as the chain-up
1166
+ // path (InvokeVFuncPointer); the only difference is where the function pointer came
1167
+ // from.
1168
+ Napi::Value CallClassVfunc(const Napi::CallbackInfo& info) {
1169
+ Napi::Env env = info.Env();
1170
+ if (info.Length() < 4 || !info[1].IsString() || !info[2].IsString() || !info[3].IsString()) {
1171
+ Napi::TypeError::New(
1172
+ env, "callClassVfunc(handle, namespace: string, typeName: string, vfuncName: string, "
1173
+ "args?: unknown[])")
1174
+ .ThrowAsJavaScriptException();
1175
+ return env.Null();
1176
+ }
1177
+ GObject* obj = UnwrapGObject(env, info[0]);
1178
+ if (obj == nullptr) return env.Null(); // UnwrapGObject threw or it was null
1179
+ const std::string ns = info[1].As<Napi::String>().Utf8Value();
1180
+ const std::string tn = info[2].As<Napi::String>().Utf8Value();
1181
+ const std::string vname = info[3].As<Napi::String>().Utf8Value();
1182
+ Napi::Array args = (info.Length() >= 5 && info[4].IsArray()) ? info[4].As<Napi::Array>()
1183
+ : Napi::Array::New(env, 0);
1184
+ const std::string where = ns + "." + tn + ".prototype.vfunc_" + vname;
1185
+
1186
+ NodeGiClassVFunc* entry = ResolveClassVFunc(ns, tn, vname);
1187
+ if (!entry->usable) {
1188
+ Napi::Error::New(env, where + ": no addressable virtual function on " + ns + "." + tn)
1189
+ .ThrowAsJavaScriptException();
1190
+ return env.Null();
1191
+ }
1192
+ // The cif was prepared for THIS class's slot, so an instance of an unrelated type
1193
+ // would hand C a wrong `self` and crash the process rather than throw. A detached
1194
+ // `Ns.Type.prototype.vfunc_x.call(other)` is the reachable spelling.
1195
+ if (!g_type_is_a(G_OBJECT_TYPE(obj), entry->gtype)) {
1196
+ Napi::TypeError::New(env, where + ": `this` is a " +
1197
+ g_type_name(G_OBJECT_TYPE(obj)) + ", not a " +
1198
+ g_type_name(entry->gtype))
1199
+ .ThrowAsJavaScriptException();
1200
+ return env.Null();
1201
+ }
1202
+ return InvokeVFuncPointer(env, obj, reinterpret_cast<GICallableInfo*>(entry->info),
1203
+ &entry->invoker.cif, entry->invoker.native_address, where, args);
1204
+ }
1205
+
1206
+
1031
1207
  // Shared registration core: subclass `name` from an ALREADY-RESOLVED parent GObject
1032
1208
  // GType, reading the optional { properties, signals, vfuncs, template, children, ... }
1033
1209
  // from `optsValue` (a non-object → no options). Returns the tagged GType handle for
package/src/common.h CHANGED
@@ -32,6 +32,7 @@
32
32
 
33
33
  #include <atomic>
34
34
  #include <deque>
35
+ #include <map>
35
36
  #include <memory>
36
37
  #include <mutex>
37
38
  #include <string>
@@ -339,13 +340,17 @@ GClosure* NodeGiMakeGenericJsClosure(Napi::Env env, Napi::Value fn);
339
340
  // `values` (optional): enables the JS-value→GValue IN-arg boxing above; nullptr
340
341
  // (the default) keeps the previous behaviour (plain value → TypeError) on paths
341
342
  // that cannot release a created GValue.
343
+ // `argName` (optional): the introspected argument name, used ONLY in error
344
+ // messages so a refusal reads like gjs's ("Expected type string for argument
345
+ // 'name' but got type number"); nullptr degrades to a message without the name.
342
346
  bool JsToGIArgument(Napi::Env env, Napi::Value v, GITypeInfo* type, GIArgument* out,
343
347
  std::string* heldString,
344
348
  GITransfer transfer = GI_TRANSFER_NOTHING,
345
349
  std::vector<gpointer>* ownedStrings = nullptr,
346
350
  CreatedClosures* closures = nullptr,
347
351
  CreatedBytes* bytes = nullptr,
348
- CreatedValues* values = nullptr);
352
+ CreatedValues* values = nullptr,
353
+ const char* argName = nullptr);
349
354
 
350
355
  // ---- IN container building -----------------------------------------
351
356
  //
@@ -557,6 +562,9 @@ Napi::Value CallFunction(const Napi::CallbackInfo& info);
557
562
  Napi::Value CallMethod(const Napi::CallbackInfo& info);
558
563
  Napi::Value HasMethod(const Napi::CallbackInfo& info);
559
564
  Napi::Value HasClassMethod(const Napi::CallbackInfo& info);
565
+ Napi::Value ClassMethodArity(const Napi::CallbackInfo& info);
566
+ // The JS-visible IN-arg count of a callable — gjs's `Function.length` (see calls.cc).
567
+ int JsInArgCount(GICallableInfo* callable);
560
568
  // The nearest ancestor GType carrying object introspection info (a concrete type
561
569
  // may be private — GLocalFile); a new ref, or nullptr. Shared with object.cc so
562
570
  // method dispatch and prototype lookup resolve to the same class.
@@ -604,6 +612,8 @@ Napi::Value RegisterClass(const Napi::CallbackInfo& info);
604
612
  Napi::Value RegisterClassFromGType(const Napi::CallbackInfo& info);
605
613
  Napi::Value ConstructType(const Napi::CallbackInfo& info);
606
614
  Napi::Value CallParentVfunc(const Napi::CallbackInfo& info);
615
+ Napi::Value HasClassVfunc(const Napi::CallbackInfo& info);
616
+ Napi::Value CallClassVfunc(const Napi::CallbackInfo& info);
607
617
  Napi::Value SetConstructCallback(const Napi::CallbackInfo& info);
608
618
 
609
619
  // template.cc
package/src/marshal.cc CHANGED
@@ -424,6 +424,40 @@ static bool JsBinaryData(Napi::Env env, Napi::Value v, const uint8_t** data, siz
424
424
  return false;
425
425
  }
426
426
 
427
+ // The name SpiderMonkey's JS::InformalValueTypeName gives a value — what gjs
428
+ // interpolates into its marshalling errors. Primitives report their typeof;
429
+ // objects report their class name, which for the shapes reachable here is
430
+ // enough at "Object"/"Function".
431
+ static const char* InformalValueTypeName(const Napi::Value& v) {
432
+ if (v.IsNull()) return "null";
433
+ if (v.IsUndefined()) return "undefined";
434
+ if (v.IsBoolean()) return "boolean";
435
+ if (v.IsNumber()) return "number";
436
+ if (v.IsBigInt()) return "bigint";
437
+ if (v.IsString()) return "string";
438
+ if (v.IsSymbol()) return "symbol";
439
+ if (v.IsFunction()) return "Function";
440
+ if (v.IsArray()) return "Array";
441
+ return "Object";
442
+ }
443
+
444
+ // "Gtk.Box" for an introspected GType, g_type_name() otherwise (a registerClass
445
+ // subtype has no GI info of its own; its C name is still exact). The gjs twin is
446
+ // GIWrapperBase::format_name() (refs/gjs/gi/wrapperutils.h).
447
+ static std::string GTypeDisplayName(GType gtype) {
448
+ GIRepository* repo = DupDefaultRepository();
449
+ GIBaseInfo* info = gi_repository_find_by_gtype(repo, gtype);
450
+ std::string name;
451
+ if (info != nullptr) {
452
+ name = std::string(gi_base_info_get_namespace(info)) + "." + gi_base_info_get_name(info);
453
+ gi_base_info_unref(info);
454
+ } else {
455
+ name = g_type_name(gtype);
456
+ }
457
+ g_object_unref(repo);
458
+ return name;
459
+ }
460
+
427
461
  // `ownedStrings` (optional): when a transfer-full string IN/INOUT arg is g_strdup'd
428
462
  // here, the freshly-allocated pointer is appended so the caller can g_free it if the
429
463
  // invoke never adopts it (an arg-marshal error before the call, or a failed invoke).
@@ -434,7 +468,8 @@ bool JsToGIArgument(Napi::Env env, Napi::Value v, GITypeInfo* type, GIArgument*
434
468
  std::vector<gpointer>* ownedStrings,
435
469
  CreatedClosures* closures,
436
470
  CreatedBytes* bytes,
437
- CreatedValues* values) {
471
+ CreatedValues* values,
472
+ const char* argName) {
438
473
  if (v.IsEmpty()) {
439
474
  // Residue of a swallowed napi failure (a fallible Get()/coercion upstream
440
475
  // failed on a terminating env, or a throwing getter left the exception
@@ -472,6 +507,19 @@ bool JsToGIArgument(Napi::Env env, Napi::Value v, GITypeInfo* type, GIArgument*
472
507
  out->v_string = nullptr;
473
508
  return true;
474
509
  }
510
+ if (!v.IsString()) {
511
+ // gjs refuses any non-string here rather than coercing (arg-cache.cpp
512
+ // report_typeof_mismatch) — coercion is how `add_titled(child, 5, …)`
513
+ // silently registered a stack page named "5" while the same call threw
514
+ // on gjs, so @gjsify/gtk-host's refused-layout-write recovery never ran.
515
+ // Thrown as a plain Error, not TypeError: gjs_throw builds JSEXN_ERR.
516
+ std::string msg = std::string("Expected type string for ") +
517
+ (argName != nullptr ? std::string("argument '") + argName + "'"
518
+ : std::string("argument")) +
519
+ " but got type " + InformalValueTypeName(v);
520
+ Napi::Error::New(env, msg).ThrowAsJavaScriptException();
521
+ return false;
522
+ }
475
523
  if (transfer == GI_TRANSFER_EVERYTHING) {
476
524
  // The callee adopts the string and g_free's it. heldString points into a
477
525
  // std::string buffer (NOT g_malloc'd) → an invalid free. Hand over a
@@ -497,6 +545,25 @@ bool JsToGIArgument(Napi::Env env, Napi::Value v, GITypeInfo* type, GIArgument*
497
545
  handled = true;
498
546
  } else if (v.IsExternal()) {
499
547
  GObject* obj = v.As<Napi::External<GObject>>().Data();
548
+ // Runtime type check, as gjs (wrapperutils.h GIWrapperBase::typecheck):
549
+ // handing GTK a pointer of the wrong GType is not an error IT reports
550
+ // as an exception — adw_preferences_page_add() logs one CRITICAL and
551
+ // returns at exit 0, so the caller's recovery path never runs. Only a
552
+ // handle carrying the GObject type tag can be dereferenced safely (a
553
+ // fundamental handle also lands here and keeps the old pass-through).
554
+ if (v.As<Napi::External<GObject>>().CheckTypeTag(&kGObjectHandleTag) &&
555
+ obj != nullptr) {
556
+ GType expected = gi_registered_type_info_get_g_type(
557
+ reinterpret_cast<GIRegisteredTypeInfo*>(iface));
558
+ if (expected != G_TYPE_INVALID && expected != G_TYPE_NONE &&
559
+ !g_type_is_a(G_OBJECT_TYPE(obj), expected)) {
560
+ std::string msg = "Object is of type " + GTypeDisplayName(G_OBJECT_TYPE(obj)) +
561
+ " - cannot convert to " + g_type_name(expected);
562
+ gi_base_info_unref(iface);
563
+ Napi::TypeError::New(env, msg).ThrowAsJavaScriptException();
564
+ return false;
565
+ }
566
+ }
500
567
  // A (transfer full) GObject IN arg CONSUMES a reference: the callee
501
568
  // takes over the one it is handed. Passing the handle's own ref
502
569
  // makes the callee and the JS wrapper share a single ref that BOTH