@gtkx/runtime 1.2.2 → 1.4.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 (101) hide show
  1. package/README.md +6 -4
  2. package/dist/arg.d.ts +6 -1
  3. package/dist/arg.d.ts.map +1 -1
  4. package/dist/arg.js +2 -1
  5. package/dist/arg.js.map +1 -1
  6. package/dist/closure.d.ts.map +1 -1
  7. package/dist/closure.js +2 -11
  8. package/dist/closure.js.map +1 -1
  9. package/dist/descriptors.d.ts +21 -3
  10. package/dist/descriptors.d.ts.map +1 -1
  11. package/dist/descriptors.js +50 -21
  12. package/dist/descriptors.js.map +1 -1
  13. package/dist/field.d.ts +42 -0
  14. package/dist/field.d.ts.map +1 -0
  15. package/dist/field.js +41 -0
  16. package/dist/field.js.map +1 -0
  17. package/dist/fn.d.ts +2 -0
  18. package/dist/fn.d.ts.map +1 -1
  19. package/dist/fn.js +50 -10
  20. package/dist/fn.js.map +1 -1
  21. package/dist/index.d.ts +14 -6
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +13 -5
  24. package/dist/index.js.map +1 -1
  25. package/dist/mixin.d.ts +2 -1
  26. package/dist/mixin.d.ts.map +1 -1
  27. package/dist/mixin.js +1 -1
  28. package/dist/mixin.js.map +1 -1
  29. package/dist/native-value.d.ts +9 -3
  30. package/dist/native-value.d.ts.map +1 -1
  31. package/dist/native-value.js +86 -9
  32. package/dist/native-value.js.map +1 -1
  33. package/dist/object.d.ts +14 -6
  34. package/dist/object.d.ts.map +1 -1
  35. package/dist/object.js +21 -10
  36. package/dist/object.js.map +1 -1
  37. package/dist/param-spec.d.ts +32 -1
  38. package/dist/param-spec.d.ts.map +1 -1
  39. package/dist/param-spec.js +67 -5
  40. package/dist/param-spec.js.map +1 -1
  41. package/dist/promisify.d.ts +13 -1
  42. package/dist/promisify.d.ts.map +1 -1
  43. package/dist/promisify.js +14 -1
  44. package/dist/promisify.js.map +1 -1
  45. package/dist/properties.d.ts +48 -5
  46. package/dist/properties.d.ts.map +1 -1
  47. package/dist/properties.js +191 -20
  48. package/dist/properties.js.map +1 -1
  49. package/dist/regex.d.ts +25 -0
  50. package/dist/regex.d.ts.map +1 -0
  51. package/dist/regex.js +59 -0
  52. package/dist/regex.js.map +1 -0
  53. package/dist/register-class.d.ts +165 -15
  54. package/dist/register-class.d.ts.map +1 -1
  55. package/dist/register-class.js +175 -19
  56. package/dist/register-class.js.map +1 -1
  57. package/dist/registry.d.ts +48 -4
  58. package/dist/registry.d.ts.map +1 -1
  59. package/dist/registry.js +147 -3
  60. package/dist/registry.js.map +1 -1
  61. package/dist/signal.d.ts +11 -1
  62. package/dist/signal.d.ts.map +1 -1
  63. package/dist/signal.js +66 -6
  64. package/dist/signal.js.map +1 -1
  65. package/dist/t.d.ts +11 -0
  66. package/dist/t.d.ts.map +1 -1
  67. package/dist/t.js +3 -0
  68. package/dist/t.js.map +1 -1
  69. package/dist/type.d.ts +2 -1
  70. package/dist/type.d.ts.map +1 -1
  71. package/dist/type.js +5 -1
  72. package/dist/type.js.map +1 -1
  73. package/dist/value.d.ts +48 -4
  74. package/dist/value.d.ts.map +1 -1
  75. package/dist/value.js +172 -16
  76. package/dist/value.js.map +1 -1
  77. package/dist/variant.d.ts +75 -26
  78. package/dist/variant.d.ts.map +1 -1
  79. package/dist/variant.js +44 -29
  80. package/dist/variant.js.map +1 -1
  81. package/package.json +5 -5
  82. package/src/arg.ts +6 -1
  83. package/src/closure.ts +2 -14
  84. package/src/descriptors.ts +74 -21
  85. package/src/field.ts +76 -0
  86. package/src/fn.ts +73 -17
  87. package/src/index.ts +46 -5
  88. package/src/mixin.ts +1 -1
  89. package/src/native-value.ts +146 -14
  90. package/src/object.ts +23 -11
  91. package/src/param-spec.ts +96 -8
  92. package/src/promisify.ts +27 -1
  93. package/src/properties.ts +280 -32
  94. package/src/regex.ts +92 -0
  95. package/src/register-class.ts +485 -42
  96. package/src/registry.ts +219 -4
  97. package/src/signal.ts +117 -4
  98. package/src/t.ts +13 -0
  99. package/src/type.ts +7 -0
  100. package/src/value.ts +268 -14
  101. package/src/variant.ts +184 -53
