@gtkx/runtime 1.0.0-rc.3 → 1.0.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 (141) hide show
  1. package/README.md +8 -9
  2. package/dist/application-class.d.ts +32 -0
  3. package/dist/application-class.d.ts.map +1 -0
  4. package/dist/application-class.js +68 -0
  5. package/dist/application-class.js.map +1 -0
  6. package/dist/arg.d.ts +9 -3
  7. package/dist/arg.d.ts.map +1 -1
  8. package/dist/arg.js +4 -3
  9. package/dist/arg.js.map +1 -1
  10. package/dist/bind.d.ts +11 -0
  11. package/dist/bind.d.ts.map +1 -1
  12. package/dist/bind.js +11 -0
  13. package/dist/bind.js.map +1 -1
  14. package/dist/callback.d.ts +2 -1
  15. package/dist/callback.d.ts.map +1 -1
  16. package/dist/callback.js +119 -21
  17. package/dist/callback.js.map +1 -1
  18. package/dist/closure.d.ts +26 -0
  19. package/dist/closure.d.ts.map +1 -0
  20. package/dist/closure.js +99 -0
  21. package/dist/closure.js.map +1 -0
  22. package/dist/descriptors.d.ts +105 -6
  23. package/dist/descriptors.d.ts.map +1 -1
  24. package/dist/descriptors.js +56 -12
  25. package/dist/descriptors.js.map +1 -1
  26. package/dist/error.d.ts +7 -0
  27. package/dist/error.d.ts.map +1 -1
  28. package/dist/error.js +1 -1
  29. package/dist/error.js.map +1 -1
  30. package/dist/fn.d.ts +24 -3
  31. package/dist/fn.d.ts.map +1 -1
  32. package/dist/fn.js +70 -33
  33. package/dist/fn.js.map +1 -1
  34. package/dist/index.d.ts +29 -7
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +26 -4
  37. package/dist/index.js.map +1 -1
  38. package/dist/internal.d.ts +1 -0
  39. package/dist/internal.d.ts.map +1 -1
  40. package/dist/internal.js +1 -0
  41. package/dist/internal.js.map +1 -1
  42. package/dist/library.d.ts +2 -1
  43. package/dist/library.d.ts.map +1 -1
  44. package/dist/library.js +2 -1
  45. package/dist/library.js.map +1 -1
  46. package/dist/lifecycle.d.ts +52 -10
  47. package/dist/lifecycle.d.ts.map +1 -1
  48. package/dist/lifecycle.js +62 -18
  49. package/dist/lifecycle.js.map +1 -1
  50. package/dist/listeners.d.ts +6 -0
  51. package/dist/listeners.d.ts.map +1 -1
  52. package/dist/listeners.js +3 -1
  53. package/dist/listeners.js.map +1 -1
  54. package/dist/mixin.d.ts +16 -2
  55. package/dist/mixin.d.ts.map +1 -1
  56. package/dist/mixin.js +18 -6
  57. package/dist/mixin.js.map +1 -1
  58. package/dist/native-value.d.ts +8 -0
  59. package/dist/native-value.d.ts.map +1 -1
  60. package/dist/native-value.js +12 -3
  61. package/dist/native-value.js.map +1 -1
  62. package/dist/object.d.ts +34 -6
  63. package/dist/object.d.ts.map +1 -1
  64. package/dist/object.js +89 -25
  65. package/dist/object.js.map +1 -1
  66. package/dist/param-spec.d.ts +11 -0
  67. package/dist/param-spec.d.ts.map +1 -0
  68. package/dist/param-spec.js +149 -0
  69. package/dist/param-spec.js.map +1 -0
  70. package/dist/promisify.d.ts +3 -5
  71. package/dist/promisify.d.ts.map +1 -1
  72. package/dist/promisify.js +4 -3
  73. package/dist/promisify.js.map +1 -1
  74. package/dist/properties.d.ts +39 -7
  75. package/dist/properties.d.ts.map +1 -1
  76. package/dist/properties.js +305 -42
  77. package/dist/properties.js.map +1 -1
  78. package/dist/register-class.d.ts +119 -14
  79. package/dist/register-class.d.ts.map +1 -1
  80. package/dist/register-class.js +109 -80
  81. package/dist/register-class.js.map +1 -1
  82. package/dist/registry.d.ts +81 -17
  83. package/dist/registry.d.ts.map +1 -1
  84. package/dist/registry.js +72 -26
  85. package/dist/registry.js.map +1 -1
  86. package/dist/signal.d.ts +22 -3
  87. package/dist/signal.d.ts.map +1 -1
  88. package/dist/signal.js +64 -8
  89. package/dist/signal.js.map +1 -1
  90. package/dist/t.d.ts +44 -0
  91. package/dist/t.d.ts.map +1 -1
  92. package/dist/t.js.map +1 -1
  93. package/dist/type.d.ts +2 -2
  94. package/dist/type.d.ts.map +1 -1
  95. package/dist/type.js +2 -0
  96. package/dist/type.js.map +1 -1
  97. package/dist/value.d.ts +10 -10
  98. package/dist/value.d.ts.map +1 -1
  99. package/dist/value.js +44 -22
  100. package/dist/value.js.map +1 -1
  101. package/dist/vfunc-call.d.ts +37 -0
  102. package/dist/vfunc-call.d.ts.map +1 -0
  103. package/dist/vfunc-call.js +168 -0
  104. package/dist/vfunc-call.js.map +1 -0
  105. package/dist/vfunc-seeds.d.ts +12 -0
  106. package/dist/vfunc-seeds.d.ts.map +1 -0
  107. package/dist/vfunc-seeds.js +19 -0
  108. package/dist/vfunc-seeds.js.map +1 -0
  109. package/dist/vfunc.d.ts +8 -0
  110. package/dist/vfunc.d.ts.map +1 -0
  111. package/dist/vfunc.js +23 -0
  112. package/dist/vfunc.js.map +1 -0
  113. package/package.json +4 -4
  114. package/src/application-class.ts +116 -0
  115. package/src/arg.ts +11 -5
  116. package/src/bind.ts +11 -0
  117. package/src/callback.ts +163 -23
  118. package/src/closure.ts +136 -0
  119. package/src/descriptors.ts +137 -41
  120. package/src/error.ts +8 -1
  121. package/src/fn.ts +104 -39
  122. package/src/index.ts +36 -7
  123. package/src/internal.ts +1 -0
  124. package/src/library.ts +2 -1
  125. package/src/lifecycle.ts +99 -22
  126. package/src/listeners.ts +9 -1
  127. package/src/mixin.ts +27 -8
  128. package/src/native-value.ts +13 -3
  129. package/src/object.ts +128 -34
  130. package/src/param-spec.ts +235 -0
  131. package/src/promisify.ts +8 -8
  132. package/src/properties.ts +459 -45
  133. package/src/register-class.ts +329 -114
  134. package/src/registry.ts +150 -38
  135. package/src/signal.ts +101 -17
  136. package/src/t.ts +44 -0
  137. package/src/type.ts +3 -2
  138. package/src/value.ts +57 -19
  139. package/src/vfunc-call.ts +243 -0
  140. package/src/vfunc-seeds.ts +36 -0
  141. package/src/vfunc.ts +31 -0
