@gjsify/node-gi 0.43.0 → 0.45.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/calls.cc CHANGED
@@ -432,6 +432,70 @@ void ThrowGError(Napi::Env env, GError* error, const std::string& context) {
432
432
  Napi::Error::New(env, "Calling " + context + ": " + message).ThrowAsJavaScriptException();
433
433
  }
434
434
 
435
+ // Which of a callable's args are NOT consumed from the JS argument list:
436
+ // - the user_data + destroy-notify slots associated with a callback arg
437
+ // (filled from the callback itself);
438
+ // - array-length args of an array IN/OUT arg or of the return value
439
+ // (autofilled from the array's JS length / wired as an OUT slot; flagged
440
+ // in `isLenArg` so the invoke loop can tell the two skip kinds apart).
441
+ // SHARED between the invoke loop and JsInArgCount below so the arity a
442
+ // function REPORTS is by construction the arity the invoke CONSUMES.
443
+ static void ComputeSkippedArgs(GICallableInfo* callable, unsigned int n_args,
444
+ std::vector<bool>* skip, std::vector<bool>* isLenArg) {
445
+ for (unsigned int i = 0; i < n_args; i++) {
446
+ GIArgInfo* ai = gi_callable_info_get_arg(callable, i);
447
+ GITypeInfo* ti = gi_arg_info_get_type_info(ai);
448
+ if (gi_type_info_get_tag(ti) == GI_TYPE_TAG_INTERFACE) {
449
+ GIBaseInfo* iface = gi_type_info_get_interface(ti);
450
+ if (iface != nullptr && GI_IS_CALLBACK_INFO(iface)) {
451
+ int ci = ArgClosureIndex(ai);
452
+ int di = ArgDestroyIndex(ai);
453
+ if (ci >= 0 && static_cast<unsigned int>(ci) < n_args) (*skip)[ci] = true;
454
+ if (di >= 0 && static_cast<unsigned int>(di) < n_args) (*skip)[di] = true;
455
+ }
456
+ if (iface != nullptr) gi_base_info_unref(iface);
457
+ } else if (gi_type_info_get_tag(ti) == GI_TYPE_TAG_ARRAY) {
458
+ unsigned int L = 0;
459
+ if (gi_type_info_get_array_length_index(ti, &L) && L < n_args) {
460
+ (*skip)[L] = true;
461
+ (*isLenArg)[L] = true;
462
+ }
463
+ }
464
+ gi_base_info_unref(ti);
465
+ gi_base_info_unref(ai);
466
+ }
467
+ GITypeInfo* rt = gi_callable_info_get_return_type(callable);
468
+ if (gi_type_info_get_tag(rt) == GI_TYPE_TAG_ARRAY) {
469
+ unsigned int L = 0;
470
+ if (gi_type_info_get_array_length_index(rt, &L) && L < n_args) {
471
+ (*skip)[L] = true;
472
+ (*isLenArg)[L] = true;
473
+ }
474
+ }
475
+ gi_base_info_unref(rt);
476
+ }
477
+
478
+ // The number of JS arguments a callable consumes: IN/INOUT args minus the
479
+ // skipped ones. This is what gjs reports as `Function.length` (m_js_in_argc,
480
+ // refs/gjs/gi/function.cpp Function::init counts exactly the same set), and it
481
+ // is what @gjsify/gtk-host's descriptor conformance reads to hold the widget
482
+ // table against the installed typelib.
483
+ int JsInArgCount(GICallableInfo* callable) {
484
+ unsigned int n_args = gi_callable_info_get_n_args(callable);
485
+ std::vector<bool> skip(n_args, false);
486
+ std::vector<bool> isLenArg(n_args, false);
487
+ ComputeSkippedArgs(callable, n_args, &skip, &isLenArg);
488
+ int count = 0;
489
+ for (unsigned int i = 0; i < n_args; i++) {
490
+ if (skip[i]) continue;
491
+ GIArgInfo* ai = gi_callable_info_get_arg(callable, i);
492
+ GIDirection d = gi_arg_info_get_direction(ai);
493
+ gi_base_info_unref(ai);
494
+ if (d == GI_DIRECTION_IN || d == GI_DIRECTION_INOUT) count++;
495
+ }
496
+ return count;
497
+ }
498
+
435
499
  static Napi::Value InvokeFunctionInfo(Napi::Env env, GIFunctionInfo* func, gpointer instance,
436
500
  Napi::Array args, const std::string& displayName) {
437
501
  GICallableInfo* callable = reinterpret_cast<GICallableInfo*>(func);
@@ -488,56 +552,10 @@ static Napi::Value InvokeFunctionInfo(Napi::Env env, GIFunctionInfo* func, gpoin
488
552
  }
489
553
  }
