@gtkx/runtime 1.0.0-rc.4 → 1.1.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 (156) hide show
  1. package/README.md +5 -5
  2. package/dist/application-class.d.ts +37 -0
  3. package/dist/application-class.d.ts.map +1 -0
  4. package/dist/application-class.js +78 -0
  5. package/dist/application-class.js.map +1 -0
  6. package/dist/arg.d.ts +2 -1
  7. package/dist/arg.d.ts.map +1 -1
  8. package/dist/arg.js +2 -1
  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 +4 -3
  15. package/dist/callback.d.ts.map +1 -1
  16. package/dist/callback.js +124 -29
  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/default-application.d.ts +4 -0
  23. package/dist/default-application.d.ts.map +1 -0
  24. package/dist/default-application.js +17 -0
  25. package/dist/default-application.js.map +1 -0
  26. package/dist/descriptors.d.ts +98 -4
  27. package/dist/descriptors.d.ts.map +1 -1
  28. package/dist/descriptors.js +72 -6
  29. package/dist/descriptors.js.map +1 -1
  30. package/dist/error.d.ts +1 -0
  31. package/dist/error.d.ts.map +1 -1
  32. package/dist/error.js.map +1 -1
  33. package/dist/exit-hook.js +13 -0
  34. package/dist/exit-hook.js.map +1 -1
  35. package/dist/fn.d.ts +24 -2
  36. package/dist/fn.d.ts.map +1 -1
  37. package/dist/fn.js +67 -32
  38. package/dist/fn.js.map +1 -1
  39. package/dist/folded-lengths.d.ts +16 -0
  40. package/dist/folded-lengths.d.ts.map +1 -0
  41. package/dist/folded-lengths.js +38 -0
  42. package/dist/folded-lengths.js.map +1 -0
  43. package/dist/index.d.ts +14 -6
  44. package/dist/index.d.ts.map +1 -1
  45. package/dist/index.js +12 -4
  46. package/dist/index.js.map +1 -1
  47. package/dist/internal.d.ts +2 -0
  48. package/dist/internal.d.ts.map +1 -1
  49. package/dist/internal.js +2 -0
  50. package/dist/internal.js.map +1 -1
  51. package/dist/library.d.ts +3 -1
  52. package/dist/library.d.ts.map +1 -1
  53. package/dist/library.js +3 -1
  54. package/dist/library.js.map +1 -1
  55. package/dist/lifecycle.d.ts +58 -11
  56. package/dist/lifecycle.d.ts.map +1 -1
  57. package/dist/lifecycle.js +95 -22
  58. package/dist/lifecycle.js.map +1 -1
  59. package/dist/listeners.d.ts.map +1 -1
  60. package/dist/listeners.js +19 -5
  61. package/dist/listeners.js.map +1 -1
  62. package/dist/mixin.d.ts +3 -2
  63. package/dist/mixin.d.ts.map +1 -1
  64. package/dist/mixin.js +13 -1
  65. package/dist/mixin.js.map +1 -1
  66. package/dist/native-value.d.ts.map +1 -1
  67. package/dist/native-value.js +4 -3
  68. package/dist/native-value.js.map +1 -1
  69. package/dist/object.d.ts +40 -7
  70. package/dist/object.d.ts.map +1 -1
  71. package/dist/object.js +95 -26
  72. package/dist/object.js.map +1 -1
  73. package/dist/param-spec.d.ts +11 -0
  74. package/dist/param-spec.d.ts.map +1 -0
  75. package/dist/param-spec.js +147 -0
  76. package/dist/param-spec.js.map +1 -0
  77. package/dist/promisify.d.ts.map +1 -1
  78. package/dist/promisify.js +3 -2
  79. package/dist/promisify.js.map +1 -1
  80. package/dist/properties.d.ts +39 -7
  81. package/dist/properties.d.ts.map +1 -1
  82. package/dist/properties.js +310 -43
  83. package/dist/properties.js.map +1 -1
  84. package/dist/register-class.d.ts +147 -12
  85. package/dist/register-class.d.ts.map +1 -1
  86. package/dist/register-class.js +129 -83
  87. package/dist/register-class.js.map +1 -1
  88. package/dist/registry.d.ts +80 -21
  89. package/dist/registry.d.ts.map +1 -1
  90. package/dist/registry.js +86 -33
  91. package/dist/registry.js.map +1 -1
  92. package/dist/signal.d.ts +11 -4
  93. package/dist/signal.d.ts.map +1 -1
  94. package/dist/signal.js +85 -25
  95. package/dist/signal.js.map +1 -1
  96. package/dist/t.d.ts +2 -0
  97. package/dist/t.d.ts.map +1 -1
  98. package/dist/t.js +1 -0
  99. package/dist/t.js.map +1 -1
  100. package/dist/tuple.d.ts +6 -3
  101. package/dist/tuple.d.ts.map +1 -1
  102. package/dist/tuple.js +9 -7
  103. package/dist/tuple.js.map +1 -1
  104. package/dist/type.d.ts +2 -1
  105. package/dist/type.d.ts.map +1 -1
  106. package/dist/type.js +2 -0
  107. package/dist/type.js.map +1 -1
  108. package/dist/value.d.ts +10 -1
  109. package/dist/value.d.ts.map +1 -1
  110. package/dist/value.js +99 -24
  111. package/dist/value.js.map +1 -1
  112. package/dist/vfunc-call.d.ts +37 -0
  113. package/dist/vfunc-call.d.ts.map +1 -0
  114. package/dist/vfunc-call.js +172 -0
  115. package/dist/vfunc-call.js.map +1 -0
  116. package/dist/vfunc-seeds.d.ts +12 -0
  117. package/dist/vfunc-seeds.d.ts.map +1 -0
  118. package/dist/vfunc-seeds.js +19 -0
  119. package/dist/vfunc-seeds.js.map +1 -0
  120. package/dist/vfunc.d.ts +8 -0
  121. package/dist/vfunc.d.ts.map +1 -0
  122. package/dist/vfunc.js +29 -0
  123. package/dist/vfunc.js.map +1 -0
  124. package/package.json +14 -4
  125. package/src/application-class.ts +126 -0
  126. package/src/arg.ts +2 -1
  127. package/src/bind.ts +11 -0
  128. package/src/callback.ts +178 -34
  129. package/src/closure.ts +136 -0
  130. package/src/default-application.ts +21 -0
  131. package/src/descriptors.ts +150 -34
  132. package/src/error.ts +1 -0
  133. package/src/exit-hook.ts +18 -0
  134. package/src/fn.ts +107 -36
  135. package/src/folded-lengths.ts +64 -0
  136. package/src/index.ts +21 -6
  137. package/src/internal.ts +2 -0
  138. package/src/library.ts +3 -1
  139. package/src/lifecycle.ts +138 -25
  140. package/src/listeners.ts +25 -6
  141. package/src/mixin.ts +15 -2
  142. package/src/native-value.ts +5 -3
  143. package/src/object.ts +134 -35
  144. package/src/param-spec.ts +234 -0
  145. package/src/promisify.ts +4 -2
  146. package/src/properties.ts +473 -46
  147. package/src/register-class.ts +386 -114
  148. package/src/registry.ts +169 -49
  149. package/src/signal.ts +116 -42
  150. package/src/t.ts +3 -0
  151. package/src/tuple.ts +18 -12
  152. package/src/type.ts +3 -1
  153. package/src/value.ts +143 -23
  154. package/src/vfunc-call.ts +247 -0
  155. package/src/vfunc-seeds.ts +36 -0
  156. package/src/vfunc.ts +38 -0
