@gjsify/node-gi 0.63.0 → 0.64.1

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/index.d.ts CHANGED
@@ -56,17 +56,18 @@ export function getEnumValues(namespace: string, name: string): Record<string, n
56
56
  export function getErrorDomain(namespace: string, name: string): { name: string; quark: number } | null;
57
57
 
58
58
  /**
59
- * Register the L1 GLib.Error factory the engine calls when a GI invoke fails, so
60
- * a failed sync call throws a real `GLib.Error` (instanceof, with `.matches()`),
61
- * together with the CLASS it builds — which a `GError`-typed IN argument uses to
62
- * recognise such an error coming back and rebuild a real `GError` from its
63
- * fields. Without the class that direction accepts only a marshalled (boxed)
64
- * error, so a caught one cannot be handed on.
65
- */
66
- export function setErrorBuilder(
67
- builder: (domainName: string, domainQuark: number, code: number, message: string) => Error,
68
- errorClass?: new (domain: unknown, code: number, message: string) => Error,
69
- ): void;
59
+ * Resolve a domain quark back to the enum type registered for it — the reverse of
60
+ * {@link getErrorDomain}, so a thrown error reaches its domain class even when
61
+ * nothing has touched that enum yet. `null` for a quark no typelib claims.
62
+ */
63
+ export function errorDomainInfo(quark: number): { namespace: string; name: string } | null;
64
+
65
+ /**
66
+ * Register the L1 `GLib.Error` factory the engine calls when a GI invoke fails. It
67
+ * receives the failed call's `GError` as a boxed handle, having taken ownership of
68
+ * it, and returns the object to throw — the same shape a marshalled error gets.
69
+ */
70
+ export function setErrorBuilder(builder: (handle: BoxedHandle) => unknown): void;
70
71
 
71
72
  /** Prepend a directory to the GIRepository typelib search path. */
72
73
  export function prependSearchPath(path: string): void;
