@gtkx/runtime 1.5.0 → 1.6.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.
Files changed (49) hide show
  1. package/dist/descriptors.d.ts +36 -7
  2. package/dist/descriptors.d.ts.map +1 -1
  3. package/dist/descriptors.js +18 -5
  4. package/dist/descriptors.js.map +1 -1
  5. package/dist/element-metadata.d.ts +9 -0
  6. package/dist/element-metadata.d.ts.map +1 -0
  7. package/dist/element-metadata.js +14 -0
  8. package/dist/element-metadata.js.map +1 -0
  9. package/dist/fn.d.ts +8 -2
  10. package/dist/fn.d.ts.map +1 -1
  11. package/dist/fn.js +20 -4
  12. package/dist/fn.js.map +1 -1
  13. package/dist/index.d.ts +2 -2
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +2 -2
  16. package/dist/index.js.map +1 -1
  17. package/dist/internal.d.ts +4 -1
  18. package/dist/internal.d.ts.map +1 -1
  19. package/dist/internal.js +4 -1
  20. package/dist/internal.js.map +1 -1
  21. package/dist/native-value.d.ts.map +1 -1
  22. package/dist/native-value.js +21 -2
  23. package/dist/native-value.js.map +1 -1
  24. package/dist/register-class.d.ts +2 -2
  25. package/dist/register-class.d.ts.map +1 -1
  26. package/dist/register-class.js +38 -1
  27. package/dist/register-class.js.map +1 -1
  28. package/dist/registry.d.ts +26 -3
  29. package/dist/registry.d.ts.map +1 -1
  30. package/dist/registry.js +75 -20
  31. package/dist/registry.js.map +1 -1
  32. package/dist/type.d.ts +10 -1
  33. package/dist/type.d.ts.map +1 -1
  34. package/dist/type.js +14 -1
  35. package/dist/type.js.map +1 -1
  36. package/dist/value.d.ts.map +1 -1
  37. package/dist/value.js +19 -3
  38. package/dist/value.js.map +1 -1
  39. package/package.json +10 -5
  40. package/src/descriptors.ts +55 -9
  41. package/src/element-metadata.ts +24 -0
  42. package/src/fn.ts +33 -5
  43. package/src/index.ts +3 -0
  44. package/src/internal.ts +8 -1
  45. package/src/native-value.ts +30 -2
  46. package/src/register-class.ts +61 -2
  47. package/src/registry.ts +110 -19
  48. package/src/type.ts +25 -0
  49. package/src/value.ts +28 -4
