@gtkx/runtime 1.0.0-rc.1 → 1.0.0-rc.2

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 (116) hide show
  1. package/dist/arg.d.ts +6 -5
  2. package/dist/arg.d.ts.map +1 -1
  3. package/dist/arg.js +5 -4
  4. package/dist/arg.js.map +1 -1
  5. package/dist/bind.d.ts +3 -2
  6. package/dist/bind.d.ts.map +1 -1
  7. package/dist/bind.js +5 -3
  8. package/dist/bind.js.map +1 -1
  9. package/dist/callback.d.ts +3 -3
  10. package/dist/callback.d.ts.map +1 -1
  11. package/dist/callback.js +51 -33
  12. package/dist/callback.js.map +1 -1
  13. package/dist/descriptors.d.ts +67 -64
  14. package/dist/descriptors.d.ts.map +1 -1
  15. package/dist/descriptors.js +92 -60
  16. package/dist/descriptors.js.map +1 -1
  17. package/dist/error.d.ts +5 -6
  18. package/dist/error.d.ts.map +1 -1
  19. package/dist/error.js +10 -6
  20. package/dist/error.js.map +1 -1
  21. package/dist/exit-hook.d.ts +2 -0
  22. package/dist/exit-hook.d.ts.map +1 -0
  23. package/dist/exit-hook.js +3 -0
  24. package/dist/exit-hook.js.map +1 -0
  25. package/dist/fn.d.ts +2 -2
  26. package/dist/fn.d.ts.map +1 -1
  27. package/dist/fn.js +26 -15
  28. package/dist/fn.js.map +1 -1
  29. package/dist/index.d.ts +3 -2
  30. package/dist/index.d.ts.map +1 -1
  31. package/dist/index.js +2 -1
  32. package/dist/index.js.map +1 -1
  33. package/dist/library.d.ts +6 -5
  34. package/dist/library.d.ts.map +1 -1
  35. package/dist/library.js +6 -5
  36. package/dist/library.js.map +1 -1
  37. package/dist/lifecycle.d.ts +10 -9
  38. package/dist/lifecycle.d.ts.map +1 -1
  39. package/dist/lifecycle.js +38 -23
  40. package/dist/lifecycle.js.map +1 -1
  41. package/dist/listeners.d.ts +8 -8
  42. package/dist/listeners.d.ts.map +1 -1
  43. package/dist/listeners.js +28 -15
  44. package/dist/listeners.js.map +1 -1
  45. package/dist/mixin.d.ts +5 -11
  46. package/dist/mixin.d.ts.map +1 -1
  47. package/dist/mixin.js +24 -14
  48. package/dist/mixin.js.map +1 -1
  49. package/dist/native-value.d.ts +3 -2
  50. package/dist/native-value.d.ts.map +1 -1
  51. package/dist/native-value.js +75 -42
  52. package/dist/native-value.js.map +1 -1
  53. package/dist/object.d.ts +4 -3
  54. package/dist/object.d.ts.map +1 -1
  55. package/dist/object.js +8 -5
  56. package/dist/object.js.map +1 -1
  57. package/dist/promisify.d.ts +9 -1
  58. package/dist/promisify.d.ts.map +1 -1
  59. package/dist/promisify.js +18 -15
  60. package/dist/promisify.js.map +1 -1
  61. package/dist/properties.d.ts +14 -0
  62. package/dist/properties.d.ts.map +1 -0
  63. package/dist/properties.js +79 -0
  64. package/dist/properties.js.map +1 -0
  65. package/dist/register-class.d.ts +10 -2
  66. package/dist/register-class.d.ts.map +1 -1
  67. package/dist/register-class.js +83 -29
  68. package/dist/register-class.js.map +1 -1
  69. package/dist/registry.d.ts +23 -21
  70. package/dist/registry.d.ts.map +1 -1
  71. package/dist/registry.js +54 -35
  72. package/dist/registry.js.map +1 -1
  73. package/dist/signal.d.ts +7 -6
  74. package/dist/signal.d.ts.map +1 -1
  75. package/dist/signal.js +19 -12
  76. package/dist/signal.js.map +1 -1
  77. package/dist/t.d.ts +2 -2
  78. package/dist/t.d.ts.map +1 -1
  79. package/dist/t.js +2 -1
  80. package/dist/t.js.map +1 -1
  81. package/dist/tuple.d.ts +3 -2
  82. package/dist/tuple.d.ts.map +1 -1
  83. package/dist/tuple.js +7 -4
  84. package/dist/tuple.js.map +1 -1
  85. package/dist/type.d.ts +48 -41
  86. package/dist/type.d.ts.map +1 -1
  87. package/dist/type.js +107 -87
  88. package/dist/type.js.map +1 -1
  89. package/dist/value.d.ts +19 -10
  90. package/dist/value.d.ts.map +1 -1
  91. package/dist/value.js +164 -99
  92. package/dist/value.js.map +1 -1
  93. package/package.json +4 -6
  94. package/src/arg.ts +7 -5
  95. package/src/bind.ts +13 -4
  96. package/src/callback.ts +97 -35
  97. package/src/descriptors.ts +227 -85
  98. package/src/error.ts +18 -9
  99. package/src/exit-hook.ts +3 -0
  100. package/src/fn.ts +53 -22
  101. package/src/index.ts +4 -2
  102. package/src/library.ts +7 -5
  103. package/src/lifecycle.ts +54 -26
  104. package/src/listeners.ts +46 -18
  105. package/src/mixin.ts +39 -14
  106. package/src/native-value.ts +103 -46
  107. package/src/object.ts +19 -7
  108. package/src/promisify.ts +42 -15
  109. package/src/properties.ts +115 -0
  110. package/src/register-class.ts +157 -30
  111. package/src/registry.ts +144 -54
  112. package/src/signal.ts +53 -13
  113. package/src/t.ts +3 -1
  114. package/src/tuple.ts +16 -4
  115. package/src/type.ts +183 -93
  116. package/src/value.ts +231 -108