@@ -599,6 +600,7 @@ declare const native: {
599
600
  getConstantValue: typeof getConstantValue;
600
601
  getEnumValues: typeof getEnumValues;
601
602
  getErrorDomain: typeof getErrorDomain;
603
+ errorDomainInfo: typeof errorDomainInfo;
602
604
  setErrorBuilder: typeof setErrorBuilder;
603
605
  prependSearchPath: typeof prependSearchPath;
604
606
  prependLibraryPath: typeof prependLibraryPath;
package/index.js CHANGED
@@ -29,6 +29,7 @@ export const findInfo = native.findInfo;
29
29
  export const getConstantValue = native.getConstantValue;
30
30
  export const getEnumValues = native.getEnumValues;
31
31
  export const getErrorDomain = native.getErrorDomain;
32
+ export const errorDomainInfo = native.errorDomainInfo;
32
33
  export const setErrorBuilder = native.setErrorBuilder;
33
34
  export const prependSearchPath = native.prependSearchPath;
34
35
  export const prependLibraryPath = native.prependLibraryPath;
@@ -10,16 +10,8 @@
10
10
  // throws; this is the same wrapper, so `try { … } catch` means the same on either runtime
11
11
  // (ADR 0099 conformance row `errors`).
12
12
  //
13
- // Two deliberate differences from the GJS override:
14
- // • the constructor check is a Proxy `construct` trap — a node-gi class has no `_init` to wrap;
15
- // • `set_int` / `set_uint` range-check their value, which in GJS is the argument marshaller's
16
- // job. node-gi's marshaller wraps modulo 2^32 (`set_uint('k', -1)` stores 4294967295), a
17
- // general divergence recorded in status/open-todos/node-gi.md; this keeps the settings
18
- // surface honest without touching the addon, so it also holds in an already-built payload.
19
-
20
- const INT32_MIN = -(2 ** 31);
21
- const INT32_MAX = 2 ** 31 - 1;
22
- const UINT32_MAX = 2 ** 32 - 1;
13
+ // One deliberate difference from the GJS override: the constructor check is a Proxy `construct`
14
+ // trap — a node-gi class has no `_init` to wrap.
23
15
 
24
16
  const CHECKED = [
25
17
  'get_boolean',
@@ -53,12 +45,6 @@ const CHECKED = [
53
45
  'reset',
54
46
  ];
55
47
 
56
- /** The range a setter's JS number must be in; GJS's marshaller throws this exact message. */
57
- const RANGES = {
58
- set_int: [INT32_MIN, INT32_MAX, 'int32'],
59
- set_uint: [0, UINT32_MAX, 'uint32'],
60
- };
61
-
62
48
  /**
63
49
  * Patch `Gio.Settings` / `Gio.SettingsSchema` like GJS does, once, and return the class to hand
64
50
  * out (a Proxy whose `new` refuses a schema that is not installed instead of aborting).
@@ -87,16 +73,8 @@ export function createGioSettings(Gio) {
87
73
  for (const name of CHECKED) {
88
74
  const real = proto[name];
89
75
  if (typeof real !== 'function') continue;
90
- const range = RANGES[name];
91
76
  proto[name] = function (key, ...args) {
92
77
  checkKey(this, key);
93
- if (range !== undefined) {
94
- const [min, max, type] = range;
95
- const value = args[0];
96
- if (typeof value !== 'number' || !Number.isFinite(value) || value < min || value > max) {
97
- throw new Error(`Argument value: value is out of range for ${type}`);
98
- }
99
- }
100
78
  return real.call(this, key, ...args);
101
79
  };
102
80
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gjsify/node-gi",
3
- "version": "0.63.0",
3
+ "version": "0.64.1",
4
4
  "description": "GObject-Introspection runtime for Node.js — load gi:// namespaces (GLib/GObject/Gio/…) with GJS-compatible semantics. Vendored + rewritten from node-gtk under MIT.",
5
5
  "type": "module",
6
6
  "license": "MIT",
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
package/src/addon.cc CHANGED
@@ -88,6 +88,7 @@ static Napi::Object Init(Napi::Env env, Napi::Object exports) {
88
88
  exports.Set("getConstantValue", Napi::Function::New(env, GetConstantValue));
89
89
  exports.Set("getEnumValues", Napi::Function::New(env, GetEnumValues));
90
90
  exports.Set("getErrorDomain", Napi::Function::New(env, GetErrorDomain));
91
+ exports.Set("errorDomainInfo", Napi::Function::New(env, ErrorDomainInfo));
91
92
  exports.Set("setErrorBuilder", Napi::Function::New(env, SetErrorBuilder));
92
93
  exports.Set("prependSearchPath", Napi::Function::New(env, PrependSearchPath));
93
94
  exports.Set("prependLibraryPath", Napi::Function::New(env, PrependLibraryPath));
package/src/cairo.cc CHANGED
@@ -17,7 +17,8 @@
17
17
  // WrapBoxed; cairo_path_t is NOT ref-counted, so a borrowed path is COPIED via
18
18
  // the gjs CairoPath::copy_ptr dummy-context trick instead of ref'd). Slice:
19
19
  // Context (drawing ops, path copy/append, dash, matrix point transforms,
20
- // in-fill/in-stroke, getSource), Surface + ImageSurface (headless pixels),
20
+ // in-fill/in-stroke, getSource, the toy-text API: selectFontFace/setFontSize/
21
+ // showText/textPath/textExtents/fontExtents), Surface + ImageSurface (headless pixels),
21
22
  // SolidPattern, LinearGradient/RadialGradient (color stops), SurfacePattern
22
23
  // (extend/filter), Path handles. Deferred: region, PDF/SVG/PS surfaces,
23
24
  // text/font ops (showText/selectFontFace — canvas2d text rides PangoCairo).
@@ -733,6 +734,70 @@ void InitCairo(Napi::Env env, Napi::Object exports) {
733
734
  CTX_EXTENTS("fillExtents", cairo_fill_extents)
734
735
  CTX_EXTENTS("strokeExtents", cairo_stroke_extents)
735
736
  CTX_EXTENTS("clipExtents", cairo_clip_extents)
737
+ // Toy text API (cairo_select_font_face & co), the GJS-compatible Context text ops.
738
+ // textExtents/fontExtents return the object shape gjs's cairo-context.cpp builds.
739
+ c.Set("selectFontFace", Napi::Function::New(env, +[](const Napi::CallbackInfo& info) -> Napi::Value {
740
+ Napi::Env env = info.Env();
741
+ cairo_t* cr = CtxArg(env, info);
742
+ if (cr == nullptr) return env.Undefined();
743
+ std::string family = info[1].ToString().Utf8Value();
744
+ cairo_select_font_face(cr, family.c_str(),
745
+ static_cast<cairo_font_slant_t>(info[2].ToNumber().Int32Value()),
746
+ static_cast<cairo_font_weight_t>(info[3].ToNumber().Int32Value()));
747
+ CheckStatus(env, cairo_status(cr), "selectFontFace");
748
+ return env.Undefined();
749
+ }));
750
+ c.Set("setFontSize", Napi::Function::New(env, CTX_1D(cairo_set_font_size)));
751
+ c.Set("showText", Napi::Function::New(env, +[](const Napi::CallbackInfo& info) -> Napi::Value {
752
+ Napi::Env env = info.Env();
753
+ cairo_t* cr = CtxArg(env, info);
754
+ if (cr == nullptr) return env.Undefined();
755
+ std::string text = info[1].ToString().Utf8Value();
756
+ cairo_show_text(cr, text.c_str());
757
+ CheckStatus(env, cairo_status(cr), "showText");
758
+ return env.Undefined();
759
+ }));
760
+ c.Set("textPath", Napi::Function::New(env, +[](const Napi::CallbackInfo& info) -> Napi::Value {
761
+ Napi::Env env = info.Env();
762
+ cairo_t* cr = CtxArg(env, info);
763
+ if (cr == nullptr) return env.Undefined();
764
+ std::string text = info[1].ToString().Utf8Value();
765
+ cairo_text_path(cr, text.c_str());
766
+ CheckStatus(env, cairo_status(cr), "textPath");
767
+ return env.Undefined();
768
+ }));
769
+ c.Set("textExtents", Napi::Function::New(env, +[](const Napi::CallbackInfo& info) -> Napi::Value {
770
+ Napi::Env env = info.Env();
771
+ cairo_t* cr = CtxArg(env, info);
772
+ if (cr == nullptr) return env.Undefined();
773
+ std::string text = info[1].ToString().Utf8Value();
774
+ cairo_text_extents_t e;
775
+ cairo_text_extents(cr, text.c_str(), &e);
776
+ if (!CheckStatus(env, cairo_status(cr), "textExtents")) return env.Undefined();
777
+ Napi::Object o = Napi::Object::New(env);
778
+ o.Set("xBearing", Napi::Number::New(env, e.x_bearing));
779
+ o.Set("yBearing", Napi::Number::New(env, e.y_bearing));
780
+ o.Set("width", Napi::Number::New(env, e.width));
781
+ o.Set("height", Napi::Number::New(env, e.height));
782
+ o.Set("xAdvance", Napi::Number::New(env, e.x_advance));
783
+ o.Set("yAdvance", Napi::Number::New(env, e.y_advance));
784
+ return o;
785
+ }));
786
+ c.Set("fontExtents", Napi::Function::New(env, +[](const Napi::CallbackInfo& info) -> Napi::Value {
787
+ Napi::Env env = info.Env();
788
+ cairo_t* cr = CtxArg(env, info);
789
+ if (cr == nullptr) return env.Undefined();
790
+ cairo_font_extents_t e;
791
+ cairo_font_extents(cr, &e);
792
+ if (!CheckStatus(env, cairo_status(cr), "fontExtents")) return env.Undefined();
793
+ Napi::Object o = Napi::Object::New(env);
794
+ o.Set("ascent", Napi::Number::New(env, e.ascent));
795
+ o.Set("descent", Napi::Number::New(env, e.descent));
796
+ o.Set("height", Napi::Number::New(env, e.height));
797
+ o.Set("maxXAdvance", Napi::Number::New(env, e.max_x_advance));
798
+ o.Set("maxYAdvance", Napi::Number::New(env, e.max_y_advance));
799
+ return o;
800
+ }));
736
801
  c.Set("status", Napi::Function::New(env, +[](const Napi::CallbackInfo& info) -> Napi::Value {
737
802
  Napi::Env env = info.Env();
738
803
  cairo_t* cr = CtxArg(env, info);
package/src/calls.cc CHANGED
@@ -380,44 +380,40 @@ bool IsSupportedOutType(GITypeInfo* type, std::string* why) {
380
380
 
381
381
  // ---- GError -> GLib.Error ----
382
382
  //
383
- // A failed GI invoke yields a GError (domain quark + code + message). GJS surfaces
384
- // it as a real `GLib.Error` (an Error subclass carrying `.domain`/`.code`/
385
- // `.message` + a `.matches(domain, code)` method). The GLib.Error CLASS itself is
386
- // owned by L1 (gi.js) so its `matches()` can resolve an error-enum object to its
387
- // domain; the engine just reports the GError's fields to an L1-registered builder
388
- // function (stored in this env's instance data — see NodeGiEnvData) and throws
389
- // what it returns.
383
+ // A failed GI invoke yields a GError (domain quark + code + message), which GJS
384
+ // throws as the error object of that DOMAIN's class. That object is L1's to build
385
+ // (gi.js wrapGError) — only JS can give it the Error brand, the domain class and
386
+ // the position properties — so the engine hands the builder registered in this
387
+ // env's instance data the one thing it needs: the `GError*` itself, as a boxed
388
+ // handle.
390
389
  //
391
- // Read a GError's domain (quark name + numeric quark), code and message, FREE it,
392
- // and throw the JS error the L1 builder produces (instanceof GLib.Error). Falls
393
- // back to a plain JS Error when no builder is registered for this env. `context`
394
- // is the "Ns.method" display name, used only in the fallback message (a real
395
- // GLib.Error carries the bare GError message, matching GJS).
390
+ // Passing the HANDLE and not the fields is the whole point. The fields used to be
391
+ // read out, the GError g_error_free'd here, and a JS Error subclass built from
392
+ // them — so a thrown error and a marshalled one were two different objects with
393
+ // two different `.domain` types. Ownership now transfers to the handle, which
394
+ // frees the GError when the JS error is collected, and both directions wrap the
395
+ // same pointer the same way.
396
+ //
397
+ // Falls back to a plain JS Error when no builder is registered for this env;
398
+ // `context` is the "Ns.method" display name, used only in that fallback message (a
399
+ // real GLib.Error carries the bare GError message, matching GJS).
396
400
  void ThrowGError(Napi::Env env, GError* error, const std::string& context) {
397
- // Read everything out of the GError BEFORE g_error_free.
398
- GQuark domainQuark = error != nullptr ? error->domain : 0;
399
- const char* domainName = domainQuark != 0 ? g_quark_to_string(domainQuark) : nullptr;
400
- std::string domain = domainName != nullptr ? domainName : "";
401
- int code = error != nullptr ? error->code : 0;
402
401
  std::string message =
403
402
  (error != nullptr && error->message != nullptr) ? error->message : "invocation failed";
404
- if (error != nullptr) g_error_free(error);
405
403
 
406
- // Prefer this env's L1 GLib.Error builder (instanceof GLib.Error + matches()).
407
404
  NodeGiEnvData* d = EnvData(env);
408
- if (d != nullptr && d->errorBuilder != nullptr) {
405
+ if (error != nullptr && d != nullptr && d->errorBuilder != nullptr) {
409
406
  napi_value builder = nullptr;
410
407
  if (napi_get_reference_value(env, d->errorBuilder, &builder) == napi_ok &&
411
408
  builder != nullptr) {
412
409
  napi_value undef = nullptr;
413
410
  napi_get_undefined(env, &undef);
414
- napi_value bargs[4] = {nullptr, nullptr, nullptr, nullptr};
415
- napi_create_string_utf8(env, domain.c_str(), NAPI_AUTO_LENGTH, &bargs[0]);
416
- napi_create_uint32(env, static_cast<uint32_t>(domainQuark), &bargs[1]);
417
- napi_create_int32(env, code, &bargs[2]);
418
- napi_create_string_utf8(env, message.c_str(), NAPI_AUTO_LENGTH, &bargs[3]);
411
+ // GI_TRANSFER_EVERYTHING: the invoke's out-GError is ours, and the handle
412
+ // adopts it rather than copying — so nothing frees it here.
413
+ napi_value handle = MakeGErrorHandle(env, error, GI_TRANSFER_EVERYTHING);
414
+ error = nullptr; // adopted by the handle; no path below may free it again
419
415
  napi_value errObj = nullptr;
420
- napi_status st = napi_call_function(env, undef, builder, 4, bargs, &errObj);
416
+ napi_status st = napi_call_function(env, undef, builder, 1, &handle, &errObj);
421
417
  if (st == napi_ok && errObj != nullptr) {
422
418
  napi_throw(env, errObj);
423
419
  return;
@@ -427,6 +423,7 @@ void ThrowGError(Napi::Env env, GError* error, const std::string& context) {
427
423
  if (napi_is_exception_pending(env, &pending) == napi_ok && pending) return;
428
424
  }
429
425
  }
426
+ if (error != nullptr) g_error_free(error);
430
427
  Napi::Error::New(env, "Calling " + context + ": " + message).ThrowAsJavaScriptException();
431
428
  }
432
429
 
@@ -636,7 +633,6 @@ static Napi::Value InvokeFunctionInfo(Napi::Env env, GIFunctionInfo* func, gpoin
636
633
  // belong to the callee (dropped only when the callee never ran).
637
634
  CreatedBytes createdBytes;
638
635
  CreatedValues createdValues;
639
- CreatedErrors createdErrors;
640
636
  bool ok = true;
641
637
  size_t jsCursor = 0;
642
638
  std::vector<LengthWrite> lengthWrites;
@@ -815,7 +811,7 @@ static Napi::Value InvokeFunctionInfo(Napi::Env env, GIFunctionInfo* func, gpoin
815
811
  jsCursor++;
816
812
  GITransfer tr = gi_arg_info_get_ownership_transfer(ai);
817
813
  if (JsToGIArgument(env, v, ti, &slots[i], &held[i], tr, &ownedInStrings, nullptr,
818
- nullptr, nullptr, &createdErrors,
814
+ nullptr, nullptr,
819
815
  gi_base_info_get_name(reinterpret_cast<GIBaseInfo*>(ai)))) {
820
816
  in_args[inPos[i]].v_pointer = &slots[i];
821
817
  out_args[outPos[i]].v_pointer = &slots[i];
@@ -922,7 +918,7 @@ static Napi::Value InvokeFunctionInfo(Napi::Env env, GIFunctionInfo* func, gpoin
922
918
  } else {
923
919
  GITransfer tr = gi_arg_info_get_ownership_transfer(ai);
924
920
  ok = JsToGIArgument(env, v, ti, &in_args[inPos[i]], &held[i], tr, &ownedInStrings,
925
- &createdClosures, &createdBytes, &createdValues, &createdErrors,
921
+ &createdClosures, &createdBytes, &createdValues,
926
922
  gi_base_info_get_name(reinterpret_cast<GIBaseInfo*>(ai)));
927
923
  }
928
924
  }
@@ -944,9 +940,6 @@ static Napi::Value InvokeFunctionInfo(Napi::Env env, GIFunctionInfo* func, gpoin
944
940
  // Likewise every GValue boxed from a plain JS value — never adopted.
945
941
  for (GValue* gv : createdValues.transferNone) NodeGiFreeBoxedGValue(gv);
946
942
  for (GValue* gv : createdValues.transferFull) NodeGiFreeBoxedGValue(gv);
947
- // Likewise every GError built from an L1 GLib.Error — never adopted.
948
- for (GError* e : createdErrors.transferNone) g_error_free(e);
949
- for (GError* e : createdErrors.transferFull) g_error_free(e);
950
943
  // Likewise the transfer-full-instance ref: the callee never consumed it.
951
944
  if (instanceRefTaken) g_object_unref(static_cast<GObject*>(instance));
952
945
  for (gpointer s : ownedInStrings) g_free(s); // never reached the callee (#658)
@@ -1001,11 +994,6 @@ static Napi::Value InvokeFunctionInfo(Napi::Env env, GIFunctionInfo* func, gpoin
1001
994
  // box is ours to free — success AND error paths, mirroring the GBytes rule.
1002
995
  // Transfer-full GValues were adopted by the callee — no release.
1003
996
  for (GValue* gv : createdValues.transferNone) NodeGiFreeBoxedGValue(gv);
1004
- // Transfer-none GError IN-args built from an L1 GLib.Error: the callee copied
1005
- // what it kept (gst_message_new_error copies the GError into the message), so
1006
- // the build is ours to free — success AND error paths, mirroring the two rules
1007
- // above. Transfer-full GErrors were adopted by the callee — no release.
1008
- for (GError* e : createdErrors.transferNone) g_error_free(e);
1009
997
  if (!success) {
1010
998
  for (const InContainer& c : inContainers) FreeInContainer(c);
1011
999
  // A failed invoke did not adopt the transfer-full IN/INOUT strings we g_strdup'd
@@ -1254,6 +1242,57 @@ static GIFunctionInfo* ResolveInstanceMethod(GIRepository* repo, GType gtype, GI
1254
1242
  return func;
1255
1243
  }
1256
1244
 
1245
+ // Resolved instance methods, keyed by the instance's concrete GType and the name
1246
+ // as JS spelled it. The resolution above is the expensive half of a GI call —
1247
+ // dup the default repository, walk the GType parents for the nearest
1248
+ // GIObjectInfo, then search own + interface + inherited method tables twice (the
1249
+ // literal name, then the snake_case alias) — and it repeats IDENTICALLY on every
1250
+ // call, because a typelib's method tables do not change at runtime. MEASURED on
1251
+ // this machine (gjs 1.88.1 / GLib 2.88.3): `set_property` on a registerClass
1252
+ // subclass cost 8224 ns through callMethod against 2815 ns for the same
1253
+ // g_object_set_property through setProperty — 5.4 µs of pure re-resolution per
1254
+ // call, which is most of what a property write on the real-GObject engine spends.
1255
+ //
1256
+ // Only a HIT is cached. A miss must stay uncached: namespaces load lazily, so a
1257
+ // name that resolves to nothing now can resolve after the typelib that declares
1258
+ // it is required — caching the miss would make load order change behaviour.
1259
+ //
1260
+ // The cache OWNS the ref it holds (GIFunctionInfos live as long as the process
1261
+ // anyway, and the entry count is bounded by the methods a program actually
1262
+ // calls); callers get a borrowed pointer and must not unref it. Written only
1263
+ // from the JS thread, like every other entry point here.
1264
+ static std::map<std::pair<GType, std::string>, GIFunctionInfo*> g_instanceMethodCache;
1265
+
1266
+ // The resolution of ResolveInstanceMethod, memoized. Returns a BORROWED pointer
1267
+ // (never unref) or nullptr.
1268
+ static GIFunctionInfo* ResolveInstanceMethodCached(GType gtype, const std::string& method) {
1269
+ const std::pair<GType, std::string> key(gtype, method);
1270
+ auto it = g_instanceMethodCache.find(key);
1271
+ if (it != g_instanceMethodCache.end()) return it->second;
1272
+
1273
+ GIRepository* repo = DupDefaultRepository();
1274
+ GIObjectInfo* objInfo = FindNearestObjectInfo(repo, gtype);
1275
+ GIFunctionInfo* func = nullptr;
1276
+ if (objInfo != nullptr) {
1277
+ func = ResolveInstanceMethod(repo, gtype, objInfo, method);
1278
+ gi_base_info_unref(objInfo);
1279
+ }
1280
+ g_object_unref(repo);
1281
+ if (func != nullptr) g_instanceMethodCache.emplace(key, func);
1282
+ return func;
1283
+ }
1284
+
1285
+ // Whether `gtype` has introspection info at all — the distinction callMethod
1286
+ // reports as "no introspection info for X" before it even looks for a method.
1287
+ static bool HasObjectInfo(GType gtype) {
1288
+ GIRepository* repo = DupDefaultRepository();
1289
+ GIObjectInfo* objInfo = FindNearestObjectInfo(repo, gtype);
1290
+ const bool found = objInfo != nullptr;
1291
+ if (objInfo != nullptr) gi_base_info_unref(objInfo);
1292
+ g_object_unref(repo);
1293
+ return found;
1294
+ }
1295
+
1257
1296
  // callMethod(handle, methodName, args?: unknown[]) -> unknown
1258
1297
  // Resolve an instance method by walking the instance's GIObjectInfo (own +
1259
1298
  // implemented-interface methods at each level, then up the parent chain), then
@@ -1274,20 +1313,14 @@ Napi::Value CallMethod(const Napi::CallbackInfo& info) {
1274
1313
  : Napi::Array::New(env, 0);
1275
1314
 
1276
1315
  GType gtype = G_OBJECT_TYPE(obj);
1277
- GIRepository* repo = DupDefaultRepository();
1316
+ GIFunctionInfo* func = ResolveInstanceMethodCached(gtype, method);
1278
1317
 
1279
- GIObjectInfo* objInfo = FindNearestObjectInfo(repo, gtype);
1280
- if (objInfo == nullptr) {
1281
- g_object_unref(repo);
1318
+ if (func == nullptr && !HasObjectInfo(gtype)) {
1282
1319
  Napi::Error::New(env, std::string("no introspection info for ") + G_OBJECT_TYPE_NAME(obj))
1283
1320
  .ThrowAsJavaScriptException();
1284
1321
  return env.Null();
1285
1322
  }
1286
1323
 
1287
- GIFunctionInfo* func = ResolveInstanceMethod(repo, gtype, objInfo, method);
1288
- gi_base_info_unref(objInfo);
1289
- g_object_unref(repo);
1290
-
1291
1324
  if (func == nullptr) {
1292
1325
  // On a terminating env the method-name read (info[1].Utf8Value) degrades to
1293
1326
  // "" (a swallowed napi failure), so resolution misses and we land here — but
@@ -1302,15 +1335,13 @@ Napi::Value CallMethod(const Napi::CallbackInfo& info) {
1302
1335
  return env.Null();
1303
1336
  }
1304
1337
  if (!gi_callable_info_is_method(reinterpret_cast<GICallableInfo*>(func))) {
1305
- gi_base_info_unref(func);
1306
1338
  Napi::TypeError::New(env, method + " is not an instance method").ThrowAsJavaScriptException();
1307
1339
  return env.Null();
1308
1340
  }
1309
1341
 
1310
- Napi::Value result =
1311
- InvokeFunctionInfo(env, func, obj, args, std::string(G_OBJECT_TYPE_NAME(obj)) + "." + method);
1312
- gi_base_info_unref(func);
1313
- return result;
1342
+ // The cache owns `func`; this is a borrowed pointer.
1343
+ return InvokeFunctionInfo(env, func, obj, args,
1344
+ std::string(G_OBJECT_TYPE_NAME(obj)) + "." + method);
1314
1345
  }
1315
1346
 
1316
1347
  // hasMethod(handle, methodName) -> boolean
@@ -1333,20 +1364,10 @@ Napi::Value HasMethod(const Napi::CallbackInfo& info) {
1333
1364
  if (obj == nullptr) return env.Null();
1334
1365
  std::string method = info[1].As<Napi::String>().Utf8Value();
1335
1366
 
1336
- GType gtype = G_OBJECT_TYPE(obj);
1337
- GIRepository* repo = DupDefaultRepository();
1338
- GIObjectInfo* objInfo = FindNearestObjectInfo(repo, gtype);
1339
- if (objInfo == nullptr) {
1340
- g_object_unref(repo);
1341
- return Napi::Boolean::New(env, false);
1342
- }
1343
- GIFunctionInfo* func = ResolveInstanceMethod(repo, gtype, objInfo, method);
1344
- gi_base_info_unref(objInfo);
1345
- g_object_unref(repo);
1367
+ GIFunctionInfo* func = ResolveInstanceMethodCached(G_OBJECT_TYPE(obj), method);
1346
1368
  if (func == nullptr) return Napi::Boolean::New(env, false);
1347
- const bool invocable = gi_callable_info_is_method(reinterpret_cast<GICallableInfo*>(func));
1348
- gi_base_info_unref(func);
1349
- return Napi::Boolean::New(env, invocable);
1369
+ return Napi::Boolean::New(env,
1370
+ gi_callable_info_is_method(reinterpret_cast<GICallableInfo*>(func)));
1350
1371
  }
1351
1372
 
1352
1373
  // hasClassMethod(namespace, typeName, methodName) -> boolean
package/src/class.cc CHANGED
@@ -907,7 +907,7 @@ static Napi::Value InvokeVFuncPointer(Napi::Env env, GObject* obj, GICallableInf
907
907
  jsCursor++;
908
908
  GITransfer tr = gi_arg_info_get_ownership_transfer(ai);
909
909
  if (JsToGIArgument(env, v, ti, &slots[i], &holds[i], tr, &ownedInStrings, nullptr,
910
- nullptr, nullptr, nullptr,
910
+ nullptr, nullptr,
911
911
  gi_base_info_get_name(reinterpret_cast<GIBaseInfo*>(ai))))
912
912
  giArgs[1 + i].v_pointer = &slots[i];
913
913
  else
@@ -963,7 +963,7 @@ static Napi::Value InvokeVFuncPointer(Napi::Env env, GObject* obj, GICallableInf
963
963
  jsCursor++;
964
964
  GITransfer tr = gi_arg_info_get_ownership_transfer(ai);
965
965
  ok = JsToGIArgument(env, v, ti, &giArgs[1 + i], &holds[i], tr, &ownedInStrings, nullptr,
966
- nullptr, nullptr, nullptr,
966
+ nullptr, nullptr,
967
967
  gi_base_info_get_name(reinterpret_cast<GIBaseInfo*>(ai)));
968
968
  }
969
969