@@ -1,59 +1,217 @@
1
1
  import type { Descriptor } from "@gtkx/native";
2
2
  import {
3
3
  registerClass as nativeRegisterClass,
4
+ type RegisterClassInterface as NativeRegisterClassInterface,
4
5
  type RegisterClassOptions as NativeRegisterClassOptions,
5
6
  type RegisterClassVfunc as NativeRegisterClassVfunc,
6
7
  } from "@gtkx/native";
7
8
  import { type AnyClass, getParentClass, walkClassChain } from "@gtkx/utils";
8
9
  import { wrapCallback } from "./callback.js";
10
+ import { insertMixinLayer } from "./mixin.js";
9
11
  import {
10
- buildAccessors,
12
+ buildPropertyDispatch,
13
+ GET_PROPERTY_VFUNC,
11
14
  makeGetProperty,
12
15
  makeSetProperty,
13
- type PropertyAccessor,
16
+ type PropertyDispatch,
14
17
  type PropertySpec,
18
+ SET_PROPERTY_VFUNC,
15
19
  toNativeProperties,
16
20
  } from "./properties.js";
17
21
  import {
18
22
  getClassType,
19
- getInterfaceVfuncRegistry,
20
- getVfuncRegistry,
23
+ getInterfaceMixin,
24
+ markDerivedClass,
21
25
  registerClassType,
22
26
  type VfuncDescriptor,
23
27
  } from "./registry.js";
24
- import { TYPE_INVALID, typeInterfaces } from "./type.js";
28
+ import { TYPE_INTERFACE, TYPE_INVALID, typeFundamental, typeInterfaces, typeIsA } from "./type.js";
29
+ import { findClassVfuncDescriptor, findInterfaceVfuncDescriptor } from "./vfunc.js";
25
30
 