package/src/object.ts CHANGED
@@ -23,6 +23,7 @@ const gObjectGetProperty = bind(
23
23
  [objectT("borrowed"), stringT("borrowed"), VALUE_T],
24
24
  voidT,
25
25
  );
26
+
26
27
  const gObjectSetProperty = bind(
27
28
  LIB,
28
29
  "g_object_set_property",
@@ -39,15 +40,23 @@ const gObjectSetProperty = bind(
39
40
  * @param props Property names mapped to `[descriptor, value]` pairs.
40
41
  * @returns The handle of the newly created object.
41
42
  */
42
- export function newObjectWithProperties(gtype: bigint, props: Record<string, unknown>): ExternalObject<Handle> {
43
+ function newObjectWithProperties(gtype: bigint, props: Record<string, unknown>): ExternalObject<Handle> {
43
44
  const names: string[] = [];
44
45
  const values: ExternalObject<Handle>[] = [];
45
46
 
46
47
  for (const name in props) {
47
- const entry = props[name];
48
- if (!Array.isArray(entry)) continue;
49
- const [descriptor, value] = entry;
50
- if (value === undefined) continue;
48
+ const entry: unknown = props[name];
49
+
50
+ if (!Array.isArray(entry)) {
51
+ continue;
52
+ }
53
+
54
+ const [descriptor, value] = entry as [Descriptor, unknown];
55
+
56
+ if (value === undefined) {
57
+ continue;
58
+ }
59
+
51
60
  names.push(name);
52
61
  values.push(toValue(descriptor, value));
53
62
  }
@@ -63,9 +72,10 @@ export function newObjectWithProperties(gtype: bigint, props: Record<string, unk
63
72
  * @param propertyName The property name.
64
73
  * @param descriptor Describes the property's type.
65
74
  */
66
- export function getObjectProperty(obj: object, propertyName: string, descriptor: Descriptor): unknown {
75
+ function getObjectProperty(obj: object, propertyName: string, descriptor: Descriptor): unknown {
67
76
  const value = newValueForDescriptor(descriptor);
68
77
  gObjectGetProperty(getHandle(obj), propertyName, value);
78
+
69
79
  return fromValue(value);
70
80
  }
71
81
 
@@ -78,6 +88,8 @@ export function getObjectProperty(obj: object, propertyName: string, descriptor:
78
88
  * @param descriptor Describes the property's type.
79
89
  * @param jsValue The value to set.
80
90
  */
81
- export function setObjectProperty(obj: object, propertyName: string, descriptor: Descriptor, jsValue: unknown): void {
91
+ function setObjectProperty(obj: object, propertyName: string, descriptor: Descriptor, jsValue: unknown): void {
82
92
  gObjectSetProperty(getHandle(obj), propertyName, toValue(descriptor, jsValue));
83
93
  }
94
+
95
+ export { newObjectWithProperties, getObjectProperty, setObjectProperty };
package/src/promisify.ts CHANGED
@@ -1,11 +1,43 @@
1
1
  import { tryGetHandle } from "./registry.js";
2
2
 
3
+ /**
4
+ * Extracts the finished value from the async result a GIO-style operation completes with.
5
+ *
6
+ * @template R - The async result type passed to the completion callback.
7
+ * @template T - The finished value type.
8
+ */
9
+ type FinishResult<R extends object, T> = (result: R) => T;
10
+
11
+ type Settlement<R extends object, T> = {
12
+ finish: FinishResult<R, T>;
13
+ creationStack: Error | undefined;
14
+ resolve: (value: T) => void;
15
+ reject: (reason: Error) => void;
16
+ };
17
+
3
18
  const attachCreationStack = (error: unknown, creationStack: Error | undefined): void => {
4
- if (creationStack === undefined || !(error instanceof Error)) return;
5
- if (error.cause !== undefined || !Object.isExtensible(error)) return;
19
+ if (creationStack === undefined || !(error instanceof Error)) {
20
+ return;
21
+ }
22
+
23
+ if (error.cause !== undefined || !Object.isExtensible(error)) {
24
+ return;
25
+ }
26
+
6
27
  error.cause = creationStack;
7
28
  };
8
29
 
30
+ const settle = <R extends object, T>(settlement: Settlement<R, T>, asyncResult: object): void => {
31
+ const { finish, creationStack, resolve, reject } = settlement;
32
+
33
+ try {
34
+ resolve(finish(asyncResult as R));
35
+ } catch (error) {
36
+ attachCreationStack(error, creationStack);
37
+ reject(error instanceof Error ? error : new Error(String(error)));
38
+ }
39
+ };
40
+
9
41
  /**
10
42
  * Wraps a GIO-style asynchronous function that takes a completion callback into a
11
43
  * promise, invoking the finish function to extract the result. Outside production,
@@ -17,24 +49,19 @@ const attachCreationStack = (error: unknown, creationStack: Error | undefined):
17
49
  * @param leading Arguments passed to `asyncFn` before the cancellable and callback.
18
50
  * @returns A promise resolving to the finished result, or rejecting if `finish` throws.
19
51
  */
20
- export const promisify = <R extends object, T>(
52
+ const promisify = <R extends object, T>(
21
53
  asyncFn: (...args: unknown[]) => void,
22
- finish: (result: R) => T,
54
+ finish: FinishResult<R, T>,
23
55
  cancellable: object | null | undefined,
24
56
  ...leading: unknown[]
25
57
  ): Promise<T> =>
26
58
  new Promise<T>((resolve, reject) => {
27
- let creationStack: Error | undefined;
28
- if (process.env.NODE_ENV !== "production") {
29
- creationStack = new Error("gtkx async operation started here");
30
- Error.captureStackTrace(creationStack, promisify);
31
- }
59
+ const creationStack =
60
+ process.env.NODE_ENV === "production" ? undefined : new Error("gtkx async operation started here");
61
+
32
62
  asyncFn(...leading, tryGetHandle(cancellable), (_source: object | null, asyncResult: object) => {
33
- try {
34
- resolve(finish(asyncResult as R));
35
- } catch (error) {
36
- attachCreationStack(error, creationStack);
37
- reject(error);
38
- }
63
+ settle({ finish, creationStack, resolve, reject }, asyncResult);
39
64
  });
40
65
  });
66
+
67
+ export { promisify, type FinishResult };
@@ -0,0 +1,115 @@
1
+ import type { ExternalObject, Handle, RegisterClassProperty } from "@gtkx/native";
2
+ import type { AnyClass } from "@gtkx/utils";
3
+ import { camelCase } from "@gtkx/utils";
4
+ import { bind } from "./bind.js";
5
+ import { LIB, PARAM_T, VALUE_T } from "./library.js";
6
+ import { getHandle } from "./registry.js";
7
+ import { fromValue, intoValue } from "./value.js";
8
+
9
+ type PropertyAccessor = {
10
+ name: string;
11
+ storage: symbol;
12
+ pspec: object;
13
+ };
14
+
15
+ type PropertySpec = object;
16
+ type NotifyingObject = { notify?: (propertyName: string) => void };
17
+
18
+ const FIRST_PROPERTY_ID = 1;
19
+ const paramSpecDefaultValue = bind(LIB, "g_param_spec_get_default_value", [PARAM_T], VALUE_T);
20
+
21
+ const underscoreCase = (name: string): string => name.replaceAll("-", "_");
22
+ const accessorNames = (name: string): string[] => [...new Set([name, underscoreCase(name), camelCase(name)])];
23
+
24
+ function readStored(instance: Record<symbol, unknown>, accessor: PropertyAccessor): unknown {
25
+ if (!Object.hasOwn(instance, accessor.storage)) {
26
+ const defaults = paramSpecDefaultValue(getHandle(accessor.pspec)) as ExternalObject<Handle>;
27
+ instance[accessor.storage] = fromValue(defaults);
28
+ }
29
+
30
+ return instance[accessor.storage];
31
+ }
32
+
33
+ function defineAccessor(prototype: object, accessor: PropertyAccessor, alias: string): void {
34
+ if (Object.getOwnPropertyDescriptor(prototype, alias) !== undefined) {
35
+ return;
36
+ }
37
+
38
+ Object.defineProperty(prototype, alias, {
39
+ configurable: true,
40
+ enumerable: true,
41
+ get(this: Record<symbol, unknown>) {
42
+ return readStored(this, accessor);
43
+ },
44
+ set(this: Record<symbol, unknown>, value: unknown) {
45
+ if (readStored(this, accessor) === value) {
46
+ return;
47
+ }
48
+
49
+ this[accessor.storage] = value;
50
+ (this as NotifyingObject).notify?.(accessor.name);
51
+ },
52
+ });
53
+ }
54
+
55
+ function installAccessors(klass: AnyClass, accessor: PropertyAccessor): void {
56
+ const prototype = (klass as { prototype: object }).prototype;
57
+
58
+ for (const alias of accessorNames(accessor.name)) {
59
+ defineAccessor(prototype, accessor, alias);
60
+ }
61
+ }
62
+
63
+ function resolveAccessor(accessors: PropertyAccessor[], propertyId: number): PropertyAccessor {
64
+ const accessor = accessors[propertyId - FIRST_PROPERTY_ID];
65
+
66
+ if (accessor === undefined) {
67
+ throw new RangeError(`registerClass: no property registered for id ${String(propertyId)}`);
68
+ }
69
+
70
+ return accessor;
71
+ }
72
+
73
+ function makeGetProperty(accessors: PropertyAccessor[]) {
74
+ return function getProperty(this: object, propertyId: number, value: object): void {
75
+ const accessor = resolveAccessor(accessors, propertyId);
76
+ const current = (this as Record<string, unknown>)[camelCase(accessor.name)];
77
+ intoValue(getHandle(value), current);
78
+ };
79
+ }
80
+
81
+ function makeSetProperty(accessors: PropertyAccessor[]) {
82
+ return function setProperty(this: object, propertyId: number, value: object): void {
83
+ const accessor = resolveAccessor(accessors, propertyId);
84
+ const jsValue = fromValue(getHandle(value));
85
+ (this as Record<string, unknown>)[camelCase(accessor.name)] = jsValue;
86
+ };
87
+ }
88
+
89
+ function buildAccessors(klass: AnyClass, properties: Record<string, PropertySpec>): PropertyAccessor[] {
90
+ const accessors: PropertyAccessor[] = [];
91
+
92
+ for (const [name, pspec] of Object.entries(properties)) {
93
+ const accessor: PropertyAccessor = { name, storage: Symbol(`gtkx:property:${name}`), pspec };
94
+ installAccessors(klass, accessor);
95
+ accessors.push(accessor);
96
+ }
97
+
98
+ return accessors;
99
+ }
100
+
101
+ function toNativeProperties(properties: Record<string, PropertySpec>): RegisterClassProperty[] {
102
+ return Object.values(properties).map((pspec, index) => ({
103
+ id: index + FIRST_PROPERTY_ID,
104
+ pspec: getHandle(pspec),
105
+ }));
106
+ }
107
+
108
+ export {
109
+ buildAccessors,
110
+ makeGetProperty,
111
+ makeSetProperty,
112
+ toNativeProperties,
113
+ type PropertyAccessor,
114
+ type PropertySpec,
115
+ };
@@ -1,10 +1,19 @@
1
+ import type { Descriptor } from "@gtkx/native";
1
2
  import {
3
+ registerClass as nativeRegisterClass,
2
4
  type RegisterClassOptions as NativeRegisterClassOptions,
3
5
  type RegisterClassVfunc as NativeRegisterClassVfunc,
4
- registerClass as nativeRegisterClass,
5
6
  } from "@gtkx/native";
6
7
  import { type AnyClass, getParentClass, walkClassChain } from "@gtkx/utils";
7
8
  import { wrapCallback } from "./callback.js";
9
+ import {
10
+ buildAccessors,
11
+ makeGetProperty,
12
+ makeSetProperty,
13
+ type PropertyAccessor,
14
+ type PropertySpec,
15
+ toNativeProperties,
16
+ } from "./properties.js";
8
17
  import {
9
18
  getClassType,
10
19
  getInterfaceVfuncRegistry,
@@ -14,12 +23,25 @@ import {
14
23
  } from "./registry.js";
15
24
  import { TYPE_INVALID, typeInterfaces } from "./type.js";
16
25
 
17
- type RegisterClassOptions = { typeName?: string };
26
+ type RegisterClassOptions = {
27
+ typeName?: string;
28
+ /**
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`.
33
+ */
34
+ properties?: Record<string, PropertySpec>;
35
+ };
36
+
18
37
  type VfuncFn = NativeRegisterClassVfunc["fn"];
19
38
  type DiscoveredVfunc<K extends "class" | "interface"> = VfuncDescriptor<K> & { methodName: string; fn: VfuncFn };
20
39
  type DiscoveredClassVfunc = DiscoveredVfunc<"class">;
21
40
  type DiscoveredInterfaceVfunc = DiscoveredVfunc<"interface">;
22
- type InterfaceVfuncBinding = { gtype: bigint; vfuncs: DiscoveredInterfaceVfunc[] };
41
+ type InterfaceVfuncBinding = { gtype: bigint; vtableSize: number; vfuncs: DiscoveredInterfaceVfunc[] };
42
+
43
+ const UNSUPPORTED_CONSTRUCT_VFUNCS: Set<string> = new Set(["constructed", "setProperty", "getProperty"]);
44
+ const VALUE_ARG_INDEX = 2;
23
45
 
24
46
  /**
25
47
  * Registers a subclass of a wrapper class as a new GType, wiring up any class and
@@ -31,8 +53,9 @@ type InterfaceVfuncBinding = { gtype: bigint; vfuncs: DiscoveredInterfaceVfunc[]
31
53
  * @param options Registration options, such as an explicit type name.
32
54
  * @returns The same class, now registered.
33
55
  */
34
- export function registerClass<T extends AnyClass>(klass: T, options: RegisterClassOptions = {}): T {
56
+ function registerClass<T extends AnyClass>(klass: T, options: RegisterClassOptions = {}): T {
35
57
  const parentType = resolveParentType(klass);
58
+
36
59
  if (parentType === TYPE_INVALID) {
37
60
  throw new TypeError(`registerClass: ${klass.name} must extend a registered wrapper class`);
38
61
  }
@@ -43,11 +66,12 @@ export function registerClass<T extends AnyClass>(klass: T, options: RegisterCla
43
66
  throw new Error("registerClass: cannot derive a GType name (anonymous class with no typeName option)");
44
67
  }
45
68
 
46
- const classVfuncs = discoverClassVfuncs(klass);
69
+ const properties = options.properties ?? {};
70
+ const accessors = buildAccessors(klass, properties);
71
+ const classVfuncs = [...discoverClassVfuncs(klass), ...propertyVfuncs(klass, accessors)];
47
72
  const claimedMethodNames = new Set(classVfuncs.map((vfunc) => vfunc.methodName));
48
73
  const interfaceBindings = discoverInheritedInterfaceVfuncs(klass, parentType, claimedMethodNames);
49
-
50
- const nativeOptions = toNativeOptions(classVfuncs, interfaceBindings);
74
+ const nativeOptions = toNativeOptions(classVfuncs, interfaceBindings, properties);
51
75
  const newType: bigint = nativeRegisterClass(name, parentType, nativeOptions);
52
76
  registerClassType(klass, newType);
53
77
 
@@ -58,20 +82,57 @@ function resolveParentType(klass: AnyClass): bigint {
58
82
  return (
59
83
  walkClassChain(getParentClass(klass), (cls) => {
60
84
  const gtype = getClassType(cls);
61
- return gtype !== TYPE_INVALID ? gtype : undefined;
85
+
86
+ return gtype === TYPE_INVALID ? undefined : gtype;
62
87
  }) ?? TYPE_INVALID
63
88
  );
64
89
  }
65
90
 
66
91
  function ownInstanceMethodNames(klass: AnyClass): string[] {
67
92
  const proto = (klass as { prototype?: object }).prototype;
68
- if (!proto) return [];
93
+
94
+ if (!proto) {
95
+ return [];
96
+ }
97
+
69
98
  return Object.getOwnPropertyNames(proto).filter((name) => {
70
- if (name === "constructor") return false;
71
- return typeof (proto as Record<string, unknown>)[name] === "function";
99
+ if (name === "constructor") {
100
+ return false;
101
+ }
102
+
103
+ return typeof Object.getOwnPropertyDescriptor(proto, name)?.value === "function";
72
104
  });
73
105
  }
74
106
 
107
+ function buildDiscoveredVfunc<K extends "class" | "interface">(
108
+ proto: Record<string, VfuncFn>,
109
+ methodName: string,
110
+ resolveDescriptor: (methodName: string) => VfuncDescriptor<K> | undefined,
111
+ skip: Set<string> | undefined,
112
+ ): DiscoveredVfunc<K> | undefined {
113
+ if (skip?.has(methodName)) {
114
+ return undefined;
115
+ }
116
+
117
+ const descriptor = resolveDescriptor(methodName);
118
+
119
+ if (!descriptor) {
120
+ return undefined;
121
+ }
122
+
123
+ const fn = proto[methodName];
124
+
125
+ if (!fn) {
126
+ return undefined;
127
+ }
128
+
129
+ return {
130
+ ...descriptor,
131
+ methodName,
132
+ fn: wrapVfunc(fn, descriptor.argDescriptors, descriptor.returnDescriptor),
133
+ };
134
+ }
135
+
75
136
  function collectDiscoveredVfuncs<K extends "class" | "interface">(
76
137
  klass: AnyClass,
77
138
  resolveDescriptor: (methodName: string) => VfuncDescriptor<K> | undefined,
@@ -79,31 +140,30 @@ function collectDiscoveredVfuncs<K extends "class" | "interface">(
79
140
  ): DiscoveredVfunc<K>[] {
80
141
  const proto = (klass as { prototype: Record<string, VfuncFn> }).prototype;
81
142
  const result: DiscoveredVfunc<K>[] = [];
143
+
82
144
  for (const methodName of ownInstanceMethodNames(klass)) {
83
- if (skip?.has(methodName)) continue;
84
- const descriptor = resolveDescriptor(methodName);
85
- if (!descriptor) continue;
86
- const fn = proto[methodName];
87
- if (!fn) continue;
88
- result.push({
89
- ...descriptor,
90
- methodName,
91
- fn: wrapVfunc(fn, descriptor.argDescriptors, descriptor.returnDescriptor),
92
- });
145
+ const discovered = buildDiscoveredVfunc(proto, methodName, resolveDescriptor, skip);
146
+
147
+ if (discovered) {
148
+ result.push(discovered);
149
+ }
93
150
  }
151
+
94
152
  return result;
95
153
  }
96
154
 
97
- const UNSUPPORTED_CONSTRUCT_VFUNCS: Set<string> = new Set(["constructed", "setProperty", "getProperty"]);
98
-
99
155
  function discoverClassVfuncs(klass: AnyClass): DiscoveredClassVfunc[] {
100
156
  return collectDiscoveredVfuncs(klass, (methodName) => {
101
157
  const descriptor = findClassVfuncDescriptor(klass, methodName);
158
+
102
159
  if (descriptor && UNSUPPORTED_CONSTRUCT_VFUNCS.has(methodName)) {
103
160
  throw new Error(
104
- `registerClass: overriding the GObject construct-time vtable slot '${methodName}' is not supported; run construct-time initialization in the subclass constructor, after super(...), instead`,
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",
105
164
  );
106
165
  }
166
+
107
167
  return descriptor ?? undefined;
108
168
  });
109
169
  }
@@ -122,12 +182,15 @@ function discoverInheritedInterfaceVfuncs(
122
182
  claimedMethodNames: Set<string>,
123
183
  ): InterfaceVfuncBinding[] {
124
184
  const bindings: InterfaceVfuncBinding[] = [];
185
+
125
186
  for (const interfaceGtype of typeInterfaces(parentGtype)) {
126
- const vfuncs = discoverInterfaceVfuncs(klass, interfaceGtype, claimedMethodNames);
127
- if (vfuncs.length > 0) {
128
- bindings.push({ gtype: interfaceGtype, vfuncs });
187
+ const [first, ...rest] = discoverInterfaceVfuncs(klass, interfaceGtype, claimedMethodNames);
188
+
189
+ if (first !== undefined) {
190
+ bindings.push({ gtype: interfaceGtype, vtableSize: first.vtableSize, vfuncs: [first, ...rest] });
129
191
  }
130
192
  }
193
+
131
194
  return bindings;
132
195
  }
133
196
 
@@ -137,11 +200,16 @@ function discoverInterfaceVfuncs(
137
200
  claimedMethodNames: Set<string>,
138
201
  ): DiscoveredInterfaceVfunc[] {
139
202
  const vfuncRegistry = getInterfaceVfuncRegistry(interfaceGtype);
140
- if (!vfuncRegistry) return [];
203
+
204
+ if (!vfuncRegistry) {
205
+ return [];
206
+ }
207
+
141
208
  return collectDiscoveredVfuncs(
142
209
  klass,
143
210
  (methodName) => {
144
211
  const entry = vfuncRegistry[methodName];
212
+
145
213
  return entry?.kind === "interface" ? entry : undefined;
146
214
  },
147
215
  claimedMethodNames,
@@ -152,27 +220,86 @@ function findClassVfuncDescriptor(klass: AnyClass, methodName: string): VfuncDes
152
220
  return (
153
221
  walkClassChain(getParentClass(klass), (cls) => {
154
222
  const entry = getVfuncRegistry(cls)?.[methodName];
223
+
155
224
  return entry?.kind === "class" ? entry : undefined;
156
225
  }) ?? null
157
226
  );
158
227
  }
159
228
 
229
+ function propertyVfuncs(klass: AnyClass, accessors: PropertyAccessor[]): DiscoveredClassVfunc[] {
230
+ if (accessors.length === 0) {
231
+ return [];
232
+ }
233
+
234
+ return [
235
+ buildPropertyVfunc(klass, "getProperty", makeGetProperty(accessors), true),
236
+ buildPropertyVfunc(klass, "setProperty", makeSetProperty(accessors), false),
237
+ ];
238
+ }
239
+
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
+ function markValueCallerAllocated(argDescriptors: Descriptor[]): Descriptor[] {
243
+ return argDescriptors.map((arg, index) =>
244
+ index === VALUE_ARG_INDEX ? { ...arg, callerAllocated: true } : arg);
245
+ }
246
+
247
+ function buildPropertyVfunc(
248
+ klass: AnyClass,
249
+ methodName: string,
250
+ fn: VfuncFn,
251
+ isValueOut: boolean,
252
+ ): DiscoveredClassVfunc {
253
+ const descriptor = findClassVfuncDescriptor(klass, methodName);
254
+
255
+ if (!descriptor) {
256
+ throw new Error(`registerClass: the parent class exposes no '${methodName}' vtable slot`);
257
+ }
258
+
259
+ const argDescriptors = isValueOut
260
+ ? markValueCallerAllocated(descriptor.argDescriptors)
261
+ : descriptor.argDescriptors;
262
+
263
+ return {
264
+ ...descriptor,
265
+ methodName,
266
+ argDescriptors,
267
+ fn: wrapVfunc(fn, argDescriptors, descriptor.returnDescriptor),
268
+ };
269
+ }
270
+
160
271
  function toNativeOptions(
161
272
  classVfuncs: DiscoveredClassVfunc[],
162
273
  interfaceBindings: InterfaceVfuncBinding[],
274
+ properties: Record<string, PropertySpec>,
163
275
  ): NativeRegisterClassOptions | undefined {
164
276
  const hasInterfaces = interfaceBindings.length > 0;
165
277
  const hasClassVfuncs = classVfuncs.length > 0;
166
- if (!hasClassVfuncs && !hasInterfaces) {
278
+ const hasProperties = Object.keys(properties).length > 0;
279
+
280
+ if (!hasClassVfuncs && !hasInterfaces && !hasProperties) {
167
281
  return undefined;
168
282
  }
283
+
169
284
  const options: NativeRegisterClassOptions = {};
170
- if (hasClassVfuncs) options.vfuncs = [...classVfuncs];
285
+
286
+ if (hasProperties) {
287
+ options.properties = toNativeProperties(properties);
288
+ }
289
+
290
+ if (hasClassVfuncs) {
291
+ options.vfuncs = [...classVfuncs];
292
+ }
293
+
171
294
  if (hasInterfaces) {
172
295
  options.interfaces = interfaceBindings.map((binding) => ({
173
296
  type: binding.gtype,
297
+ vtableSize: binding.vtableSize,
174
298
  vfuncs: [...binding.vfuncs],
175
299
  }));
176
300
  }
301
+
177
302
  return options;
178
303
  }
304
+
305
+ export { registerClass };