package/src/fn.ts CHANGED
@@ -1,14 +1,16 @@
1
- import type { CallDescriptor, Descriptor, ExternalObject, Ref } from "@gtkx/native";
1
+ import type { CallDescriptor, Descriptor, ExternalObject, Handle, Ref } from "@gtkx/native";
2
2
  import { call, bind as nativeBind } from "@gtkx/native";
3
3
  import type { RefSeeds } from "./vfunc-seeds.js";
4
- import { type Arg, isCallerAllocatedArg, isOutputArg, isRefArg, requiresInputArg } from "./arg.js";
4
+ import { type Arg, isCallerAllocatedArg, isOutputArg, isRefArg, isUnpackedArg, requiresInputArg } from "./arg.js";
5
5
  import { wrapCallbackValue } from "./callback.js";
6
- import { boxedT, refT } from "./descriptors.js";
6
+ import { boxedT, isGtypeDescriptor, refT } from "./descriptors.js";
7
7
  import { checkError } from "./error.js";
8
8
  import { LIB } from "./library.js";
9
- import { fromNative } from "./native-value.js";
9
+ import { fromNative, toNative } from "./native-value.js";
10
10
  import { getHandle } from "./registry.js";
11
11
  import { hasSurfacedPrimary, packTupleResult } from "./tuple.js";
12
+ import { TYPE_INVALID } from "./type.js";
13
+ import { fromValue, getValueType } from "./value.js";
12
14
 
13
15
  /** The signature a native function is bound against. */
14
16
  type FnSpec = {
@@ -29,6 +31,8 @@ type FnSpec = {
29
31
  * architectures.
30
32
  */
31
33
  fixedArgCount?: number;
34
+ /** The callee returns a `GValue`, surfaced as what it holds rather than as the value itself. */
35
+ isReturnUnpacked?: boolean;
32
36
  };
33
37
 
34
38
  type ArgSpec = {
@@ -36,13 +40,18 @@ type ArgSpec = {
36
40
  index: number;
37
41
  isRef: boolean;
38
42
  isCallerAllocated: boolean;
43
+ isUnpacked: boolean;
39
44
  requiresInput: boolean;
40
45
  inputIndex: number;
41
46
  isOutParam: boolean;
47
+ carriesGtype: boolean;
42
48
  };
43
49
 
44
50
  const NO_OUT_PARAMS: unknown[] = [];
45
51
 
52
+ const hasGtypeInput = (descriptor: Descriptor): boolean =>
53
+ isGtypeDescriptor(descriptor) || (descriptor.kind === "array" && isGtypeDescriptor(descriptor.itemDescriptor));
54
+
46
55
  const buildNativeArgTypes = (args: Arg[], canThrow: boolean): Descriptor[] => {
47
56
  const nativeArgTypes = args.map((argSpec) =>
48
57
  argSpec.direction !== undefined && argSpec.isCallerAllocated !== true ? refT(argSpec.type) : argSpec.type,
@@ -70,9 +79,11 @@ const buildArgSpecs = (args: Arg[]): ArgSpec[] => {
70
79
  index,
71
80
  isRef,
72
81
  isCallerAllocated: isCallerAllocatedArg(arg),
82
+ isUnpacked: isUnpackedArg(arg),
73
83
  requiresInput,
74
84
  inputIndex: requiresInput ? inputCursor++ : -1,
75
85
  isOutParam,
86
+ carriesGtype: hasGtypeInput(arg.type),
76
87
  };
77
88
  });
78
89
  };
@@ -83,8 +94,11 @@ const resolveCallerAllocated = (inputs: unknown[], inputIndex: number): unknown
83
94
  return wrapper == null ? wrapper : getHandle(wrapper);
84
95
  };