26
- type RegisterClassOptions = {
31
+ /**
32
+ * A generated interface value, such as `Gio.ListModel`, in the form {@link registerClass} takes it.
33
+ * `__impl__` exists only in the type system: it carries the interface's `Impl` type, of which a class
34
+ * has to match every member it declares itself, which rejects a class value and a member whose
35
+ * signature does not fit the slot it fills. Leaving a slot to the interface is not rejected,
36
+ * because `registerClass` does not reject it either: every member of an `Impl` type is optional,
37
+ * and the `Partial` holds that open whatever the type declares. The `object` beside it keeps
38
+ * TypeScript from also demanding one member in common. An interface that introspection describes
39
+ * no vtable for carries `unknown`, since it has no slot a class could fill.
40
+ */
41
+ type Interface<TImpl> = AnyClass & {
42
+ /** Type-level slot holding the interface's `Impl` type; no value ever carries it. */
43
+ __impl__: (impl: Partial<TImpl> & object) => void;
44
+ };
45
+
46
+ /**
47
+ * One key of `RegisterClassOptions.properties` with every underscore turned into the dash a canonical
48
+ * GObject property name separates its words with.
49
+ */
50
+ type Dashed<TName extends string> = TName extends `${infer THead}_${infer TTail}`
51
+ ? Dashed<`${THead}-${TTail}`>
52
+ : TName;
53
+
54
+ /**
55
+ * One dashed key in the camelCase spelling the accessors {@link registerClass} installs carry, which is
56
+ * the spelling the hooks that address a property by name take.
57
+ */
58
+ type Camelized<TName extends string> = TName extends `${infer THead}-${infer TTail}`
59
+ ? `${THead}${Capitalize<Camelized<TTail>>}`
60
+ : TName;
61
+
62
+ /**
63
+ * The names a registered class carries in its property map: every key of
64
+ * `RegisterClassOptions.properties` in camelCase, whichever of the three spellings it was written in.
65
+ * A `properties` object given a type of its own rather than left to inference has `string` for its key
66
+ * type and names nothing, because a name only known as `string` addresses no member in particular.
67
+ */
68
+ type InstalledNames<TProperties> = string extends keyof TProperties
69
+ ? never
70
+ : Camelized<Dashed<keyof TProperties & string>>;
71
+
72
+ /**
73
+ * An instance of a registered class: everything the class itself declares, plus the property map the
74
+ * hooks that address a property by name, such as `useProperty` from `@gtkx/react`, read the installed
75
+ * names off. Each name is typed with the value type the class declares for the member of that name,
76
+ * so a property the class does not `declare` contributes nothing.
77
+ */
78
+ type RegisteredInstance<TInstance, TProperties> = TInstance & {
79
+ /** Type-level map from installed property name to value type; no value ever carries it. */
80
+ __properties__: Pick<TInstance, InstalledNames<TProperties> & keyof TInstance>;
81
+ };
82
+
83
+ /**
84
+ * The construct signature and prototype a registered class carries, both giving
85
+ * {@link RegisteredInstance}. The signature stays abstract for as long as the class itself is, so
86
+ * registering an abstract base leaves it as impossible to construct as it was.
87
+ */
88
+ type RegisteredConstructor<TClass, TArgs extends unknown[], TInstance> = {
89
+ /** Object the class's instances inherit from. */
90
+ prototype: TInstance;
91
+ } & (TClass extends new (...args: never) => unknown
92
+ ? new (...args: TArgs) => TInstance
93
+ : abstract new (...args: TArgs) => TInstance);
94
+
95
+ /**
96
+ * The class {@link registerClass} hands back: the same class, with the same statics, whose instances
97
+ * carry the properties `RegisterClassOptions.properties` installed. Binding the call to a name, rather
98
+ * than discarding it, is what carries those names into the type system.
99
+ */
100
+ type RegisteredClass<TClass extends AnyClass, TProperties> =
101
+ TClass extends abstract new (...args: infer TArgs) => infer TInstance
102
+ ? Omit<TClass, "prototype"> & RegisteredConstructor<TClass, TArgs, RegisteredInstance<TInstance, TProperties>>
103
+ : never;
104
+
105
+ /** What {@link registerClass} adds to the new GType beyond the vtable slots it discovers on the class. */
106
+ type RegisterClassOptions<TInstance extends object, TProperties extends Record<string, PropertySpec>> = {
107
+ /** Name to register the new GType under, defaulting to the class's own name. */
27
108
  typeName?: string;
28
109
  /**
29
- * Properties to install on the new type, keyed by canonical property name. Each value is the
30
- * `GObject.ParamSpec` describing it, for example `GObject.paramSpecString(...)`. Accessors are
31
- * generated on the prototype for the dashed, underscored and camelCased spellings of the name
32
- * unless the class already defines one, and writing through them emits `notify`.
110
+ * Interfaces the new type implements on top of the ones it inherits, given as the interface values
111
+ * themselves, such as `Gio.ListModel`. Their vtable slots are filled from the `vfunc`-prefixed methods on
112
+ * the class's prototype chain, each of which has to match the interface's `Impl` type, such as
113
+ * `Gio.ListModelImpl`.
114
+ */
115
+ implements?: Interface<TInstance>[];
116
+ /**
117
+ * Properties to install on the new type, keyed by the name JavaScript addresses each one by and
118
+ * valued with the `GObject.ParamSpec` describing it. A key is read in camelCase however it is
119
+ * written, so `dewPoint`, `dew_point` and `dew-point` all name the same member, and the ParamSpec
120
+ * has to carry the canonical spelling of that name, `dew-point`, or registration throws: the
121
+ * ParamSpec's name is the one GObject emits `notify` with, and a name the key does not spell
122
+ * reaches nothing that listens for it. Every property gains prototype accessors, one for the key
123
+ * as written, one for it with dashes turned into underscores and one for it in camelCase, each
124
+ * unless the class already defines that name. They serve the value from storage of their own on
125
+ * the instance, which is also what the type's `get_property` and `set_property` slots read and
126
+ * write, so a value set from JavaScript, from `g_object_set_property` and at construction all
127
+ * land in the same place.
128
+ *
129
+ * A write the ParamSpec would refuse throws rather than reaching GObject, which reports such a
130
+ * write as a GLib critical and drops it: a `TypeError` for a read-only or construct-only
131
+ * property and for a value of a type the property cannot hold, and a `RangeError` for a value
132
+ * the ParamSpec rejects. The same two checks run over a value handed to the constructor, where
133
+ * a construct-only property is the one that is writable. An accepted write emits one `notify`,
134
+ * which a `freeze_notify` batch collects; a write of the value the property already holds is
135
+ * dropped and emits none.
136
+ *
137
+ * A class that defines the camelCase member itself owns the property: its own accessor decides
138
+ * what a write means, the other two spellings forward to it, and the type's property slots read
139
+ * and write it rather than the generated storage.
33
140
  */
34
- properties?: Record<string, PropertySpec>;
141
+ properties?: TProperties;
35
142
  };
36
143
 
144
+ /** {@link RegisterClassOptions} with the widest instance and property types {@link registerClass} accepts. */
145
+ type AnyRegisterClassOptions = RegisterClassOptions<object, Record<string, PropertySpec>>;
37
146
  type VfuncFn = NativeRegisterClassVfunc["fn"];
38
- type DiscoveredVfunc<K extends "class" | "interface"> = VfuncDescriptor<K> & { methodName: string; fn: VfuncFn };
39
- type DiscoveredClassVfunc = DiscoveredVfunc<"class">;
40
- type DiscoveredInterfaceVfunc = DiscoveredVfunc<"interface">;
41
- type InterfaceVfuncBinding = { gtype: bigint; vtableSize: number; vfuncs: DiscoveredInterfaceVfunc[] };
147
+ type DiscoveredVfunc = VfuncDescriptor & { methodName: string; fn: VfuncFn };
148
+ type MethodTable = Map<string, VfuncFn>;
149
+ type InstanceMembers = { methods: MethodTable; names: Set<string>; inheritedNames: Set<string> };
150
+
151
+ type InterfaceVfuncBinding = {
152
+ gtype: bigint;
153
+ vfuncs: DiscoveredVfunc[];
154
+ };
155
+
156
+ type PropertyVfuncSource = {
157
+ klass: AnyClass;
158
+ methods: MethodTable;
159
+ dispatch: PropertyDispatch;
160
+ adoptedTypes: bigint[];
161
+ };
162
+
163
+ type PropertyVfuncSpec = {
164
+ methodName: string;
165
+ isValueOut: boolean;
166
+ makeDispatch: (dispatch: PropertyDispatch) => VfuncFn;
167
+ };
42
168
 
43
- const UNSUPPORTED_CONSTRUCT_VFUNCS: Set<string> = new Set(["constructed", "setProperty", "getProperty"]);
44
169
  const VALUE_ARG_INDEX = 2;
45
170
 
171
+ const PROPERTY_VFUNC_SPECS: PropertyVfuncSpec[] = [
172
+ { methodName: GET_PROPERTY_VFUNC, isValueOut: true, makeDispatch: makeGetProperty },
173
+ { methodName: SET_PROPERTY_VFUNC, isValueOut: false, makeDispatch: makeSetProperty },
174
+ ];
175
+
176
+ const PROPERTY_VFUNC_NAMES: Set<string> = new Set(PROPERTY_VFUNC_SPECS.map((spec) => spec.methodName));
177
+
46
178
  /**
47
- * Registers a subclass of a wrapper class as a new GType, wiring up any class and
48
- * inherited-interface virtual functions it overrides. Throws if the class does not
49
- * extend a registered wrapper class, has no derivable type name, or overrides an
50
- * unsupported construct-time vtable slot.
179
+ * Registers a subclass of a wrapper class as a new GType, wiring up any class and interface
180
+ * virtual functions it overrides, both for the interfaces it inherits and for the ones
181
+ * `RegisterClassOptions.implements` names.
182
+ *
183
+ * Throws when the class does not extend a registered wrapper class, when it has no derivable type
184
+ * name, when an entry in `RegisterClassOptions.implements` is not a registered interface, when a
185
+ * listed interface has a prerequisite that neither the parent type nor another listed interface meets,
186
+ * and when an entry in `RegisterClassOptions.properties` names its `GObject.ParamSpec` something other
187
+ * than the canonical spelling of the key it sits under.
188
+ *
189
+ * A slot is filled from the `vfunc`-prefixed methods on the class's prototype chain, up to but not
190
+ * including the registered ancestor the class extends, so a method an intermediate base class
191
+ * declares fills a slot the same way one the class itself declares does. A slot nothing on that
192
+ * chain fills is left untouched.
193
+ *
194
+ * Declare every slot as a method: a class field holding a function, such as `vfuncGetNItems = () => 1`,
195
+ * is assigned to each instance after registration and never reaches the vtable.
196
+ *
197
+ * An override of `vfuncConstructed` runs from inside the base constructor, before JavaScript
198
+ * installs the subclass's field initializers and runs its constructor body, so a field still
199
+ * reads `undefined` there and reading a `#private` field throws. Declare state the override
200
+ * touches without an initializer, and assign private state from the constructor body after
201
+ * `super()`. An instance a native caller creates, through `GObject.newv` or `Gtk.Builder`,
202
+ * never runs the subclass constructor at all, so its declared fields stay uninitialized for
203
+ * the object's whole life.
51
204
  *
52
205
  * @param klass The subclass to register.
53
- * @param options Registration options, such as an explicit type name.
54
- * @returns The same class, now registered.
206
+ * @param options What the new GType gains beyond the vtable slots the class overrides.
207
+ * @returns The same class, now registered, with every name in `options.properties` in its property map.
55
208
  */
56
- function registerClass<T extends AnyClass>(klass: T, options: RegisterClassOptions = {}): T {
209
+ function registerClass<
210
+ T extends AnyClass,
211
+ TProperties extends Record<string, PropertySpec> = Record<never, PropertySpec>,
212
+ >(klass: T, options?: RegisterClassOptions<T["prototype"], TProperties>): RegisteredClass<T, TProperties>;
213
+
214
+ function registerClass(klass: AnyClass, options: AnyRegisterClassOptions = {}): AnyClass {
57
215
  const parentType = resolveParentType(klass);
58
216
 
59
217
  if (parentType === TYPE_INVALID) {
@@ -66,18 +224,56 @@ function registerClass<T extends AnyClass>(klass: T, options: RegisterClassOptio
66
224
  throw new Error("registerClass: cannot derive a GType name (anonymous class with no typeName option)");
67
225
  }
68
226
 
227
+ const declaredTypes = resolveInterfaceTypes(klass, options.implements ?? []);
228
+ const adoptedTypes = declaredTypes.filter((gtype) => !typeIsA(parentType, gtype));
69
229
  const properties = options.properties ?? {};
70
- const accessors = buildAccessors(klass, properties);
71
- const classVfuncs = [...discoverClassVfuncs(klass), ...propertyVfuncs(klass, accessors)];
230
+ const { methods, inheritedNames } = collectInstanceMembers(klass);
231
+ const dispatch = buildPropertyDispatch({ klass, properties, adoptedTypes });
232
+
233
+ const classVfuncs = [
234
+ ...discoverClassVfuncs(klass, methods),
235
+ ...propertyVfuncs({ klass, methods, dispatch, adoptedTypes }),
236
+ ];
237
+
72
238
  const claimedMethodNames = new Set(classVfuncs.map((vfunc) => vfunc.methodName));
73
- const interfaceBindings = discoverInheritedInterfaceVfuncs(klass, parentType, claimedMethodNames);
239
+ const interfaceBindings = discoverInterfaceBindings(methods, parentType, declaredTypes, claimedMethodNames);
74
240
  const nativeOptions = toNativeOptions(classVfuncs, interfaceBindings, properties);
75
241
  const newType: bigint = nativeRegisterClass(name, parentType, nativeOptions);
76
242
  registerClassType(klass, newType);
243
+ markDerivedClass(klass);
244
+ applyInterfaceMixins(klass, adoptedTypes, inheritedNames);
77
245
 
78
246
  return klass;
79
247
  }
80
248
 
249
+ function resolveInterfaceType(klass: AnyClass, entry: AnyClass): bigint {
250
+ const gtype = getClassType(entry);
251
+
252
+ if (typeFundamental(gtype) !== TYPE_INTERFACE) {
253
+ const label = typeof entry === "function" ? entry.name : String(entry);
254
+
255
+ throw new TypeError(
256
+ `registerClass: ${klass.name} lists '${label}' in implements, which is not a registered interface`,
257
+ );
258
+ }
259
+
260
+ return gtype;
261
+ }
262
+
263
+ function resolveInterfaceTypes(klass: AnyClass, entries: AnyClass[]): bigint[] {
264
+ return [...new Set(entries.map((entry) => resolveInterfaceType(klass, entry)))];
265
+ }
266
+
267
+ function applyInterfaceMixins(klass: AnyClass, adoptedTypes: bigint[], inheritedNames: Set<string>): void {
268
+ for (const gtype of adoptedTypes) {
269
+ const mixin = getInterfaceMixin(gtype);
270
+
271
+ if (mixin !== undefined) {
272
+ insertMixinLayer(klass, mixin, inheritedNames);
273
+ }
274
+ }
275
+ }
276
+
81
277
  function resolveParentType(klass: AnyClass): bigint {
82
278
  return (
83
279
  walkClassChain(getParentClass(klass), (cls) => {
@@ -88,28 +284,57 @@ function resolveParentType(klass: AnyClass): bigint {
88
284
  );
89
285
  }
90
286
 
91
- function ownInstanceMethodNames(klass: AnyClass): string[] {
287
+ function addOwnMember(members: InstanceMembers, proto: object, name: string, isInherited: boolean): void {
288
+ if (name === "constructor") {
289
+ return;
290
+ }
291
+
292
+ if (isInherited) {
293
+ members.inheritedNames.add(name);
294
+ }
295
+
296
+ if (members.names.has(name)) {
297
+ return;
298
+ }
299
+
300
+ members.names.add(name);
301
+ const descriptor = Object.getOwnPropertyDescriptor(proto, name);
302
+
303
+ if (typeof descriptor?.value === "function") {
304
+ members.methods.set(name, descriptor.value as VfuncFn);
305
+ }
306
+ }
307
+
308
+ function addOwnMembers(members: InstanceMembers, klass: AnyClass, isInherited: boolean): void {
92
309
  const proto = (klass as { prototype?: object }).prototype;
93
310
 
94
- if (!proto) {
95
- return [];
311
+ if (proto === undefined) {
312
+ return;
96
313
  }
97
314
 
98
- return Object.getOwnPropertyNames(proto).filter((name) => {
99
- if (name === "constructor") {
100
- return false;
101
- }
315
+ for (const name of Object.getOwnPropertyNames(proto)) {
316
+ addOwnMember(members, proto, name, isInherited);
317
+ }
318
+ }
319
+
320
+ function collectInstanceMembers(klass: AnyClass): InstanceMembers {
321
+ const members: InstanceMembers = { methods: new Map(), names: new Set(), inheritedNames: new Set() };
322
+ let cls: AnyClass | null = klass;
323
+
324
+ while (cls !== null && getClassType(cls) === TYPE_INVALID) {
325
+ addOwnMembers(members, cls, cls !== klass);
326
+ cls = getParentClass(cls);
327
+ }
102
328
 
103
- return typeof Object.getOwnPropertyDescriptor(proto, name)?.value === "function";
104
- });
329
+ return members;
105
330
  }
106
331
 
107
- function buildDiscoveredVfunc<K extends "class" | "interface">(
108
- proto: Record<string, VfuncFn>,
332
+ function buildDiscoveredVfunc(
333
+ fn: VfuncFn,
109
334
  methodName: string,
110
- resolveDescriptor: (methodName: string) => VfuncDescriptor<K> | undefined,
335
+ resolveDescriptor: (methodName: string) => VfuncDescriptor | undefined,
111
336
  skip: Set<string> | undefined,
112
- ): DiscoveredVfunc<K> | undefined {
337
+ ): DiscoveredVfunc | undefined {
113
338
  if (skip?.has(methodName)) {
114
339
  return undefined;
115
340
  }
@@ -120,12 +345,6 @@ function buildDiscoveredVfunc<K extends "class" | "interface">(
120
345
  return undefined;
121
346
  }
122
347
 
123
- const fn = proto[methodName];
124
-
125
- if (!fn) {
126
- return undefined;
127
- }
128
-
129
348
  return {
130
349
  ...descriptor,
131
350
  methodName,
@@ -133,16 +352,15 @@ function buildDiscoveredVfunc<K extends "class" | "interface">(
133
352
  };
134
353
  }
135
354
 
136
- function collectDiscoveredVfuncs<K extends "class" | "interface">(
137
- klass: AnyClass,
138
- resolveDescriptor: (methodName: string) => VfuncDescriptor<K> | undefined,
355
+ function collectDiscoveredVfuncs(
356
+ methods: MethodTable,
357
+ resolveDescriptor: (methodName: string) => VfuncDescriptor | undefined,
139
358
  skip?: Set<string>,
140
- ): DiscoveredVfunc<K>[] {
141
- const proto = (klass as { prototype: Record<string, VfuncFn> }).prototype;
142
- const result: DiscoveredVfunc<K>[] = [];
359
+ ): DiscoveredVfunc[] {
360
+ const result: DiscoveredVfunc[] = [];
143
361
 
144
- for (const methodName of ownInstanceMethodNames(klass)) {
145
- const discovered = buildDiscoveredVfunc(proto, methodName, resolveDescriptor, skip);
362
+ for (const [methodName, fn] of methods) {
363
+ const discovered = buildDiscoveredVfunc(fn, methodName, resolveDescriptor, skip);
146
364
 
147
365
  if (discovered) {
148
366
  result.push(discovered);
@@ -152,20 +370,12 @@ function collectDiscoveredVfuncs<K extends "class" | "interface">(
152
370
  return result;
153
371
  }
154
372
 
155
- function discoverClassVfuncs(klass: AnyClass): DiscoveredClassVfunc[] {
156
- return collectDiscoveredVfuncs(klass, (methodName) => {
157
- const descriptor = findClassVfuncDescriptor(klass, methodName);
158
-
159
- if (descriptor && UNSUPPORTED_CONSTRUCT_VFUNCS.has(methodName)) {
160
- throw new Error(
161
- `registerClass: overriding the GObject construct-time vtable slot '${methodName}' is not ` +
162
- "supported; run construct-time initialization in the subclass constructor, after " +
163
- "super(...), instead",
164
- );
165
- }
166
-
167
- return descriptor ?? undefined;
168
- });
373
+ function discoverClassVfuncs(klass: AnyClass, methods: MethodTable): DiscoveredVfunc[] {
374
+ return collectDiscoveredVfuncs(
375
+ methods,
376
+ (methodName) => findClassVfuncDescriptor(klass, methodName) ?? undefined,
377
+ PROPERTY_VFUNC_NAMES,
378
+ );
169
379
  }
170
380
 
171
381
  function wrapVfunc(
@@ -176,18 +386,21 @@ function wrapVfunc(
176
386
  return wrapCallback(fn as (...args: unknown[]) => unknown, { argDescriptors, returnDescriptor }, "this");
177
387
  }
178
388
 
179
- function discoverInheritedInterfaceVfuncs(
180
- klass: AnyClass,
389
+ function discoverInterfaceBindings(
390
+ methods: MethodTable,
181
391
  parentGtype: bigint,
392
+ declaredTypes: bigint[],
182
393
  claimedMethodNames: Set<string>,
183
394
  ): InterfaceVfuncBinding[] {
395
+ const declared: Set<bigint> = new Set(declaredTypes);
396
+ const gtypes: Set<bigint> = new Set([...typeInterfaces(parentGtype), ...declaredTypes]);
184
397
  const bindings: InterfaceVfuncBinding[] = [];
185
398
 
186
- for (const interfaceGtype of typeInterfaces(parentGtype)) {
187
- const [first, ...rest] = discoverInterfaceVfuncs(klass, interfaceGtype, claimedMethodNames);
399
+ for (const gtype of gtypes) {
400
+ const vfuncs = discoverInterfaceVfuncs(methods, gtype, claimedMethodNames);
188
401
 
189
- if (first !== undefined) {
190
- bindings.push({ gtype: interfaceGtype, vtableSize: first.vtableSize, vfuncs: [first, ...rest] });
402
+ if (vfuncs.length > 0 || declared.has(gtype)) {
403
+ bindings.push({ gtype, vfuncs });
191
404
  }
192
405
  }
193
406
 
@@ -195,53 +408,44 @@ function discoverInheritedInterfaceVfuncs(
195
408
  }
196
409
 
197
410
  function discoverInterfaceVfuncs(
198
- klass: AnyClass,
411
+ methods: MethodTable,
199
412
  interfaceGtype: bigint,
200
413
  claimedMethodNames: Set<string>,
201
- ): DiscoveredInterfaceVfunc[] {
202
- const vfuncRegistry = getInterfaceVfuncRegistry(interfaceGtype);
203
-
204
- if (!vfuncRegistry) {
205
- return [];
206
- }
207
-
414
+ ): DiscoveredVfunc[] {
208
415
  return collectDiscoveredVfuncs(
209
- klass,
210
- (methodName) => {
211
- const entry = vfuncRegistry[methodName];
212
-
213
- return entry?.kind === "interface" ? entry : undefined;
214
- },
416
+ methods,
417
+ (methodName) => findInterfaceVfuncDescriptor(interfaceGtype, methodName),
215
418
  claimedMethodNames,
216
419
  );
217
420
  }
218
421
 
219
- function findClassVfuncDescriptor(klass: AnyClass, methodName: string): VfuncDescriptor<"class"> | null {
220
- return (
221
- walkClassChain(getParentClass(klass), (cls) => {
222
- const entry = getVfuncRegistry(cls)?.[methodName];
422
+ const hasDispatchedProperties = (dispatch: PropertyDispatch, adoptedTypes: bigint[]): boolean =>
423
+ dispatch.accessors.length > 0 || adoptedTypes.length > 0;
223
424
 
224
- return entry?.kind === "class" ? entry : undefined;
225
- }) ?? null
226
- );
227
- }
425
+ function propertyVfuncFor(
426
+ source: PropertyVfuncSource,
427
+ spec: PropertyVfuncSpec,
428
+ ): DiscoveredVfunc | undefined {
429
+ const { klass, methods, dispatch, adoptedTypes } = source;
228
430
 
229
- function propertyVfuncs(klass: AnyClass, accessors: PropertyAccessor[]): DiscoveredClassVfunc[] {
230
- if (accessors.length === 0) {
231
- return [];
232
- }
431
+ const dispatched = hasDispatchedProperties(dispatch, adoptedTypes)
432
+ ? spec.makeDispatch(dispatch)
433
+ : undefined;
233
434
 
234
- return [
235
- buildPropertyVfunc(klass, "getProperty", makeGetProperty(accessors), true),
236
- buildPropertyVfunc(klass, "setProperty", makeSetProperty(accessors), false),
237
- ];
435
+ const fn = methods.get(spec.methodName) ?? dispatched;
436
+
437
+ return fn === undefined ? undefined : buildPropertyVfunc(klass, spec.methodName, fn, spec.isValueOut);
438
+ }
439
+
440
+ function propertyVfuncs(source: PropertyVfuncSource): DiscoveredVfunc[] {
441
+ return PROPERTY_VFUNC_SPECS.map((spec) => propertyVfuncFor(source, spec)).filter(
442
+ (vfunc) => vfunc !== undefined,
443
+ );
238
444
  }
239
445
 
240
- // `get_property` fills a GValue the caller owns, so the marshalled copy handed to JavaScript has to
241
- // be written back into it; `set_property` only reads its GValue and needs no copy back.
242
446
  function markValueCallerAllocated(argDescriptors: Descriptor[]): Descriptor[] {
243
447
  return argDescriptors.map((arg, index) =>
244
- index === VALUE_ARG_INDEX ? { ...arg, callerAllocated: true } : arg);
448
+ index === VALUE_ARG_INDEX ? { ...arg, isCallerAllocated: true } : arg);
245
449
  }
246
450
 
247
451
  function buildPropertyVfunc(
@@ -249,7 +453,7 @@ function buildPropertyVfunc(
249
453
  methodName: string,
250
454
  fn: VfuncFn,
251
455
  isValueOut: boolean,
252
- ): DiscoveredClassVfunc {
456
+ ): DiscoveredVfunc {
253
457
  const descriptor = findClassVfuncDescriptor(klass, methodName);
254
458
 
255
459
  if (!descriptor) {
@@ -268,8 +472,23 @@ function buildPropertyVfunc(
268
472
  };
269
473
  }
270
474
 
475
+ function toNativeInterface(binding: InterfaceVfuncBinding): NativeRegisterClassInterface {
476
+ const nativeInterface: NativeRegisterClassInterface = {
477
+ type: binding.gtype,
478
+ vfuncs: [...binding.vfuncs],
479
+ };
480
+
481
+ const vtableSize = binding.vfuncs[0]?.vtableSize;
482
+
483
+ if (vtableSize !== undefined) {
484
+ nativeInterface.vtableSize = vtableSize;
485
+ }
486
+
487
+ return nativeInterface;
488
+ }
489
+
271
490
  function toNativeOptions(
272
- classVfuncs: DiscoveredClassVfunc[],
491
+ classVfuncs: DiscoveredVfunc[],
273
492
  interfaceBindings: InterfaceVfuncBinding[],
274
493
  properties: Record<string, PropertySpec>,
275
494
  ): NativeRegisterClassOptions | undefined {
@@ -292,14 +511,10 @@ function toNativeOptions(
292
511
  }
293
512
 
294
513
  if (hasInterfaces) {
295
- options.interfaces = interfaceBindings.map((binding) => ({
296
- type: binding.gtype,
297
- vtableSize: binding.vtableSize,
298
- vfuncs: [...binding.vfuncs],
299
- }));
514
+ options.interfaces = interfaceBindings.map((binding) => toNativeInterface(binding));
300
515
  }
301
516
 
302
517
  return options;
303
518
  }
304
519
 
305
- export { registerClass };
520
+ export { type Interface, registerClass };