490
554
 
491
- // Pre-scan: the user_data + destroy-notify slots associated with a callback
492
- // arg are filled from the callback, not consumed from the JS argument list.
555
+ // Pre-scan: which args are NOT JS-consumed (see ComputeSkippedArgs).
493
556
  std::vector<bool> skip(n_args, false);
494
- for (unsigned int i = 0; i < n_args; i++) {
495
- GIArgInfo* ai = gi_callable_info_get_arg(callable, i);
496
- GITypeInfo* ti = gi_arg_info_get_type_info(ai);
497
- if (gi_type_info_get_tag(ti) == GI_TYPE_TAG_INTERFACE) {
498
- GIBaseInfo* iface = gi_type_info_get_interface(ti);
499
- if (iface != nullptr && GI_IS_CALLBACK_INFO(iface)) {
500
- int ci = ArgClosureIndex(ai);
501
- int di = ArgDestroyIndex(ai);
502
- if (ci >= 0 && static_cast<unsigned int>(ci) < n_args) skip[ci] = true;
503
- if (di >= 0 && static_cast<unsigned int>(di) < n_args) skip[di] = true;
504
- }
505
- if (iface != nullptr) gi_base_info_unref(iface);
506
- }
507
- gi_base_info_unref(ti);
508
- gi_base_info_unref(ai);
509
- }
510
-
511
- // Pre-scan: array-length args. An array IN/OUT arg — or the return value — may
512
- // carry a separate introspectable length arg. Mark it skip (not JS-consumed)
513
- // and flag it so its OUT slot is wired below / its IN value is autofilled from
514
- // the array's JS length when the array arg is marshalled. This extends the same
515
- // skip mechanism the callback closure/destroy slots already use.
516
557
  std::vector<bool> isLenArg(n_args, false);
517
- for (unsigned int i = 0; i < n_args; i++) {
518
- GIArgInfo* ai = gi_callable_info_get_arg(callable, i);
519
- GITypeInfo* ti = gi_arg_info_get_type_info(ai);
520
- if (gi_type_info_get_tag(ti) == GI_TYPE_TAG_ARRAY) {
521
- unsigned int L = 0;
522
- if (gi_type_info_get_array_length_index(ti, &L) && L < n_args) {
523
- skip[L] = true;
524
- isLenArg[L] = true;
525
- }
526
- }
527
- gi_base_info_unref(ti);
528
- gi_base_info_unref(ai);
529
- }
530
- {
531
- GITypeInfo* rt = gi_callable_info_get_return_type(callable);
532
- if (gi_type_info_get_tag(rt) == GI_TYPE_TAG_ARRAY) {
533
- unsigned int L = 0;
534
- if (gi_type_info_get_array_length_index(rt, &L) && L < n_args) {
535
- skip[L] = true;
536
- isLenArg[L] = true;
537
- }
538
- }
539
- gi_base_info_unref(rt);
540
- }
558
+ ComputeSkippedArgs(callable, n_args, &skip, &isLenArg);
541
559
 
542
560
  // Caller-allocates OUT storage (fixed C array or boxed struct): blob != nullptr
543
561
  // marks the arg; boxedGType != 0 selects the boxed copy/free path on read-back.