85
96
 
97
+ const inputValueFor = (spec: ArgSpec, inputs: unknown[]): unknown =>
98
+ spec.carriesGtype ? toNative(spec.arg.type, inputs[spec.inputIndex]) : inputs[spec.inputIndex];
99
+
86
100
  const buildRefValue = (spec: ArgSpec, inputs: unknown[], seeds: RefSeeds | undefined): Ref => ({
87
- value: spec.requiresInput ? inputs[spec.inputIndex] : (seeds?.get(spec.index) ?? null),
101
+ value: spec.requiresInput ? inputValueFor(spec, inputs) : (seeds?.get(spec.index) ?? null),
88
102
  });
89
103
 
90
104
  const buildNativeValue = (spec: ArgSpec, inputs: unknown[], seeds: RefSeeds | undefined): unknown => {
@@ -102,12 +116,30 @@ const buildNativeValue = (spec: ArgSpec, inputs: unknown[], seeds: RefSeeds | un
102
116
  return wrapCallbackValue(arg.type, inputs[inputIndex]);
103
117
  }
104
118
 
105
- return inputs[inputIndex];
119
+ return inputValueFor(spec, inputs);
106
120
  };
107
121
 
108
122
  const buildNativeValues = (plans: ArgSpec[], inputs: unknown[], seeds: RefSeeds | undefined): unknown[] =>
109
123
  plans.map((plan) => buildNativeValue(plan, inputs, seeds));
110
124
 