package/src/fn.ts CHANGED
@@ -1,13 +1,14 @@
1
- import type { Descriptor, Ref } from "@gtkx/native";
2
- import { type Arg, isCallerAllocatedArg, isInoutArg, isOutputArg, isRefArg } from "./arg.js";
3
- import { bind } from "./bind.js";
1
+ import type { CallDescriptor, Descriptor, ExternalObject, Ref } from "@gtkx/native";
2
+ import { call, bind as nativeBind } from "@gtkx/native";
3
+ import type { RefSeeds } from "./vfunc-seeds.js";
4
+ import { type Arg, isCallerAllocatedArg, isOutputArg, isRefArg, requiresInputArg } from "./arg.js";
4
5
  import { wrapCallbackValue } from "./callback.js";
5
6
  import { boxedT, refT } from "./descriptors.js";
6
7
  import { checkError } from "./error.js";
7
8
  import { LIB } from "./library.js";
8
9
  import { fromNative } from "./native-value.js";
9
10
  import { getHandle } from "./registry.js";
10
- import { packTupleResult } from "./tuple.js";
11
+ import { hasSurfacedPrimary, packTupleResult } from "./tuple.js";
11
12
 
12
13
  /** The signature a native function is bound against. */
13
14
  type FnSpec = {
@@ -15,12 +16,24 @@ type FnSpec = {
15
16
  args: Arg[];
16
17
  /** Descriptor for the C return value, packed first when output arguments are also returned. */
17
18
  returns: Descriptor;
19
+ /**
20
+ * The callee returns a value the bindings do not surface, so the call is made against the
21
+ * declared return type and the value is dropped instead of being packed into the result.
22
+ */
23
+ isReturnSkipped?: boolean;
18
24
  /** The function takes a trailing `GError**`, whose contents are thrown as an error on return. */
19
25
  canThrow?: boolean;
26
+ /**
27
+ * How many of `args` precede the callee's ellipsis, marking it variadic. Omitting it binds a
28
+ * fixed-arity call, which passes the wrong argument classes to a variadic callee on some
29
+ * architectures.
30
+ */
31
+ fixedArgCount?: number;
20
32
  };
21
33
 
22
34
  type ArgSpec = {
23
35
  arg: Arg;
36
+ index: number;
24
37
  isRef: boolean;
25
38
  isCallerAllocated: boolean;
26
39
  requiresInput: boolean;
@@ -28,6 +41,8 @@ type ArgSpec = {
28
41
  isOutParam: boolean;
29
42
  };
30
43
 
44
+ const NO_OUT_PARAMS: unknown[] = [];
45
+
31
46
  const buildNativeArgTypes = (args: Arg[], canThrow: boolean): Descriptor[] => {
32
47
  const nativeArgTypes = args.map((argSpec) =>
33
48
  argSpec.direction !== undefined && argSpec.isCallerAllocated !== true ? refT(argSpec.type) : argSpec.type,
@@ -45,13 +60,14 @@ const buildNativeArgTypes = (args: Arg[], canThrow: boolean): Descriptor[] => {
45
60
  const buildArgSpecs = (args: Arg[]): ArgSpec[] => {
46
61
  let inputCursor = 0;
47
62
 
48
- return args.map((arg) => {
63
+ return args.map((arg, index) => {
49
64
  const isRef = isRefArg(arg);
50
- const requiresInput = !isRef || isInoutArg(arg);
65
+ const requiresInput = requiresInputArg(arg);
51
66
  const isOutParam = isOutputArg(arg) && arg.isConsumed !== true;
52
67
 
53
68
  return {
54
69
  arg,
70
+ index,
55
71
  isRef,
56
72
  isCallerAllocated: isCallerAllocatedArg(arg),
57
73
  requiresInput,
@@ -67,19 +83,19 @@ const resolveCallerAllocated = (inputs: unknown[], inputIndex: number): unknown
67
83
  return wrapper == null ? wrapper : getHandle(wrapper);
68
84
  };
69
85
 
70
- const buildRefValue = (requiresInput: boolean, inputs: unknown[], inputIndex: number): Ref => ({
71
- value: requiresInput ? inputs[inputIndex] : null,
86
+ const buildRefValue = (spec: ArgSpec, inputs: unknown[], seeds: RefSeeds | undefined): Ref => ({
87
+ value: spec.requiresInput ? inputs[spec.inputIndex] : (seeds?.get(spec.index) ?? null),
72
88
  });
73
89
 
74
- const buildNativeValue = (spec: ArgSpec, inputs: unknown[]): unknown => {
75
- const { arg, isRef, isCallerAllocated, requiresInput, inputIndex } = spec;
90
+ const buildNativeValue = (spec: ArgSpec, inputs: unknown[], seeds: RefSeeds | undefined): unknown => {
91
+ const { arg, isRef, isCallerAllocated, inputIndex } = spec;
76
92
 
77
93
  if (isCallerAllocated) {
78
94
  return resolveCallerAllocated(inputs, inputIndex);
79
95
  }
80
96
 
81
97
  if (isRef) {
82
- return buildRefValue(requiresInput, inputs, inputIndex);
98
+ return buildRefValue(spec, inputs, seeds);
83
99
  }
84
100
 
85
101
  if (arg.type.kind === "callback") {
@@ -89,44 +105,84 @@ const buildNativeValue = (spec: ArgSpec, inputs: unknown[]): unknown => {
89
105
  return inputs[inputIndex];
90
106
  };
91
107
 
92
- const buildNativeValues = (plans: ArgSpec[], inputs: unknown[]): unknown[] =>
93
- plans.map((plan) => buildNativeValue(plan, inputs));
108
+ const buildNativeValues = (plans: ArgSpec[], inputs: unknown[], seeds: RefSeeds | undefined): unknown[] =>
109
+ plans.map((plan) => buildNativeValue(plan, inputs, seeds));
110
+
111
+ const readOutParams = (outPlans: ArgSpec[], inputs: unknown[], nativeValues: unknown[]): unknown[] => {
112
+ if (outPlans.length === 0) {
113
+ return NO_OUT_PARAMS;
114
+ }
94
115
 
95
- const readOutParams = (plans: ArgSpec[], inputs: unknown[], nativeValues: unknown[]): unknown[] => {
96
- const outParams: unknown[] = [];
116
+ return Array.from(outPlans, (plan) =>
117
+ plan.isCallerAllocated
118
+ ? inputs[plan.inputIndex]
119
+ : fromNative(plan.arg.type, (nativeValues[plan.index] as Ref).value));
120
+ };
97
121
 
98
- for (const [index, { arg, isCallerAllocated, inputIndex, isOutParam }] of plans.entries()) {
99
- if (!isOutParam) {
100
- continue;
101
- }
122
+ const isPassThroughPlan = (plan: ArgSpec, index: number): boolean =>
123
+ plan.inputIndex === index &&
124
+ !plan.isRef &&
125
+ !plan.isCallerAllocated &&
126
+ plan.arg.type.kind !== "callback";
102
127
 
103
- outParams.push(
104
- isCallerAllocated ? inputs[inputIndex] : fromNative(arg.type, (nativeValues[index] as Ref).value),
105
- );
128
+ const resizeInputs = (inputs: unknown[], argCount: number): unknown[] => {
129
+ while (inputs.length < argCount) {
130
+ inputs.push(undefined);
106
131
  }
107
132
 
108
- return outParams;
133
+ if (inputs.length > argCount) {
134
+ inputs.length = argCount;
135
+ }
136
+
137
+ return inputs;
109
138
  };
110
139
 
111
- function fn(sharedLibrary: string, symbol: string, spec: FnSpec): (...inputs: unknown[]) => unknown {
140
+ const directCallable = (
141
+ descriptor: ExternalObject<CallDescriptor>,
142
+ returnDescriptor: Descriptor,
143
+ hasPrimary: boolean,
144
+ argCount: number,
145
+ ): ((...inputs: unknown[]) => unknown) => {
146
+ const marshal = (inputs: unknown[]): unknown =>
147
+ fromNative(returnDescriptor, call(descriptor, resizeInputs(inputs, argCount)));
148
+
149
+ if (hasPrimary) {
150
+ return (...inputs) => marshal(inputs);
151
+ }
152
+
153
+ return (...inputs) => {
154
+ marshal(inputs);
155
+ };
156
+ };
157
+
158
+ function fromNativeCallable(
159
+ descriptor: ExternalObject<CallDescriptor>,
160
+ spec: FnSpec,
161
+ getRefSeeds?: () => RefSeeds | undefined,
162
+ ): (...inputs: unknown[]) => unknown {
112
163
  const { args, returns: returnDescriptor, canThrow = false } = spec;
113
- const nativeArgTypes = buildNativeArgTypes(args, canThrow);
114
- const nativeFn = bind(sharedLibrary, symbol, nativeArgTypes, returnDescriptor);
115
- const hasPrimary = returnDescriptor.kind !== "void";
164
+ const hasPrimary = hasSurfacedPrimary(returnDescriptor, spec.isReturnSkipped);
116
165
  const plans = buildArgSpecs(args);
166
+ const outPlans = plans.filter((plan) => plan.isOutParam);
167
+ const arePassThrough = plans.every((plan, index) => isPassThroughPlan(plan, index));
117
168
 
118
- const shape = (inputs: unknown[], nativeValues: unknown[], nativeResult: unknown): unknown => {
119
- const primary = hasPrimary ? fromNative(returnDescriptor, nativeResult) : undefined;
169
+ if (!canThrow && arePassThrough) {
170
+ return directCallable(descriptor, returnDescriptor, hasPrimary, plans.length);
171
+ }
120
172
 
121
- return packTupleResult(readOutParams(plans, inputs, nativeValues), primary, hasPrimary);
122
- };
173
+ const shape = (inputs: unknown[], nativeValues: unknown[], nativeResult: unknown): unknown =>
174
+ packTupleResult(
175
+ readOutParams(outPlans, inputs, nativeValues),
176
+ fromNative(returnDescriptor, nativeResult),
177
+ hasPrimary,
178
+ );
123
179
 
124
180
  if (canThrow) {
125
181
  return (...inputs) => {
126
- const nativeValues = buildNativeValues(plans, inputs);
182
+ const nativeValues = buildNativeValues(plans, inputs, getRefSeeds?.());
127
183
  const errorRef: Ref = { value: null };
128
184
  nativeValues.push(errorRef);
129
- const nativeResult = nativeFn(...nativeValues);
185
+ const nativeResult = call(descriptor, nativeValues);
130
186
  checkError(errorRef);
131
187
 
132
188
  return shape(inputs, nativeValues, nativeResult);
@@ -134,10 +190,25 @@ function fn(sharedLibrary: string, symbol: string, spec: FnSpec): (...inputs: un
134
190
  }
135
191
 
136
192
  return (...inputs) => {
137
- const nativeValues = buildNativeValues(plans, inputs);
193
+ const nativeValues = buildNativeValues(plans, inputs, getRefSeeds?.());
138
194
 
139
- return shape(inputs, nativeValues, nativeFn(...nativeValues));
195
+ return shape(inputs, nativeValues, call(descriptor, nativeValues));
140
196
  };
141
197
  }
142
198
 
143
- export { fn };
199
+ /**
200
+ * Binds a symbol in a shared library to a callable that marshals its inputs and packs output
201
+ * arguments into the result. When the spec sets `canThrow`, the reported `GError` is thrown.
202
+ *
203
+ * @param sharedLibrary Shared library the symbol is looked up in.
204
+ * @param symbol Name of the C symbol to bind.
205
+ * @param spec Argument and return descriptors the call is marshalled through.
206
+ */
207
+ function fn(sharedLibrary: string, symbol: string, spec: FnSpec): (...inputs: unknown[]) => unknown {
208
+ const nativeArgTypes = buildNativeArgTypes(spec.args, spec.canThrow ?? false);
209
+ const descriptor = nativeBind(sharedLibrary, symbol, nativeArgTypes, spec.returns, spec.fixedArgCount);
210
+
211
+ return fromNativeCallable(descriptor, spec);
212
+ }
213
+
214
+ export { buildNativeArgTypes, fn, fromNativeCallable };
@@ -0,0 +1,64 @@
1
+ import type { Descriptor } from "@gtkx/native";
2
+
3
+ type LengthSource = { kind: "return" } | { kind: "outArg"; argIndex: number };
4
+ type LengthSources = Map<number, LengthSource[]>;
5
+
6
+ type FoldedLengthSpec = {
7
+ argDescriptors: Descriptor[];
8
+ returnDescriptor: Descriptor;
9
+ userDataIndex?: number | undefined;
10
+ };
11
+
12
+ const sizedArrayLengthIndex = (descriptor: Descriptor): number | undefined =>
13
+ descriptor.kind === "array" && descriptor.arrayKind === "sized" ? descriptor.sizeParamIndex : undefined;
14
+
15
+ const outArgLengthIndex = (descriptor: Descriptor | undefined): number | undefined =>
16
+ descriptor?.kind === "ref" ? sizedArrayLengthIndex(descriptor.innerDescriptor) : undefined;
17
+
18
+ const effectiveArgIndex = (argIndex: number | undefined, userDataIndex: number | undefined): number | undefined => {
19
+ if (argIndex === undefined || userDataIndex === undefined || argIndex < userDataIndex) {
20
+ return argIndex;
21
+ }
22
+
23
+ return argIndex === userDataIndex ? undefined : argIndex - 1;
24
+ };
25
+
26
+ const addLengthSource = (sources: LengthSources, index: number | undefined, source: LengthSource): void => {
27
+ if (index === undefined) {
28
+ return;
29
+ }
30
+
31
+ const existing = sources.get(index);
32
+
33
+ if (existing === undefined) {
34
+ sources.set(index, [source]);
35
+
36
+ return;
37
+ }
38
+
39
+ existing.push(source);
40
+ };
41
+
42
+ const addOutArgLengthSources = (sources: LengthSources, spec: FoldedLengthSpec): void => {
43
+ const { userDataIndex } = spec;
44
+
45
+ for (const [declaredIndex, descriptor] of spec.argDescriptors.entries()) {
46
+ const argIndex = effectiveArgIndex(declaredIndex, userDataIndex);
47
+
48
+ if (argIndex !== undefined) {
49
+ const lengthIndex = effectiveArgIndex(outArgLengthIndex(descriptor), userDataIndex);
50
+ addLengthSource(sources, lengthIndex, { kind: "outArg", argIndex });
51
+ }
52
+ }
53
+ };
54
+
55
+ const foldedLengthSources = (spec: FoldedLengthSpec): LengthSources => {
56
+ const sources: LengthSources = new Map();
57
+ const returnLengthIndex = effectiveArgIndex(sizedArrayLengthIndex(spec.returnDescriptor), spec.userDataIndex);
58
+ addLengthSource(sources, returnLengthIndex, { kind: "return" });
59
+ addOutArgLengthSources(sources, spec);
60
+
61
+ return sources;
62
+ };
63
+
64
+ export { foldedLengthSources, type LengthSource, type LengthSources };
package/src/index.ts CHANGED
@@ -1,9 +1,13 @@
1
1
  import "./exit-hook.js";
2
2
 
3
+ /** @public */
4
+ export { type ApplicationClass, type CommandLineApplication, createApplication } from "./application-class.js";
5
+ /** @public */
6
+ export { type ClosureCallback, ClosureMarshalError, toClosure, tryToClosure } from "./closure.js";
3
7
  /** @public */
4
8
  export { createErrorDomain, type ErrorDomain } from "./error.js";
5
9
  /** @public */
6
- export { onExit, quit, quitApplication, runApplication } from "./lifecycle.js";
10
+ export { onExit, quit, quitApplication, runApplication, type RunApplicationResult } from "./lifecycle.js";
7
11
  /** @public */
8
12
  export { offSignal, onceSignal, onSignal } from "./listeners.js";
9
13
  /** @public */
@@ -11,13 +15,21 @@ export { installMixins, type Mixin } from "./mixin.js";
11
15
  /** @public */
12
16
  export { fromNative, toNative } from "./native-value.js";
13
17
  /** @public */
14
- export { getObjectProperty, newObjectWithProperties, setObjectProperty } from "./object.js";
18
+ export {
19
+ type ConstructBinding,
20
+ type ConstructBindings,
21
+ getObjectProperty,
22
+ newObjectWithProperties,
23
+ registerConstructProperties,
24
+ setObjectProperty,
25
+ } from "./object.js";
15
26
  /** @public */
16
27
  export { promisify } from "./promisify.js";
17
28
  /** @public */
18
- export { registerClass } from "./register-class.js";
29
+ export { type Interface, registerClass } from "./register-class.js";
19
30
  /** @public */
20
31
  export {
32
+ getClassType,
21
33
  getHandle,
22
34
  getInstanceType,
23
35
  getWrapperClass,
@@ -25,11 +37,10 @@ export {
25
37
  registerWrapperClass,
26
38
  type StaticBase,
27
39
  setHandle,
28
- tryGetHandle,
29
40
  wrapHandle,
30
41
  } from "./registry.js";
31
42
  /** @public */
32
- export { connectSignal, emitSignal, getSignalBaseName, type SignalHandler } from "./signal.js";
43
+ export { connectSignal, disconnectSignal, emitSignal, getSignalBaseName, type SignalHandler } from "./signal.js";
33
44
  /** @public */
34
45
  export { t } from "./t.js";
35
46
  /** @public */
@@ -70,4 +81,8 @@ export {
70
81
  /** @public */
71
82
  export { getBoxedValue, setBoxedValue } from "./value.js";
72
83
  /** @public */
73
- export { alloc, type ExternalObject, type Handle, read, setWrapper, write } from "@gtkx/native";
84
+ export { callParent, callVfunc } from "./vfunc-call.js";
85
+ /** @public */
86
+ export { alloc, type ExternalObject, type Handle, read, write } from "@gtkx/native";
87
+ /** @public */
88
+ export { type AnyClass } from "@gtkx/utils";
package/src/internal.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  export { checkError, createErrorDomain } from "./error.js";
2
+ export { type ApplicationInstance, getApplicationInstance } from "./lifecycle.js";
2
3
  export { registerClassType, resolveWrapperClass, wrapHandle } from "./registry.js";
4
+ export { hasSignalListener } from "./signal.js";
3
5
  export { resolveType } from "./type.js";
4
6
  export {
5
7
  fromValue,
package/src/library.ts CHANGED
@@ -1,7 +1,9 @@
1
1
  import { type BoxedDescriptor, boxedT, type FundamentalDescriptor, fundamentalT } from "./descriptors.js";
2
2
 
3
3
  const LIB = "libgobject-2.0.so.0,libglib-2.0.so.0";
4
+ const GIO_LIB = "libgio-2.0.so.0";
4
5
  const VALUE_SIZE = 24;
6
+ const CLOSURE_SIZE = 32;
5
7
  const VALUE_T: BoxedDescriptor = boxedT("GValue", { sharedLibrary: LIB, getTypeFnName: "g_value_get_type" });
6
8
 
7
9
  const PARAM_T: FundamentalDescriptor = fundamentalT(LIB, "g_param_spec_ref", "g_param_spec_unref", {
@@ -14,4 +16,4 @@ const VARIANT_T: FundamentalDescriptor = fundamentalT(LIB, "g_variant_ref", "g_v
14
16
  typeName: "GVariant",
15
17
  });
16
18
 
17
- export { LIB, VALUE_SIZE, VALUE_T, PARAM_T, VARIANT_T };
19
+ export { CLOSURE_SIZE, GIO_LIB, LIB, VALUE_SIZE, VALUE_T, PARAM_T, VARIANT_T };
package/src/lifecycle.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { keepAlive, quit as nativeQuit } from "@gtkx/native";
2
- import { blockMatchedSignalHandlers } from "./signal.js";
2
+ import { isDerivedApplication, type LocalCommandLineApplication, shutDownThroughRun } from "./application-class.js";
3
+ import { claimDefaultApplication, releaseDefaultApplication } from "./default-application.js";
3
4
 
4
5
  /**
5
6
  * The GIO and GTK application surface {@link runApplication} and {@link quitApplication} drive, so any
@@ -9,13 +10,16 @@ import { blockMatchedSignalHandlers } from "./signal.js";
9
10
  type ApplicationLike = {
10
11
  /** Returns whether the application has already been registered. */
11
12
  getIsRegistered(): boolean;
13
+ /** Returns whether another process owns the application ID, making this one a remote instance. */
14
+ getIsRemote?(): boolean;
12
15
  /** Registers the application with the session, returning whether it succeeded. */
13
16
  register(cancellable: null): boolean;
14
17
  /** Emits `activate`, bringing up the application's initial user interface. */
15
18
  activate(): void;
16
- /** Quits the application, so `run` returns at the next main-loop iteration after `shutdown` runs. */
17
- quit(): void;
18
- /** Runs the application's main loop until it shuts down and returns the exit status. */
19
+ /**
20
+ * Runs GLib's own `g_application_run`, whose tail emits `shutdown`, destroys the application
21
+ * implementation and clears the registration.
22
+ */
19
23
  run(argv: string[]): number;
20
24
  /** Returns the windows currently attached to the application. */
21
25
  getWindows?(): object[];
@@ -23,9 +27,24 @@ type ApplicationLike = {
23
27
  removeWindow?(window: object): void;
24
28
  /** Connects a handler to the application's `activate` or `shutdown` signal. */
25
29
  on(signal: "activate" | "shutdown", handler: () => void): unknown;
30
+ /** Emits one of the application's own signals. */
31
+ emit(signal: "shutdown"): unknown;
32
+ };
33
+
34
+ type ApplicationInstance = "primary" | "remote" | "shutDown" | "unregistered";
35
+
36
+ /** What {@link runApplication} reports about the process it just started. */
37
+ type RunApplicationResult = {
38
+ /** Whether this process owns the application ID and may build a user interface. */
39
+ isPrimary: boolean;
40
+ /** The status GLib determined for the command line, which the process should exit with. */
41
+ exitStatus: number;
26
42
  };
27
43
 
28
44
  const shutdownCallbacks: (() => void)[] = [];
45
+ const startedApplications: WeakSet<object> = new WeakSet();
46
+ const registeredApplications: WeakSet<object> = new WeakSet();
47
+ const shutDownApplications: WeakSet<object> = new WeakSet();
29
48
  /**
30
49
  * Runs every registered exit callback and shuts down the native runtime. Safe to
31
50
  * call more than once; only the first call takes effect.
@@ -60,13 +79,9 @@ const onExit = (callback: () => void): void => {
60
79
  shutdownCallbacks.push(callback);
61
80
  };
62
81
 
63
- /**
64
- * Registers the application if needed and activates it, keeping the runtime alive
65
- * while it is active and releasing it on shutdown.
66
- *
67
- * @param application The application to register and activate.
68
- */
69
- const runApplication = (application: ApplicationLike): void => {
82
+ const startApplication = (application: ApplicationLike & LocalCommandLineApplication, argv: string[]): number => {
83
+ startedApplications.add(application);
84
+
70
85
  application.on("activate", () => {
71
86
  keepAlive(true);
72
87
  });
@@ -75,22 +90,90 @@ const runApplication = (application: ApplicationLike): void => {
75
90
  keepAlive(false);
76
91
  });
77
92
 
78
- if (!application.getIsRegistered()) {
79
- application.register(null);
93
+ return application.runLocalCommandLine(argv)[2];
94
+ };
95
+
96
+ const restartApplication = (application: ApplicationLike): number => {
97
+ shutDownApplications.delete(application);
98
+
99
+ if (!application.register(null)) {
100
+ return 1;
80
101
  }
81
102
 
82
103
  application.activate();
104
+
105
+ return 0;
106
+ };
107
+
108
+ const getApplicationInstance = (application: ApplicationLike): ApplicationInstance => {
109
+ if (!application.getIsRegistered()) {
110
+ return registeredApplications.has(application) ? "shutDown" : "unregistered";
111
+ }
112
+
113
+ return application.getIsRemote?.() === true ? "remote" : "primary";
83
114
  };
84
115
 
85
116
  /**
86
- * Detaches every window from the application and runs its main loop until it shuts
87
- * down, so pending shutdown work completes before the process exits. Does nothing
88
- * when the application was never registered.
117
+ * Hands `argv` to GLib's own command line handling, which parses the application's options, prints
118
+ * `--help`, runs `handle-local-options`, registers the application, and either activates it or
119
+ * forwards the command line to the process that already owns the application ID. The runtime is
120
+ * held alive while the application is active and released on shutdown.
89
121
  *
90
- * @param application The application to shut down.
122
+ * `g_application_run()` is not what starts the application, because it would drive its own main loop
123
+ * and freeze Node; only the local command line handling it delegates to runs here. {@link
124
+ * quitApplication} calls it once no window holds the application open, to reach the teardown only it
125
+ * performs.
126
+ *
127
+ * GLib parses a given application's command line at most once, so starting an application that has
128
+ * already run registers and activates it instead of reading `argv` again.
129
+ *
130
+ * When another process already owns the application ID, this one registers as a remote instance: no
131
+ * user interface may be built here, because a remote application has no `GtkApplicationImpl` and
132
+ * attaching a window to it crashes.
133
+ *
134
+ * The application also claims the process-wide default `Gio.Application.getDefault()` returns, before
135
+ * its command line is parsed, so an `activate` handler and anything the host reads afterwards see the
136
+ * application being started rather than an earlier one. That holds whether the start ends registered,
137
+ * remote or unregistered: a refused command line still has to be reportable through the default, which
138
+ * is how `gtkx dev` learns which application its entry mounted. {@link quitApplication} gives the
139
+ * default back up, since GLib drops it only at finalize.
140
+ *
141
+ * @param application The application to start.
142
+ * @param argv The command line, whose first entry names the program as `--help` should print it.
143
+ * @returns Whether this process may build a user interface, and the status to exit with.
91
144
  */
92
- const quitApplication = (application: ApplicationLike): void => {
93
- if (!application.getIsRegistered()) {
145
+ const runApplication = (application: ApplicationLike, argv: string[]): RunApplicationResult => {
146
+ if (!isDerivedApplication(application)) {
147
+ throw new Error(
148
+ "runApplication: this application was not built by GTKX, so its command line cannot be " +
149
+ "parsed and it cannot be shut down safely; render <GtkApplication> or <AdwApplication>, " +
150
+ "or construct it with createApplication from @gtkx/runtime",
151
+ );
152
+ }
153
+
154
+ claimDefaultApplication(application);
155
+
156
+ const exitStatus = startedApplications.has(application)
157
+ ? restartApplication(application)
158
+ : startApplication(application, argv);
159
+
160
+ const instance = getApplicationInstance(application);
161
+
162
+ if (instance !== "unregistered") {
163
+ registeredApplications.add(application);
164
+ }
165
+
166
+ const isPrimary = instance === "primary";
167
+
168
+ if (isPrimary) {
169
+ keepAlive(true);
170
+ }
171
+
172
+ return { isPrimary, exitStatus };
173
+ };
174
+
175
+ const tearDownApplication = (application: ApplicationLike): void => {
176
+ if (!application.getIsRegistered() || shutDownApplications.has(application)) {
94
177
  return;
95
178
  }
96
179
 
@@ -100,12 +183,42 @@ const quitApplication = (application: ApplicationLike): void => {
100
183
  application.removeWindow?.(window);
101
184
  }
102
185
 
103
- application.on("shutdown", () => {
104
- application.quit();
105
- });
186
+ shutDownThroughRun(application);
187
+
188
+ if (application.getIsRegistered()) {
189
+ shutDownApplications.add(application);
190
+ application.emit("shutdown");
191
+ }
192
+ };
106
193
 
107
- blockMatchedSignalHandlers(application, "activate");
108
- application.run([]);
194
+ /**
195
+ * Detaches every window from the application and runs GLib's own shutdown, which emits `shutdown`,
196
+ * destroys the application implementation and releases the D-Bus registration. Does nothing for an
197
+ * application that is not registered, so a repeated call is a no-op.
198
+ *
199
+ * GLib's own shutdown is reachable once per application: reaching it marks the application as
200
+ * quitting, which GLib never undoes. An application that has already quit falls back to emitting
201
+ * `shutdown`, which releases the runtime and leaves the registration for GLib to drop at finalize.
202
+ *
203
+ * The process-wide default {@link runApplication} claimed is given up whichever of those paths the
204
+ * application took, including the ones that have no teardown left to do, so
205
+ * `Gio.Application.getDefault()` never hands back an application that has been torn down or that
206
+ * never registered in the first place. GLib clears that default only at finalize, which a
207
+ * garbage-collected wrapper reaches arbitrarily late or never.
208
+ *
209
+ * @param application The application to shut down.
210
+ */
211
+ const quitApplication = (application: ApplicationLike): void => {
212
+ tearDownApplication(application);
213
+ releaseDefaultApplication(application);
109
214
  };
110
215
 
111
- export { onExit, quit, runApplication, quitApplication, type ApplicationLike };
216
+ export {
217
+ getApplicationInstance,
218
+ onExit,
219
+ quit,
220
+ runApplication,
221
+ quitApplication,
222
+ type ApplicationInstance,
223
+ type RunApplicationResult,
224
+ };
package/src/listeners.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { isSignalHandlerConnected, type SignalHandler } from "./signal.js";
1
+ import { isSignalHandlerConnected, type SignalHandler, untrackConnection } from "./signal.js";
2
2
 
3
3
  /**
4
4
  * The connect and disconnect surface {@link onSignal}, {@link onceSignal} and {@link offSignal}
@@ -11,10 +11,10 @@ type SignalConnectable = {
11
11
  disconnect(handlerId: number): void;
12
12
  };
13
13
 
14
- const listenerTable: WeakMap<object, Map<string, Map<SignalHandler, number>>> = new WeakMap();
14
+ const listenerTable: WeakMap<object, Map<string, Map<SignalHandler, number[]>>> = new WeakMap();
15
15
 
16
16
  const findListenerHandlerId = (instance: object, signal: string, handler: SignalHandler): number | undefined =>
17
- listenerTable.get(instance)?.get(signal)?.get(handler);
17
+ listenerTable.get(instance)?.get(signal)?.get(handler)?.at(-1);
18
18
 
19
19
  const trackListener = (instance: object, signal: string, handler: SignalHandler, handlerId: number): void => {
20
20
  let bySignal = listenerTable.get(instance);
@@ -31,7 +31,24 @@ const trackListener = (instance: object, signal: string, handler: SignalHandler,
31
31
  bySignal.set(signal, byHandler);
32
32
  }
33
33
 
34
- byHandler.set(handler, handlerId);
34
+ let handlerIds = byHandler.get(handler);
35
+
36
+ if (!handlerIds) {
37
+ handlerIds = [];
38
+ byHandler.set(handler, handlerIds);
39
+ }
40
+
41
+ handlerIds.push(handlerId);
42
+ };
43
+
44
+ const removeTrackedHandlerId = (handlerIds: number[], handlerId: number): number => {
45
+ const index = handlerIds.lastIndexOf(handlerId);
46
+
47
+ if (index !== -1) {
48
+ handlerIds.splice(index, 1);
49
+ }
50
+
51
+ return handlerIds.length;
35
52
  };
36
53
 
37
54
  const untrackHandlerId = (instance: object, signal: string, handlerId: number): void => {
@@ -42,8 +59,8 @@ const untrackHandlerId = (instance: object, signal: string, handlerId: number):
42
59
  return;
43
60
  }
44
61
 
45
- for (const [handler, id] of byHandler) {
46
- if (id === handlerId) {
62
+ for (const [handler, handlerIds] of byHandler) {
63
+ if (removeTrackedHandlerId(handlerIds, handlerId) === 0) {
47
64
  byHandler.delete(handler);
48
65
  }
49
66
  }
@@ -81,6 +98,7 @@ function onceSignal(instance: SignalConnectable, signal: string, handler: Signal
81
98
 
82
99
  const wrapped: SignalHandler = (...args) => {
83
100
  untrackHandlerId(instance, signal, handlerId);
101
+ untrackConnection(instance, handlerId);
84
102
  instance.disconnect(handlerId);
85
103
 
86
104
  return handler(...args);
@@ -107,6 +125,7 @@ function offSignal(instance: SignalConnectable, signal: string, handler: SignalH
107
125
  }
108
126
 
109
127
  untrackHandlerId(instance, signal, handlerId);
128
+ untrackConnection(instance, handlerId);
110
129
 
111
130
  if (!isSignalHandlerConnected(instance, handlerId)) {
112
131
  return;