@@ -739,7 +757,8 @@ static Napi::Value InvokeFunctionInfo(Napi::Env env, GIFunctionInfo* func, gpoin
739
757
  Napi::Value v = jsCursor < args.Length() ? args.Get(jsCursor) : env.Undefined();
740
758
  jsCursor++;
741
759
  GITransfer tr = gi_arg_info_get_ownership_transfer(ai);
742
- if (JsToGIArgument(env, v, ti, &slots[i], &held[i], tr, &ownedInStrings)) {
760
+ if (JsToGIArgument(env, v, ti, &slots[i], &held[i], tr, &ownedInStrings, nullptr,
761
+ nullptr, nullptr, gi_base_info_get_name(reinterpret_cast<GIBaseInfo*>(ai)))) {
743
762
  in_args[inPos[i]].v_pointer = &slots[i];
744
763
  out_args[outPos[i]].v_pointer = &slots[i];
745
764
  } else {
@@ -835,7 +854,8 @@ static Napi::Value InvokeFunctionInfo(Napi::Env env, GIFunctionInfo* func, gpoin
835
854
  } else {
836
855
  GITransfer tr = gi_arg_info_get_ownership_transfer(ai);
837
856
  ok = JsToGIArgument(env, v, ti, &in_args[inPos[i]], &held[i], tr, &ownedInStrings,
838
- &createdClosures, &createdBytes, &createdValues);
857
+ &createdClosures, &createdBytes, &createdValues,
858
+ gi_base_info_get_name(reinterpret_cast<GIBaseInfo*>(ai)));
839
859
  }
840
860
  }
841
861
 
@@ -1250,19 +1270,12 @@ Napi::Value HasMethod(const Napi::CallbackInfo& info) {
1250
1270
  // implemented-interface and inherited methods. Keyed by TYPE rather than by an
1251
1271
  // instance handle because it backs the lazy method resolution on the class
1252
1272
  // PROTOTYPE (#1175), which must answer before any instance exists.
1253
- Napi::Value HasClassMethod(const Napi::CallbackInfo& info) {
1254
- Napi::Env env = info.Env();
1255
- if (info.Length() < 3 || !info[0].IsString() || !info[1].IsString() || !info[2].IsString()) {
1256
- Napi::TypeError::New(env,
1257
- "hasClassMethod(namespace: string, typeName: string, methodName: string)")
1258
- .ThrowAsJavaScriptException();
1259
- return env.Null();
1260
- }
1261
- std::string ns = info[0].As<Napi::String>().Utf8Value();
1262
- std::string tn = info[1].As<Napi::String>().Utf8Value();
1263
- std::string method = info[2].As<Napi::String>().Utf8Value();
1264
-
1265
- GIRepository* repo = DupDefaultRepository();
1273
+ // The class-level resolution HasClassMethod and ClassMethodArity share: the
1274
+ // literal-then-snake_case walk over own, implemented-interface and inherited
1275
+ // methods for an object type, or the interface's own methods for an interface
1276
+ // type. Returns a new ref or nullptr.
1277
+ static GIFunctionInfo* ResolveClassLevelMethod(GIRepository* repo, const std::string& ns,
1278
+ const std::string& tn, const std::string& method) {
1266
1279
  GIBaseInfo* base = gi_repository_find_by_name(repo, ns.c_str(), tn.c_str());
1267
1280
  GIFunctionInfo* func = nullptr;
1268
1281
  if (base != nullptr) {
@@ -1284,6 +1297,23 @@ Napi::Value HasClassMethod(const Napi::CallbackInfo& info) {
1284
1297
  }
1285
1298
  gi_base_info_unref(base);
1286
1299
  }
1300
+ return func;
1301
+ }
1302
+
1303
+ Napi::Value HasClassMethod(const Napi::CallbackInfo& info) {
1304
+ Napi::Env env = info.Env();
1305
+ if (info.Length() < 3 || !info[0].IsString() || !info[1].IsString() || !info[2].IsString()) {
1306
+ Napi::TypeError::New(env,
1307
+ "hasClassMethod(namespace: string, typeName: string, methodName: string)")
1308
+ .ThrowAsJavaScriptException();
1309
+ return env.Null();
1310
+ }
1311
+ std::string ns = info[0].As<Napi::String>().Utf8Value();
1312
+ std::string tn = info[1].As<Napi::String>().Utf8Value();
1313
+ std::string method = info[2].As<Napi::String>().Utf8Value();
1314
+
1315
+ GIRepository* repo = DupDefaultRepository();
1316
+ GIFunctionInfo* func = ResolveClassLevelMethod(repo, ns, tn, method);
1287
1317
  g_object_unref(repo);
1288
1318
  if (func == nullptr) return Napi::Boolean::New(env, false);
1289
1319
  const bool invocable = gi_callable_info_is_method(reinterpret_cast<GICallableInfo*>(func));
@@ -1291,6 +1321,89 @@ Napi::Value HasClassMethod(const Napi::CallbackInfo& info) {
1291
1321
  return Napi::Boolean::New(env, invocable);
1292
1322
  }
1293
1323
 
1324
+ // classMethodArity(namespace, typeName, methodName) -> number
1325
+ // The JS-visible IN-arg count of the method — what gjs reports as the
1326
+ // materialized function's `length` — or -1 when the name does not resolve to an
1327
+ // invocable instance method. Resolution is byte-identical to hasClassMethod
1328
+ // (shared ResolveClassLevelMethod), so `arity >= 0` IS the hasClassMethod
1329
+ // answer and the prototype materializer needs a single native call for both.
1330
+ // Arity 0 is a real answer (`Gtk.Widget.show()`), which is why absence is -1.
1331
+ Napi::Value ClassMethodArity(const Napi::CallbackInfo& info) {
1332
+ Napi::Env env = info.Env();
1333
+ if (info.Length() < 3 || !info[0].IsString() || !info[1].IsString() || !info[2].IsString()) {
1334
+ Napi::TypeError::New(
1335
+ env, "classMethodArity(namespace: string, typeName: string, methodName: string)")
1336
+ .ThrowAsJavaScriptException();
1337
+ return env.Null();
1338
+ }
1339
+ std::string ns = info[0].As<Napi::String>().Utf8Value();
1340
+ std::string tn = info[1].As<Napi::String>().Utf8Value();
1341
+ std::string method = info[2].As<Napi::String>().Utf8Value();
1342
+
1343
+ GIRepository* repo = DupDefaultRepository();
1344
+ GIFunctionInfo* func = ResolveClassLevelMethod(repo, ns, tn, method);
1345
+ g_object_unref(repo);
1346
+ if (func == nullptr) return Napi::Number::New(env, -1);
1347
+ const bool invocable = gi_callable_info_is_method(reinterpret_cast<GICallableInfo*>(func));
1348
+ const int arity = invocable ? JsInArgCount(reinterpret_cast<GICallableInfo*>(func)) : -1;
1349
+ gi_base_info_unref(func);
1350
+ return Napi::Number::New(env, arity);
1351
+ }
1352
+
1353
+ // Resolve a CLASS-STRUCT method — one of the GObjectClass functions gjs exposes as a
1354
+ // STATIC on the constructor (`Gtk.Button.list_properties()`, `.find_property()`).
1355
+ // Absent from the object's own GIObjectInfo, which is why CallStaticMethod's three
1356
+ // find_method arms missed it and every such call threw "no static method '…' on
1357
+ // Ns.Class" — including @gjsify/gtk-host's paramSpecs(), on every element-creation
1358
+ // path. `list_properties` is declared on GObject.ObjectClass and NOWHERE else
1359
+ // (Gio.SimpleActionClass has no GIR record at all) and class structs carry no GI
1360
+ // inheritance, so the walk is over the object-info PARENT chain, each level's class
1361
+ // struct in turn — reproducing what gjs reaches through the CONSTRUCTOR PROTOTYPE
1362
+ // CHAIN, where gjs_define_static_methods put GObjectClass's methods on
1363
+ // GObject.Object's constructor and Gtk.Button inherits them. Returns a new ref or
1364
+ // nullptr; same shape as ResolveMethodByName's parent walk above.
1365
+ static GIFunctionInfo* ResolveClassStructMethod(GIObjectInfo* objInfo, const char* name) {
1366
+ GIObjectInfo* walk =
1367
+ reinterpret_cast<GIObjectInfo*>(gi_base_info_ref(reinterpret_cast<GIBaseInfo*>(objInfo)));
1368
+ GIFunctionInfo* found = nullptr;
1369
+ while (walk != nullptr) {
1370
+ GIStructInfo* cs = gi_object_info_get_class_struct(walk);
1371
+ if (cs != nullptr) {
1372
+ if (gi_struct_info_is_gtype_struct(cs)) found = gi_struct_info_find_method(cs, name);
1373
+ gi_base_info_unref(reinterpret_cast<GIBaseInfo*>(cs));
1374
+ }
1375
+ if (found != nullptr) break;
1376
+ GIObjectInfo* parent = gi_object_info_get_parent(walk);
1377
+ gi_base_info_unref(reinterpret_cast<GIBaseInfo*>(walk));
1378
+ walk = parent;
1379
+ }
1380
+ if (walk != nullptr) gi_base_info_unref(reinterpret_cast<GIBaseInfo*>(walk));
1381
+ return found;
1382
+ }
1383
+
1384
+ // The GTypeClass a class-struct method runs ON: the LEAF type's class, never the
1385
+ // declarer's — list_properties() on GtkButton's class must return the button's 44
1386
+ // pspecs, not GObject's zero. gjs resolves the same way, reading the gtype off the
1387
+ // constructor the call went THROUGH (`this`), which is the leaf.
1388
+ //
1389
+ // gjs's GTypeStructInstanceIn uses g_type_class_peek and states its reason: "we use
1390
+ // peek here to simplify reference counting … GType classes are never really freed …
1391
+ // we know that the GType class is referenced at least once when the JS constructor
1392
+ // is initialized". node-gi's class proxy takes no such ref, so peek alone is nullptr
1393
+ // for a type nothing has instantiated yet — hence g_type_class_ref, to REALISE the
1394
+ // class (the same reason ConstructGObject refs it before reading its pspecs). That
1395
+ // ref is KEPT: it establishes gjs's invariant once per GType (bounded — every later
1396
+ // call is served by the peek), while unrefing is the one call that could invalidate
1397
+ // the GParamSpecs list_properties just handed out, which the class owns. Since GLib
1398
+ // 2.84 g_type_class_unref is a documented no-op, so pairing buys nothing anyway.
1399
+ static gpointer ClassStructInstance(GIObjectInfo* objInfo) {
1400
+ GType gtype =
1401
+ gi_registered_type_info_get_g_type(reinterpret_cast<GIRegisteredTypeInfo*>(objInfo));
1402
+ if (gtype == G_TYPE_INVALID) return nullptr;
1403
+ gpointer klass = g_type_class_peek(gtype);
1404
+ return klass != nullptr ? klass : g_type_class_ref(gtype);
1405
+ }
1406
+
1294
1407
  // callStaticMethod(namespace, typeName, methodName, args?) -> unknown
1295
1408
  // Invoke a type-level constructor/static function (e.g. Gio.File.new_for_path,
1296
1409
  // Gtk.Label.new) — a function found ON a type but taking no instance. The Node
@@ -1317,8 +1430,27 @@ Napi::Value CallStaticMethod(const Napi::CallbackInfo& info) {
1317
1430
  return env.Null();
1318
1431
  }
1319
1432
  GIFunctionInfo* func = nullptr;
1433
+ // Non-null ONLY when the class-struct fallback is what resolved `func`: the
1434
+ // GTypeClass to invoke it on. It doubles as the flag that the "is an instance
1435
+ // method" rejection below must not fire — a class-struct method IS is_method,
1436
+ // its instance is the class rather than an object.
1437
+ gpointer classInstance = nullptr;
1320
1438
  if (GI_IS_OBJECT_INFO(typeInfo)) {
1321
- func = gi_object_info_find_method(reinterpret_cast<GIObjectInfo*>(typeInfo), method.c_str());
1439
+ GIObjectInfo* objInfo = reinterpret_cast<GIObjectInfo*>(typeInfo);
1440
+ func = gi_object_info_find_method(objInfo, method.c_str());
1441
+ if (func == nullptr) {
1442
+ func = ResolveClassStructMethod(objInfo, method.c_str());
1443
+ if (func != nullptr) {
1444
+ classInstance = ClassStructInstance(objInfo);
1445
+ if (classInstance == nullptr) {
1446
+ // No GType behind the info, so there is no class to call it on. Drop back
1447
+ // to the plain "no static method" error rather than invoking with a null
1448
+ // instance, which would hand the C function a null GObjectClass*.
1449
+ gi_base_info_unref(reinterpret_cast<GIBaseInfo*>(func));
1450
+ func = nullptr;
1451
+ }
1452
+ }
1453
+ }
1322
1454
  } else if (GI_IS_INTERFACE_INFO(typeInfo)) {
1323
1455
  func = gi_interface_info_find_method(reinterpret_cast<GIInterfaceInfo*>(typeInfo), method.c_str());
1324
1456
  } else if (GI_IS_STRUCT_INFO(typeInfo)) {
@@ -1331,7 +1463,8 @@ Napi::Value CallStaticMethod(const Napi::CallbackInfo& info) {
1331
1463
  .ThrowAsJavaScriptException();
1332
1464
  return env.Null();
1333
1465
  }
1334
- if (gi_callable_info_is_method(reinterpret_cast<GICallableInfo*>(func))) {
1466
+ const bool isMethod = gi_callable_info_is_method(reinterpret_cast<GICallableInfo*>(func));
1467
+ if (isMethod && classInstance == nullptr) {
1335
1468
  gi_base_info_unref(func);
1336
1469
  g_object_unref(repo);
1337
1470
  Napi::TypeError::New(env, ns + "." + tn + "." + method +
@@ -1339,7 +1472,10 @@ Napi::Value CallStaticMethod(const Napi::CallbackInfo& info) {
1339
1472
  .ThrowAsJavaScriptException();
1340
1473
  return env.Null();
1341
1474
  }
1342
- Napi::Value result = InvokeFunctionInfo(env, func, nullptr, args, ns + "." + tn + "." + method);
1475
+ // A class struct may also declare plain functions (no instance); only the
1476
+ // is_method ones take the class.
1477
+ Napi::Value result = InvokeFunctionInfo(env, func, isMethod ? classInstance : nullptr, args,
1478
+ ns + "." + tn + "." + method);
1343
1479
  gi_base_info_unref(func);
1344
1480
  g_object_unref(repo);
1345
1481
  return result;