125
+ const unpackValueHandle = (handle: ExternalObject<Handle> | null): unknown => {
126
+ if (handle === null || getValueType(handle) === TYPE_INVALID) {
127
+ return null;
128
+ }
129
+
130
+ return fromValue(handle);
131
+ };
132
+
133
+ const readCallerAllocated = (plan: ArgSpec, inputs: unknown[]): unknown => {
134
+ const wrapper = inputs[plan.inputIndex];
135
+
136
+ if (!plan.isUnpacked) {
137
+ return wrapper;
138
+ }
139
+
140
+ return unpackValueHandle(wrapper == null ? null : getHandle(wrapper));
141
+ };
142
+
111
143
  const readOutParams = (outPlans: ArgSpec[], inputs: unknown[], nativeValues: unknown[]): unknown[] => {
112
144
  if (outPlans.length === 0) {
113
145
  return NO_OUT_PARAMS;
@@ -115,7 +147,7 @@ const readOutParams = (outPlans: ArgSpec[], inputs: unknown[], nativeValues: unk
115
147
 
116
148
  return Array.from(outPlans, (plan) =>
117
149
  plan.isCallerAllocated
118
- ? inputs[plan.inputIndex]
150
+ ? readCallerAllocated(plan, inputs)
119
151
  : fromNative(plan.arg.type, (nativeValues[plan.index] as Ref).value));
120
152
  };
121
153
 
@@ -123,8 +155,20 @@ const isPassThroughPlan = (plan: ArgSpec, index: number): boolean =>
123
155
  plan.inputIndex === index &&
124
156
  !plan.isRef &&
125
157
  !plan.isCallerAllocated &&
158
+ !plan.carriesGtype &&
126
159
  plan.arg.type.kind !== "callback";
127
160
 
161
+ const requiredInputIndices = (plans: ArgSpec[]): number[] =>
162
+ plans.filter((plan) => plan.requiresInput && plan.arg.isRequired === true).map((plan) => plan.inputIndex);
163
+
164
+ const assertRequiredInputs = (requiredIndices: number[], inputs: unknown[]): void => {
165
+ for (const index of requiredIndices) {
166
+ if (inputs[index] === undefined) {
167
+ throw new TypeError(`Missing required argument at position ${String(index + 1)}`);
168
+ }
169
+ }
170
+ };
171
+
128
172
  const resizeInputs = (inputs: unknown[], argCount: number): unknown[] => {
129
173
  while (inputs.length < argCount) {
130
174
  inputs.push(undefined);
@@ -137,14 +181,25 @@ const resizeInputs = (inputs: unknown[], argCount: number): unknown[] => {
137
181
  return inputs;
138
182
  };
139
183
 
184
+ const returnReader = (spec: FnSpec): ((nativeResult: unknown) => unknown) =>
185
+ spec.isReturnUnpacked === true
186
+ ? (nativeResult) => unpackValueHandle((nativeResult ?? null) as ExternalObject<Handle> | null)
187
+ : (nativeResult) => fromNative(spec.returns, nativeResult);
188
+
140
189
  const directCallable = (
141
190
  descriptor: ExternalObject<CallDescriptor>,
142
- returnDescriptor: Descriptor,
191
+ readReturn: (nativeResult: unknown) => unknown,
143
192
  hasPrimary: boolean,
144
- argCount: number,
193
+ plans: ArgSpec[],
145
194
  ): ((...inputs: unknown[]) => unknown) => {
146
- const marshal = (inputs: unknown[]): unknown =>
147
- fromNative(returnDescriptor, call(descriptor, resizeInputs(inputs, argCount)));
195
+ const argCount = plans.length;
196
+ const requiredIndices = requiredInputIndices(plans);
197
+
198
+ const marshal = (inputs: unknown[]): unknown => {
199
+ assertRequiredInputs(requiredIndices, inputs);
200
+
201
+ return readReturn(call(descriptor, resizeInputs(inputs, argCount)));
202
+ };
148
203
 
149
204
  if (hasPrimary) {
150
205
  return (...inputs) => marshal(inputs);
@@ -165,20 +220,20 @@ function fromNativeCallable(
165
220
  const plans = buildArgSpecs(args);
166
221
  const outPlans = plans.filter((plan) => plan.isOutParam);
167
222
  const arePassThrough = plans.every((plan, index) => isPassThroughPlan(plan, index));
223
+ const readReturn = returnReader(spec);
168
224
 
169
225
  if (!canThrow && arePassThrough) {
170
- return directCallable(descriptor, returnDescriptor, hasPrimary, plans.length);
226
+ return directCallable(descriptor, readReturn, hasPrimary, plans);
171
227
  }
172
228
 
229
+ const requiredIndices = requiredInputIndices(plans);
230
+
173
231
  const shape = (inputs: unknown[], nativeValues: unknown[], nativeResult: unknown): unknown =>
174
- packTupleResult(
175
- readOutParams(outPlans, inputs, nativeValues),
176
- fromNative(returnDescriptor, nativeResult),
177
- hasPrimary,
178
- );
232
+ packTupleResult(readOutParams(outPlans, inputs, nativeValues), readReturn(nativeResult), hasPrimary);
179
233
 
180
234
  if (canThrow) {
181
235
  return (...inputs) => {
236
+ assertRequiredInputs(requiredIndices, inputs);
182
237
  const nativeValues = buildNativeValues(plans, inputs, getRefSeeds?.());
183
238
  const errorRef: Ref = { value: null };
184
239
  nativeValues.push(errorRef);
@@ -190,6 +245,7 @@ function fromNativeCallable(
190
245
  }
191
246
 
192
247
  return (...inputs) => {
248
+ assertRequiredInputs(requiredIndices, inputs);
193
249
  const nativeValues = buildNativeValues(plans, inputs, getRefSeeds?.());
194
250
 
195
251
  return shape(inputs, nativeValues, call(descriptor, nativeValues));
package/src/index.ts CHANGED
@@ -7,6 +7,8 @@ export { type ClosureCallback, ClosureMarshalError, toClosure, tryToClosure } fr
7
7
  /** @public */
8
8
  export { createErrorDomain, type ErrorDomain } from "./error.js";
9
9
  /** @public */
10
+ export { type Field, type StridedField } from "./field.js";
11
+ /** @public */
10
12
  export { onExit, quit, quitApplication, runApplication, type RunApplicationResult } from "./lifecycle.js";
11
13
  /** @public */
12
14
  export { offSignal, onceSignal, onSignal } from "./listeners.js";
@@ -24,23 +26,45 @@ export {
24
26
  setObjectProperty,
25
27
  } from "./object.js";
26
28
  /** @public */
27
- export { promisify } from "./promisify.js";
29
+ export { getParamSpecFlags, getParamSpecOwnerType, getParamSpecValueType } from "./param-spec.js";
30
+ /** @public */
31
+ export { promisify, trimFinish } from "./promisify.js";
32
+ /** @public */
33
+ export {
34
+ coerceObjectProperty,
35
+ getDeclaredPropertyName,
36
+ isReadableProperty,
37
+ newParamSpecOverride,
38
+ } from "./properties.js";
39
+ /** @public */
40
+ export { matchAllRegex, matchRegex } from "./regex.js";
28
41
  /** @public */
29
- export { type Interface, registerClass } from "./register-class.js";
42
+ export { type Interface, registerClass, type SignalGType, type SignalSpec } from "./register-class.js";
30
43
  /** @public */
31
44
  export {
32
45
  getClassType,
33
46
  getHandle,
34
47
  getInstanceType,
35
48
  getWrapperClass,
49
+ peekTypeClass,
50
+ registerClassStruct,
36
51
  registerInterface,
37
52
  registerWrapperClass,
53
+ registerWrapperClassResolver,
38
54
  type StaticBase,
39
55
  setHandle,
40
56
  wrapHandle,
57
+ type WrapperClassResolver,
41
58
  } from "./registry.js";
42
59
  /** @public */
43
- export { connectSignal, disconnectSignal, emitSignal, getSignalBaseName, type SignalHandler } from "./signal.js";
60
+ export {
61
+ connectSignal,
62
+ disconnectSignal,
63
+ emitSignal,
64
+ getSignalBaseName,
65
+ type SignalHandler,
66
+ signalForHandlerName,
67
+ } from "./signal.js";
44
68
  /** @public */
45
69
  export { t } from "./t.js";
46
70
  /** @public */
@@ -79,9 +103,26 @@ export {
79
103
  valueIsA,
80
104
  } from "./type.js";
81
105
  /** @public */
82
- export { getBoxedValue, setBoxedValue } from "./value.js";
106
+ export {
107
+ fromValue,
108
+ getBoxedValue,
109
+ type JsValue,
110
+ setBoxedValue,
111
+ toValueHandle,
112
+ tryToValueHandle,
113
+ ValueMarshalError,
114
+ } from "./value.js";
83
115
  /** @public */
84
- export { fromVariant, toVariant, type VariantValue } from "./variant.js";
116
+ export {
117
+ type ByteArray,
118
+ type FromVariantOptions,
119
+ fromVariant,
120
+ type RecursiveFromVariantOptions,
121
+ type RecursiveVariantValue,
122
+ toVariant,
123
+ type VariantInput,
124
+ type VariantValue,
125
+ } from "./variant.js";
85
126
  /** @public */
86
127
  export { callParent, callVfunc } from "./vfunc-call.js";
87
128
  /** @public */
package/src/mixin.ts CHANGED
@@ -89,4 +89,4 @@ function insertMixinLayer(target: AnyClass, mixin: Mixin, inheritedNames: Set<st
89
89
  Object.setPrototypeOf(target, layer);
90
90
  }
91
91
 
92
- export { insertMixinLayer, installMixins, type MixinReceiver, type Mixin };
92
+ export { copyLayerMembers, insertMixinLayer, installMixins, type MixinReceiver, type Mixin };
@@ -1,10 +1,41 @@
1
- import type { Descriptor, ExternalObject, Handle } from "@gtkx/native";
2
- import type { ArrayDescriptor, FundamentalDescriptor, HashTableDescriptor, StructDescriptor } from "./descriptors.js";
3
- import { getHandle, getWrapperClass, wrapCallScopedObject, wrapHandle, wrapObject } from "./registry.js";
1
+ import type { AnyClass } from "@gtkx/utils";
2
+ import {
3
+ bindFunctionPointer,
4
+ call,
5
+ type CallDescriptor,
6
+ type Descriptor,
7
+ type ExternalObject,
8
+ getType,
9
+ type Handle,
10
+ type Ref,
11
+ } from "@gtkx/native";
12
+ import {
13
+ type ArrayDescriptor,
14
+ boxedT,
15
+ type CallbackDescriptor,
16
+ type FundamentalDescriptor,
17
+ type HashTableDescriptor,
18
+ isGtypeDescriptor,
19
+ refT,
20
+ type StructDescriptor,
21
+ } from "./descriptors.js";
22
+ import { checkError } from "./error.js";
23
+ import { LIB } from "./library.js";
24
+ import {
25
+ coerceGType,
26
+ getHandle,
27
+ getWrapperClass,
28
+ resolveWrapperClass,
29
+ wrapCallScopedObject,
30
+ wrapFundamentalHandle,
31
+ wrapHandle,
32
+ wrapObject,
33
+ } from "./registry.js";
4
34
  import { resolveDescriptorType } from "./type.js";
5
35
 
6
36
  type MarshalledKind = "object" | "struct" | "boxed" | "fundamental" | "array" | "hashtable";
7
37
  type MarshalledDescriptor = Extract<Descriptor, { kind: MarshalledKind }>;
38
+ type DecodedCallbackTarget = { fnPtr: bigint; userData?: bigint | undefined };
8
39
 
9
40
  const MARSHALLED_KINDS: Set<Descriptor["kind"]> = new Set<MarshalledKind>([
10
41
  "object",
@@ -15,6 +46,8 @@ const MARSHALLED_KINDS: Set<Descriptor["kind"]> = new Set<MarshalledKind>([
15
46
  "hashtable",
16
47
  ]);
17
48
 
49
+ const NULL_POINTER = 0n;
50
+
18
51
  function isMarshalledDescriptor(descriptor: Descriptor): descriptor is MarshalledDescriptor {
19
52
  return MARSHALLED_KINDS.has(descriptor.kind);
20
53
  }
@@ -32,18 +65,103 @@ function collectionFromNative(descriptor: ArrayDescriptor, value: unknown): unkn
32
65
  }
33
66
 
34
67
  function boxedFromNative(descriptor: Descriptor, value: unknown): unknown {
35
- return value == null
36
- ? null
37
- : wrapHandle(value as ExternalObject<Handle>, getWrapperClass(resolveDescriptorType(descriptor)));
68
+ if (value == null) {
69
+ return null;
70
+ }
71
+
72
+ const handle = value as ExternalObject<Handle>;
73
+ const type = resolveDescriptorType(descriptor);
74
+
75
+ return wrapHandle(handle, getWrapperClass(type));
76
+ }
77
+
78
+ function fundamentalWrapperClass(descriptor: FundamentalDescriptor, handle: ExternalObject<Handle>): AnyClass {
79
+ if (descriptor.wrapperClass !== undefined) {
80
+ return descriptor.wrapperClass;
81
+ }
82
+
83
+ const declaredType = resolveDescriptorType(descriptor);
84
+
85
+ return resolveWrapperClass(getType(handle, declaredType)) ?? getWrapperClass(declaredType);
38
86
  }
39
87
 
40
88
  function fundamentalFromNative(descriptor: FundamentalDescriptor, value: unknown): unknown {
41
- return value == null
42
- ? null
43
- : wrapHandle(
44
- value as ExternalObject<Handle>,
45
- descriptor.wrapperClass ?? getWrapperClass(resolveDescriptorType(descriptor)),
46
- );
89
+ if (value == null) {
90
+ return null;
91
+ }
92
+
93
+ const handle = value as ExternalObject<Handle>;
94
+
95
+ return wrapFundamentalHandle(handle, fundamentalWrapperClass(descriptor, handle));
96
+ }
97
+
98
+ const errorRefDescriptor = (): Descriptor =>
99
+ refT(boxedT("GError", { ownership: "full", sharedLibrary: LIB, getTypeFnName: "g_error_get_type" }));
100
+
101
+ function decodedCallbackValues(
102
+ descriptor: CallbackDescriptor,
103
+ target: DecodedCallbackTarget,
104
+ inputs: unknown[],
105
+ ): unknown[] {
106
+ const values: unknown[] = [];
107
+ let cursor = 0;
108
+
109
+ for (const [index, argDescriptor] of descriptor.argDescriptors.entries()) {
110
+ if (index === descriptor.userDataIndex) {
111
+ values.push(target.userData ?? NULL_POINTER);
112
+ } else {
113
+ values.push(toNative(argDescriptor, inputs[cursor]));
114
+ cursor += 1;
115
+ }
116
+ }
117
+
118
+ return values;
119
+ }
120
+
121
+ function decodedCallbackCallable(
122
+ descriptor: CallbackDescriptor,
123
+ target: DecodedCallbackTarget,
124
+ ): (...inputs: unknown[]) => unknown {
125
+ const canThrow = descriptor.canThrow === true;
126
+ const isOneShot = descriptor.scope === "async";
127
+ const argDescriptors = canThrow ? [...descriptor.argDescriptors, errorRefDescriptor()] : descriptor.argDescriptors;
128
+ let bound: ExternalObject<CallDescriptor> | undefined;
129
+ let isSpent = false;
130
+
131
+ return (...inputs) => {
132
+ if (isSpent) {
133
+ throw new Error("An async-scoped callback was already invoked; its native caller has released it");
134
+ }
135
+
136
+ bound ??= bindFunctionPointer(target.fnPtr, argDescriptors, descriptor.returnDescriptor, "decoded callback");
137
+ const values = decodedCallbackValues(descriptor, target, inputs);
138
+ isSpent = isOneShot;
139
+
140
+ if (!canThrow) {
141
+ return fromNative(descriptor.returnDescriptor, call(bound, values));
142
+ }
143
+
144
+ const errorRef: Ref = { value: null };
145
+ values.push(errorRef);
146
+ const result = call(bound, values);
147
+ checkError(errorRef);
148
+
149
+ return fromNative(descriptor.returnDescriptor, result);
150
+ };
151
+ }
152
+
153
+ function callbackFromNative(descriptor: CallbackDescriptor, value: unknown): unknown {
154
+ if (value == null) {
155
+ return value;
156
+ }
157
+
158
+ const target = value as Partial<DecodedCallbackTarget>;
159
+
160
+ if (typeof target.fnPtr !== "bigint") {
161
+ return value;
162
+ }
163
+
164
+ return decodedCallbackCallable(descriptor, { fnPtr: target.fnPtr, userData: target.userData });
47
165
  }
48
166
 
49
167
  function hashTableFromNative(descriptor: HashTableDescriptor, value: unknown): unknown {
@@ -64,12 +182,21 @@ function hashTableFromNative(descriptor: HashTableDescriptor, value: unknown): u
64
182
  /**
65
183
  * Converts a raw value returned from native code into its JavaScript form,
66
184
  * wrapping object, struct, boxed, and fundamental handles and recursively
67
- * converting arrays and hash tables according to the descriptor.
185
+ * converting arrays and hash tables according to the descriptor. A callback
186
+ * decoded from a native function pointer and its bound user data becomes a
187
+ * callable function that invokes the native callback; it stays valid only as
188
+ * long as the native caller keeps the pointer pair alive, which for an
189
+ * async-scoped callback means until the first invocation — calling one a
190
+ * second time throws instead of reaching the released native closure.
68
191
  *
69
192
  * @param descriptor Describes the native type of the value.
70
193
  * @param value The raw native value to convert.
71
194
  */
72
195
  function fromNative(descriptor: Descriptor, value: unknown): unknown {
196
+ if (descriptor.kind === "callback") {
197
+ return callbackFromNative(descriptor, value);
198
+ }
199
+
73
200
  if (!isMarshalledDescriptor(descriptor)) {
74
201
  return value;
75
202
  }
@@ -113,13 +240,18 @@ function hashTableToNative(descriptor: HashTableDescriptor, value: unknown): unk
113
240
 
114
241
  /**
115
242
  * Converts a JavaScript value into the raw form native code expects, unwrapping
116
- * object, struct, boxed, and fundamental wrappers back to their handles and
243
+ * object, struct, boxed, and fundamental wrappers back to their handles,
244
+ * resolving a class passed for a GType to the GType it was registered under, and
117
245
  * recursively converting arrays and maps according to the descriptor.
118
246
  *
119
247
  * @param descriptor Describes the native type to convert to.
120
248
  * @param value The JavaScript value to convert.
121
249
  */
122
250
  function toNative(descriptor: Descriptor, value: unknown): unknown {
251
+ if (isGtypeDescriptor(descriptor)) {
252
+ return coerceGType(value);
253
+ }
254
+
123
255
  if (!isMarshalledDescriptor(descriptor)) {
124
256
  return value;
125
257
  }
package/src/object.ts CHANGED
@@ -3,9 +3,9 @@ import { type AnyClass, getParentClass } from "@gtkx/utils";
3
3
  import { bind } from "./bind.js";
4
4
  import { objectT, stringT, voidT } from "./descriptors.js";
5
5
  import { LIB, VALUE_T } from "./library.js";
6
- import { type ConstructProperty, constructPropertyFor } from "./properties.js";
6
+ import { coercePropertyValue, type ConstructProperty, constructPropertyFor } from "./properties.js";
7
7
  import { getHandle, registerWrapper } from "./registry.js";
8
- import { fromValue, newValueForDescriptor, toValue } from "./value.js";
8
+ import { fromValueForDescriptor, newValueForDescriptor, toValue } from "./value.js";
9
9
 
10
10
  /**
11
11
  * One construct property a wrapper class accepts: the canonical `GObject` name it is set under,
@@ -110,7 +110,7 @@ function constructPropertyForEntry(
110
110
  return constructPropertyFor(source.gtype, name, value, source.wrapper);
111
111
  }
112
112
 
113
- return { name: binding[0], value: toValue(binding[1], value) };
113
+ return { name: binding[0], value: toValue(binding[1], coercePropertyValue(source.gtype, binding[0], value)) };
114
114
  }
115
115
 
116
116
  /**
@@ -123,17 +123,24 @@ function constructPropertyForEntry(
123
123
  * for a read-only property and for a value of a type the property cannot hold, and
124
124
  * a `RangeError` for a value the ParamSpec rejects. A value marshalled through a
125
125
  * declared descriptor is converted rather than checked, so a `null` handed to a
126
- * numeric one of those lands 0 rather than being refused.
126
+ * numeric one of those lands 0 rather than being refused, and a number is fitted to
127
+ * the property first, as `coerceObjectProperty` does: truncated toward zero for a
128
+ * whole-number property and clamped to the range its `GObject.ParamSpec` allows.
127
129
  * Properties whose value is `undefined` are skipped. A type registered with
128
130
  * `registerClass` binds the wrapper before its `constructed` slot runs, so an
129
131
  * override of that slot already sees a usable instance.
132
+ * When construction returns an object that already has a wrapper — it reached
133
+ * JavaScript and was wrapped before `g_object_new` returned, for example a
134
+ * `Gtk.Window` observed through the toplevels list — that existing wrapper is
135
+ * returned instead of binding `wrapper`, so both references stay one object.
130
136
  *
131
137
  * @param gtype The GType of the object to construct.
132
138
  * @param props Property names mapped to the values to set them to.
133
139
  * @param wrapper The wrapper instance to bind to the new object.
134
- * @returns The handle of the newly created object.
140
+ * @returns The wrapper bound to the constructed object: `wrapper` itself, or
141
+ * the wrapper the object already had.
135
142
  */
136
- function newObjectWithProperties(gtype: bigint, props: object, wrapper: object): ExternalObject<Handle> {
143
+ function newObjectWithProperties<T extends object>(gtype: bigint, props: object, wrapper: T): T {
137
144
  const names: string[] = [];
138
145
  const values: ExternalObject<Handle>[] = [];
139
146
  const bindings = constructBindingsFor(wrapper.constructor as AnyClass | undefined);
@@ -148,9 +155,13 @@ function newObjectWithProperties(gtype: bigint, props: object, wrapper: object):
148
155
  }
149
156
  }
150
157
 
151
- newObject(gtype, names, values, wrapper, registerWrapper);
158
+ const existing = newObject(gtype, names, values, wrapper, registerWrapper);
152
159
 
153
- return getHandle(wrapper);
160
+ if (existing !== null) {
161
+ return existing as T;
162
+ }
163
+
164
+ return wrapper;
154
165
  }
155
166
 
156
167
  /**
@@ -165,15 +176,16 @@ function getObjectProperty(obj: object, propertyName: string, descriptor: Descri
165
176
  const value = newValueForDescriptor(descriptor);
166
177
  gObjectGetProperty(getHandle(obj), propertyName, value);
167
178
 
168
- return fromValue(value);
179
+ return fromValueForDescriptor(descriptor, value);
169
180
  }
170
181
 
171
182
  /**
172
183
  * Writes a JavaScript value to a GObject property, converting it to native form
173
184
  * using the descriptor. The descriptor converts what it is given rather than checking it against
174
185
  * the property's `GObject.ParamSpec`, so `null` and `undefined` written to a numeric or enum
175
- * property land 0, where the same write to a property installed through `registerClass` is
176
- * refused with a `TypeError`.
186
+ * property land 0, and a fractional number written to a whole-number property is truncated toward
187
+ * zero, where the same writes to a property installed through `registerClass` are refused with a
188
+ * `TypeError`.
177
189
  *
178
190
  * @param obj The object to write to.
179
191
  * @param propertyName The property name.