@@ -45,6 +45,7 @@ import {
45
45
  TYPE_NONE,
46
46
  type TypedClass,
47
47
  typeFundamental,
48
+ typeInterfacePrerequisites,
48
49
  typeInterfaces,
49
50
  typeIsA,
50
51
  typeName,
@@ -195,7 +196,7 @@ type SignalSpec = {
195
196
  /** GType of the value an emission returns, defaulting to none. */
196
197
  returnType?: SignalGType;
197
198
  /**
198
- * How the emission combines what its handlers return, limited to the two accumulators GObject
199
+ * How the emission combines what its handlers return, limited to the accumulators GObject
199
200
  * ships: `"first-wins"` stops the emission at the first handler and keeps its result, and
200
201
  * `"true-handled"` runs handlers until one returns `true`, which requires a boolean
201
202
  * `returnType`. Without one, every handler runs and the last result stands.
@@ -316,7 +317,7 @@ type RegisterClassOptions<
316
317
  * in lowercase letters, digits, `-` and `_`, and be new to the type: one an ancestor type or a
317
318
  * listed interface already carries throws, and so does one carrying an uppercase letter, which
318
319
  * GObject would carry under that exact spelling, out of reach of both its dashed spelling and
319
- * its `on<SignalName>` default handler. The two word separators spell the same signal, so a
320
+ * its `on<SignalName>` default handler. Either word separator spells the same signal, so a
320
321
  * signal declared as `data_changed` is connected to and emitted as `data-changed` too.
321
322
  *
322
323
  * Instances connect and emit by name through the same `connect`, `on`, `once`, `off` and
@@ -381,6 +382,7 @@ const PROPERTY_VFUNC_SPECS: PropertyVfuncSpec[] = [
381
382
  ];
382
383
 
383
384
  const PROPERTY_VFUNC_NAMES: Set<string> = new Set(PROPERTY_VFUNC_SPECS.map((spec) => spec.methodName));
385
+ const VFUNC_METHOD_PATTERN = /^vfunc[A-Z0-9]/;
384
386
  const TYPE_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9\-_+]{2,}$/;
385
387
  const UPPER_CASE_PATTERN = /[A-Z]/;
386
388
  const SIGNAL_OVERRIDE_PATTERN = /^on[A-Z]/;
@@ -456,6 +458,7 @@ function registerClass(klass: AnyClass, options: AnyRegisterClassOptions = {}):
456
458
 
457
459
  const name = resolveTypeName(klass, options);
458
460
  const declaredTypes = resolveInterfaceTypes(klass, options.implements ?? []);
461
+ assertInterfacePrerequisites(klass, parentType, declaredTypes);
459
462
  const adoptedTypes = declaredTypes.filter((gtype) => !typeIsA(parentType, gtype));
460
463
  const properties = options.properties ?? {};
461
464
  const signals = resolveDeclaredSignals(klass, options.signals ?? {});
@@ -471,6 +474,7 @@ function registerClass(klass: AnyClass, options: AnyRegisterClassOptions = {}):
471
474
 
472
475
  const claimedMethodNames = new Set(classVfuncs.map((vfunc) => vfunc.methodName));
473
476
  const interfaceBindings = discoverInterfaceBindings(methods, parentType, declaredTypes, claimedMethodNames);
477
+ assertClaimedVfuncs(klass, methods, claimedMethodNames, interfaceBindings);
474
478
 
475
479
  const nativeOptions = withNativeSignals(
476
480
  toNativeOptions(classVfuncs, interfaceBindings, properties, options),
@@ -677,6 +681,61 @@ function wrapVfunc(
677
681
  );
678
682
  }
679
683
 
684
+ const claimedVfuncNames = (
685
+ claimedMethodNames: Set<string>,
686
+ interfaceBindings: InterfaceVfuncBinding[],
687
+ ): Set<string> => {
688
+ const claimed = new Set(claimedMethodNames);
689
+
690
+ for (const binding of interfaceBindings) {
691
+ for (const vfunc of binding.vfuncs) {
692
+ claimed.add(vfunc.methodName);
693
+ }
694
+ }
695
+
696
+ return claimed;
697
+ };
698
+
699
+ const isPrerequisiteMet = (parentType: bigint, declaredTypes: bigint[], prerequisite: bigint): boolean =>
700
+ typeIsA(parentType, prerequisite) || declaredTypes.includes(prerequisite);
701
+
702
+ function assertPrerequisitesFor(klass: AnyClass, parentType: bigint, declaredTypes: bigint[], iface: bigint): void {
703
+ for (const prerequisite of typeInterfacePrerequisites(iface)) {
704
+ if (!isPrerequisiteMet(parentType, declaredTypes, prerequisite)) {
705
+ throw new TypeError(
706
+ `registerClass: ${klass.name} does not meet prerequisite ` +
707
+ `'${typeName(prerequisite) ?? String(prerequisite)}' of interface ` +
708
+ `'${typeName(iface) ?? String(iface)}'`,
709
+ );
710
+ }
711
+ }
712
+ }
713
+
714
+ function assertInterfacePrerequisites(klass: AnyClass, parentType: bigint, declaredTypes: bigint[]): void {
715
+ for (const iface of declaredTypes) {
716
+ assertPrerequisitesFor(klass, parentType, declaredTypes, iface);
717
+ }
718
+ }
719
+
720
+ function assertClaimedVfuncs(
721
+ klass: AnyClass,
722
+ methods: MethodTable,
723
+ claimedMethodNames: Set<string>,
724
+ interfaceBindings: InterfaceVfuncBinding[],
725
+ ): void {
726
+ const claimed = claimedVfuncNames(claimedMethodNames, interfaceBindings);
727
+
728
+ for (const methodName of methods.keys()) {
729
+ if (VFUNC_METHOD_PATTERN.test(methodName) && !claimed.has(methodName)) {
730
+ throw new Error(
731
+ `registerClass: ${klass.name}.${methodName} matches no vtable slot on any ancestor ` +
732
+ "or implemented interface, so the override would never be called; check the name against " +
733
+ "the parent class's virtual methods",
734
+ );
735
+ }
736
+ }
737
+ }
738
+
680
739
  function discoverInterfaceBindings(
681
740
  methods: MethodTable,
682
741
  parentGtype: bigint,
package/src/registry.ts CHANGED
@@ -10,11 +10,12 @@ import {
10
10
  setWrapper,
11
11
  } from "@gtkx/native";
12
12
  import { type AnyClass, walkClassChain } from "@gtkx/utils";
13
- import { copyLayerMembers, type Mixin, type MixinReceiver } from "./mixin.js";
13
+ import { copyLayerMembers, installMixins, type Mixin, type MixinReceiver } from "./mixin.js";
14
14
  import {
15
15
  TYPE_INVALID,
16
16
  TYPE_OBJECT,
17
17
  type TypedClass,
18
+ typeFundamental,
18
19
  typeInterfaces,
19
20
  typeIsA,
20
21
  typeName,
@@ -31,6 +32,22 @@ import {
31
32
  type StaticBase<C, K extends PropertyKey> = Omit<C, K> &
32
33
  (C extends abstract new (...args: infer A) => infer R ? abstract new (...args: A) => R : never);
33
34
 
35
+ /**
36
+ * Static side of class `C` with its construct signature retargeted to produce `I`. A generated
37
+ * wrapper class is declared locally and exported as a registered constant; its instance type is
38
+ * exported as an interface extending the local class so declaration merging and module
39
+ * augmentation keep working, and this type makes constructing the constant produce that interface.
40
+ */
41
+ type WrapperClass<C, I> = Omit<C, "prototype"> & {
42
+ /** Prototype retyped to the exported instance interface, so `instanceof` narrows to it. */
43
+ prototype: I;
44
+ } &
45
+ (C extends new (...args: infer A) => unknown
46
+ ? new (...args: A) => I
47
+ : C extends abstract new (...args: infer A) => unknown
48
+ ? abstract new (...args: A) => I
49
+ : never);
50
+
34
51
  /** One overridable vtable slot: where it sits in the vtable struct and how it is marshalled. */
35
52
  type VfuncDescriptor = {
36
53
  /** GIR name of the type struct holding the slot, without its namespace, such as `WidgetClass`. */
@@ -374,6 +391,31 @@ function registerInterface(cls: AnyClass, type: bigint, mixin: Mixin, layout?: I
374
391
  }
375
392
  }
376
393
 
394
+ /**
395
+ * Copies the members of registered interfaces onto a wrapper class, resolving each interface's
396
+ * mixin through the registry rather than taking the mixin itself, so a class states which
397
+ * interfaces it implements by referencing their classes. The interfaces must already be
398
+ * registered through `registerInterface`; generated code guarantees that by declaring interfaces
399
+ * ahead of the classes that implement them.
400
+ *
401
+ * @param cls Wrapper class adopting the interfaces.
402
+ * @param interfaces Registered interface classes to adopt, in order.
403
+ * @throws If an entry is not a registered interface.
404
+ */
405
+ function installInterfaces(cls: AnyClass, interfaces: AnyClass[]): void {
406
+ const mixins = interfaces.map((iface) => {
407
+ const mixin = getInterfaceMixin(getClassType(iface));
408
+
409
+ if (mixin === undefined) {
410
+ throw new Error(`installInterfaces: ${iface.name} is not a registered interface`);
411
+ }
412
+
413
+ return mixin;
414
+ });
415
+
416
+ installMixins(cls, mixins);
417
+ }
418
+
377
419
  /**
378
420
  * Wraps a native handle in a JS wrapper instance. With no class, resolves and
379
421
  * reuses the wrapper for the handle's runtime GType (composing interface mixins),
@@ -443,6 +485,23 @@ function getWrapperClass(type: bigint): AnyClass {
443
485
  return cls;
444
486
  }
445
487
 
488
+ function getExactWrapperClass(type: bigint, label?: string): AnyClass {
489
+ if (type === TYPE_INVALID) {
490
+ throw new Error(`No GType is registered under '${label ?? String(type)}'`);
491
+ }
492
+
493
+ const cls = classRegistry.get(type);
494
+
495
+ if (cls === undefined) {
496
+ throw new Error(
497
+ `No wrapper class is registered for '${label ?? typeName(type) ?? String(type)}': ` +
498
+ "its module was dropped from the bundle or never imported",
499
+ );
500
+ }
501
+
502
+ return cls;
503
+ }
504
+
446
505
  function resolveWrapperClass(type: bigint): AnyClass | null {
447
506
  let currentType = type;
448
507
 
@@ -491,7 +550,35 @@ function createComposedClass(base: AnyClass, runtimeType: bigint): AnyClass {
491
550
  return applied.size === 0 ? base : cls;
492
551
  }
493
552
 
494
- function resolveComposedClass(runtimeType: bigint): AnyClass | null {
553
+ function isWrappableBase(fallbackType: bigint): boolean {
554
+ return fallbackType === TYPE_INVALID || typeFundamental(fallbackType) === TYPE_OBJECT;
555
+ }
556
+
557
+ function isBetweenWalkAndRuntime(fallbackType: bigint, walkedType: bigint, runtimeType: bigint): boolean {
558
+ return fallbackType !== walkedType && typeIsA(fallbackType, walkedType) && typeIsA(runtimeType, fallbackType);
559
+ }
560
+
561
+ function chooseWrapBase(walked: AnyClass | null, fallback: AnyClass | undefined, runtimeType: bigint): AnyClass | null {
562
+ if (fallback === undefined || !isWrappableBase(getClassType(fallback))) {
563
+ return walked;
564
+ }
565
+
566
+ if (walked === null || isBetweenWalkAndRuntime(getClassType(fallback), getClassType(walked), runtimeType)) {
567
+ return fallback;
568
+ }
569
+
570
+ return walked;
571
+ }
572
+
573
+ function stampComposedClass(composed: AnyClass, runtimeType: bigint): AnyClass {
574
+ setClassType(composed, runtimeType);
575
+ wrapperClasses.add(composed);
576
+ composedClassRegistry.set(runtimeType, composed);
577
+
578
+ return composed;
579
+ }
580
+
581
+ function resolveComposedClass(runtimeType: bigint, fallbackClass?: () => AnyClass): AnyClass | null {
495
582
  const exact = classRegistry.get(runtimeType);
496
583
 
497
584
  if (exact) {
@@ -504,7 +591,8 @@ function resolveComposedClass(runtimeType: bigint): AnyClass | null {
504
591
  return cached;
505
592
  }
506
593
 
507
- const base = resolveWrapperClass(runtimeType);
594
+ const walked = resolveWrapperClass(runtimeType);
595
+ const base = chooseWrapBase(walked, fallbackClass?.(), runtimeType);
508
596
 
509
597
  if (base === null) {
510
598
  return null;
@@ -512,37 +600,37 @@ function resolveComposedClass(runtimeType: bigint): AnyClass | null {
512
600
 
513
601
  const composed = createComposedClass(base, runtimeType);
514
602
 
515
- if (composed === base) {
516
- return base;
603
+ if (composed !== base) {
604
+ return stampComposedClass(composed, runtimeType);
517
605
  }
518
606
 
519
- setClassType(composed, runtimeType);
520
- wrapperClasses.add(composed);
521
- composedClassRegistry.set(runtimeType, composed);
607
+ if (base === walked) {
608
+ return base;
609
+ }
522
610
 
523
- return composed;
611
+ return stampComposedClass(class extends base {}, runtimeType);
524
612
  }
525
613
 
526
- function wrapObject(value: unknown): object | null {
527
- return value == null ? null : getOrCreateWrapper(value as ExternalObject<Handle>);
614
+ function wrapObject(value: unknown, fallbackClass?: () => AnyClass): object | null {
615
+ return value == null ? null : getOrCreateWrapper(value as ExternalObject<Handle>, fallbackClass);
528
616
  }
529
617
 
530
- function wrapCallScopedObject(value: unknown): object | null {
531
- return value == null ? null : wrapperFor(value as ExternalObject<Handle>, bindCallScopedWrapper);
618
+ function wrapCallScopedObject(value: unknown, fallbackClass?: () => AnyClass): object | null {
619
+ return value == null ? null : wrapperFor(value as ExternalObject<Handle>, bindCallScopedWrapper, fallbackClass);
532
620
  }
533
621
 
534
622
  function existingWrapperFor(handle: ExternalObject<Handle>): object | null {
535
623
  return handleMap.has(handle) ? handle : getWrapper(handle);
536
624
  }
537
625
 
538
- function createWrapper(handle: ExternalObject<Handle>): object {
626
+ function createWrapper(handle: ExternalObject<Handle>, fallbackClass?: () => AnyClass): object {
539
627
  const runtimeType: bigint = getType(handle);
540
628
 
541
629
  if (runtimeType === TYPE_INVALID) {
542
630
  throw new Error("Cannot resolve runtime GLib type from handle");
543
631
  }
544
632
 
545
- const cls = resolveComposedClass(runtimeType);
633
+ const cls = resolveComposedClass(runtimeType, fallbackClass);
546
634
 
547
635
  if (!cls) {
548
636
  throw new Error(`Expected registered GLib type, got type ${String(runtimeType)}`);
@@ -551,21 +639,21 @@ function createWrapper(handle: ExternalObject<Handle>): object {
551
639
  return Object.create(cls.prototype) as object;
552
640
  }
553
641
 
554
- function wrapperFor(handle: ExternalObject<Handle>, bind: WrapperBinding): object {
642
+ function wrapperFor(handle: ExternalObject<Handle>, bind: WrapperBinding, fallbackClass?: () => AnyClass): object {
555
643
  const existing = existingWrapperFor(handle);
556
644
 
557
645
  if (existing) {
558
646
  return existing;
559
647
  }
560
648
 
561
- const instance = createWrapper(handle);
649
+ const instance = createWrapper(handle, fallbackClass);
562
650
  bind(handle, instance);
563
651
 
564
652
  return instance;
565
653
  }
566
654
 
567
- function getOrCreateWrapper(handle: ExternalObject<Handle>): object {
568
- return wrapperFor(handle, registerWrapper);
655
+ function getOrCreateWrapper(handle: ExternalObject<Handle>, fallbackClass?: () => AnyClass): object {
656
+ return wrapperFor(handle, registerWrapper, fallbackClass);
569
657
  }
570
658
 
571
659
  function instanceClassName(instance: object): string {
@@ -639,8 +727,10 @@ export {
639
727
  registerWrapperClass,
640
728
  registerWrapperClassResolver,
641
729
  registerInterface,
730
+ installInterfaces,
642
731
  wrapFundamentalHandle,
643
732
  wrapHandle,
733
+ getExactWrapperClass,
644
734
  getWrapperClass,
645
735
  resolveWrapperClass,
646
736
  getHandle,
@@ -657,5 +747,6 @@ export {
657
747
  type InterfaceProperty,
658
748
  type StaticBase,
659
749
  type VfuncDescriptor,
750
+ type WrapperClass,
660
751
  type WrapperClassResolver,
661
752
  };
package/src/type.ts CHANGED
@@ -29,6 +29,14 @@ const gTypeParent = bind(LIB, "g_type_parent", [biguint64T], biguint64T);
29
29
  const gTypeFundamental = bind(LIB, "g_type_fundamental", [biguint64T], biguint64T);
30
30
  const gTypeName = bind(LIB, "g_type_name", [biguint64T], stringT("borrowed"));
31
31
  const gTypeInterfaces = bind(LIB, "g_type_interfaces", [biguint64T, refT(uint32T)], sizedArrayT(biguint64T, 1, "full"));
32
+
33
+ const gTypeInterfacePrerequisites = bind(
34
+ LIB,
35
+ "g_type_interface_prerequisites",
36
+ [biguint64T, refT(uint32T)],
37
+ sizedArrayT(biguint64T, 1, "full"),
38
+ );
39
+
32
40
  /** GType tag for an invalid or uninitialized type. */
33
41
  const TYPE_INVALID = 0n;
34
42
  /** GType tag for the absence of a value (`void`). */
@@ -137,11 +145,26 @@ function typeInterfaces(type: bigint): bigint[] {
137
145
  return gTypeInterfaces(type, nInterfacesRef) as bigint[];
138
146
  }
139
147
 
148
+ function typeInterfacePrerequisites(type: bigint): bigint[] {
149
+ const nPrerequisitesRef = { value: 0 };
150
+
151
+ return gTypeInterfacePrerequisites(type, nPrerequisitesRef) as bigint[];
152
+ }
153
+
140
154
  /** Returns the GType registered under the given name, or `TYPE_INVALID` if none exists. */
141
155
  function typeFromName(name: string): bigint {
142
156
  return gTypeFromName(name) as bigint;
143
157
  }
144
158
 
159
+ /**
160
+ * Keeps the given wrapper classes in a tree-shaken bundle: a generated bootstrap names the classes
161
+ * its namespace cannot operate without as arguments here, which is a reference a bundler retains.
162
+ *
163
+ * @param wrappers Wrapper classes to keep.
164
+ * @returns The same classes, untouched.
165
+ */
166
+ const retainWrapperClasses = (wrappers: readonly unknown[]): readonly unknown[] => wrappers;
167
+
145
168
  function typeFundamental(type: bigint): bigint {
146
169
  return gTypeFundamental(type) as bigint;
147
170
  }
@@ -283,8 +306,10 @@ export {
283
306
  getStrvType,
284
307
  isResolvableDescriptor,
285
308
  isTypedClass,
309
+ retainWrapperClasses,
286
310
  typeIsA,
287
311
  typeParent,
312
+ typeInterfacePrerequisites,
288
313
  typeInterfaces,
289
314
  typeFromName,
290
315
  typeFundamental,
package/src/value.ts CHANGED
@@ -15,6 +15,7 @@ import {
15
15
  fundamentalT,
16
16
  int8T,
17
17
  int32T,
18
+ type ObjectDescriptor,
18
19
  objectT,
19
20
  stringT,
20
21
  uint8T,
@@ -128,6 +129,7 @@ const stringValueType = bindValueType("string", stringT("borrowed"));
128
129
  const enumValueType = bindValueType("enum", int32T);
129
130
  const flagsValueType = bindValueType("flags", uint32T);
130
131
  const objectValueType = bindValueType("object", objectT("borrowed"));
132
+ const objectValueFundamentals: Set<bigint> = new Set([TYPE_OBJECT, TYPE_INTERFACE]);
131
133
  const paramValueType = bindValueType("param", PARAM_T);
132
134
  const variantValueType = bindValueType("variant", VARIANT_T);
133
135
  const pointerValueType = bindValueType("pointer", uint64T);
@@ -718,10 +720,32 @@ function fromValue(value: ExternalObject<Handle>): unknown {
718
720
  return get(value);
719
721
  }
720
722
 
721
- const fromValueForDescriptor = (descriptor: Descriptor, value: ExternalObject<Handle>): unknown =>
722
- descriptor.kind === "array" && descriptor.arrayKind === "gbytearray"
723
- ? byteArrayValueGetterFor(descriptor.isBytes === true)(value)
724
- : fromValue(value);
723
+ const objectValueWithFallback = (
724
+ descriptor: ObjectDescriptor,
725
+ value: ExternalObject<Handle>,
726
+ ): { wrapped: unknown } | null => {
727
+ if (descriptor.fallbackClass === undefined || !objectValueFundamentals.has(typeFundamental(getValueType(value)))) {
728
+ return null;
729
+ }
730
+
731
+ return { wrapped: wrapObject(objectValueType.get(value), descriptor.fallbackClass) };
732
+ };
733
+
734
+ const fromValueForDescriptor = (descriptor: Descriptor, value: ExternalObject<Handle>): unknown => {
735
+ if (descriptor.kind === "array" && descriptor.arrayKind === "gbytearray") {
736
+ return byteArrayValueGetterFor(descriptor.isBytes === true)(value);
737
+ }
738
+
739
+ if (descriptor.kind === "object") {
740
+ const withFallback = objectValueWithFallback(descriptor, value);
741
+
742
+ if (withFallback !== null) {
743
+ return withFallback.wrapped;
744
+ }
745
+ }
746
+
747
+ return fromValue(value);
748
+ };
725
749
 
726
750
  const inferredValueGuard: ValueGuard = (jsValue) => inferValueGType(jsValue, wrapperGType(jsValue)) !== TYPE_INVALID;
727
751