node-gtk 0.14.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,237 @@
1
+ /*
2
+ * build-test-fixtures.js
3
+ *
4
+ * Makes the gobject-introspection test libraries (Utility, GIMarshallingTests,
5
+ * Regress) available for node-gtk's test suite. These libraries systematically
6
+ * exercise marshalling of every GObject type in every direction
7
+ * (in/out/inout/return), so they let us test node-gtk's type conversions
8
+ * exhaustively instead of ad-hoc.
9
+ *
10
+ * Strategy: always build from a single pinned upstream source revision, on
11
+ * every platform. Distro-shipped copies (the gobject-introspection package's
12
+ * bundled tests, or prebuilt gjs `installed-tests` typelibs) vary wildly by
13
+ * version — functions and types present on one machine are absent on another —
14
+ * which makes the test suite non-portable. Pinning one revision of the
15
+ * canonical `gobject-introspection-tests` repo and compiling it ourselves gives
16
+ * every machine (dev, Linux CI, macOS CI) the exact same API surface.
17
+ *
18
+ * The pinned sources are downloaded once (as a tarball) and cached under
19
+ * tests/gi-fixtures/.src/; the compiled output goes to tests/gi-fixtures/
20
+ * ({NAME}-1.0.typelib + lib{name}.so). Both are git-ignored.
21
+ *
22
+ * Run directly (`node scripts/build-test-fixtures.js`) or via `npm test`
23
+ * (it runs as a pretest step). Idempotent: existing fixtures are reused unless
24
+ * --force is passed. To bump the upstream revision, change SOURCE_REF.
25
+ */
26
+
27
+ const fs = require('fs')
28
+ const path = require('path')
29
+ const { execFileSync, execSync } = require('child_process')
30
+
31
+ const FIXTURES_DIR = path.join(__dirname, '..', 'tests', 'gi-fixtures')
32
+ const SRC_CACHE_DIR = path.join(FIXTURES_DIR, '.src')
33
+ const FORCE = process.argv.includes('--force')
34
+ const VERBOSE = process.argv.includes('--verbose') || process.env.VERBOSE
35
+
36
+ // Canonical upstream test sources, pinned to a specific revision so every
37
+ // machine builds the identical API. Bump SOURCE_REF to update.
38
+ const SOURCE_REPO = 'https://gitlab.gnome.org/GNOME/gobject-introspection-tests'
39
+ const SOURCE_REF = '5987255086f59ca271a3a0aa53fbbb15b189be65'
40
+
41
+ // Each fixture mirrors the upstream meson.build recipe: the GI namespace, its
42
+ // shared-library basename, the source/header files that make it up, the
43
+ // pkg-config packages it links, and the GI namespaces its typelib includes.
44
+ // Order matters: Regress's typelib includes Utility, so Utility is built first.
45
+ const FIXTURES = [
46
+ {
47
+ namespace: 'Utility',
48
+ library: 'utility',
49
+ identifierPrefix: 'Utility',
50
+ symbolPrefix: 'utility_',
51
+ sources: ['utility.c'],
52
+ headers: ['utility.h'],
53
+ packages: ['gobject-2.0'],
54
+ includes: ['GObject-2.0'],
55
+ },
56
+ {
57
+ namespace: 'GIMarshallingTests',
58
+ library: 'gimarshallingtests',
59
+ identifierPrefix: 'GIMarshallingTests',
60
+ symbolPrefix: 'gi_marshalling_tests_',
61
+ sources: ['gimarshallingtests.c', 'gimarshallingtestsextra.c'],
62
+ headers: ['gimarshallingtests.h', 'gimarshallingtestsextra.h'],
63
+ packages: ['gobject-2.0', 'gio-2.0'],
64
+ includes: ['Gio-2.0'],
65
+ },
66
+ {
67
+ namespace: 'Regress',
68
+ library: 'regress',
69
+ identifierPrefix: 'Regress',
70
+ symbolPrefix: 'regress_',
71
+ sources: ['annotation.c', 'drawable.c', 'foo.c', 'regress.c', 'regressextra.c'],
72
+ headers: ['annotation.h', 'drawable.h', 'foo.h', 'regress.h', 'regressextra.h'],
73
+ packages: ['gobject-2.0', 'gio-2.0', 'cairo', 'cairo-gobject'],
74
+ includes: ['Gio-2.0', 'cairo-1.0', 'Utility-1.0'],
75
+ },
76
+ ]
77
+
78
+ function log(...args) {
79
+ console.log('[fixtures]', ...args)
80
+ }
81
+ function vlog(...args) {
82
+ if (VERBOSE) console.log('[fixtures]', ...args)
83
+ }
84
+
85
+ function pkgConfig(args) {
86
+ return execSync(`pkg-config ${args}`, { encoding: 'utf8' }).trim()
87
+ }
88
+
89
+ function which(bin) {
90
+ try {
91
+ return execFileSync('sh', ['-c', `command -v ${bin}`], { encoding: 'utf8' }).trim()
92
+ } catch (e) {
93
+ return null
94
+ }
95
+ }
96
+
97
+ function fixtureIsPresent(fixture) {
98
+ const typelib = path.join(FIXTURES_DIR, `${fixture.namespace}-1.0.typelib`)
99
+ const lib = path.join(FIXTURES_DIR, `lib${fixture.library}.so`)
100
+ return fs.existsSync(typelib) && fs.existsSync(lib)
101
+ }
102
+
103
+ // Download + extract the pinned upstream sources once; return the dir holding
104
+ // the .c/.h files. Cached under SRC_CACHE_DIR/<ref>, keyed by revision.
105
+ function ensureSources() {
106
+ const destDir = path.join(SRC_CACHE_DIR, SOURCE_REF)
107
+ // The repo's files live at the tarball root; a marker file confirms a
108
+ // complete previous extraction.
109
+ if (fs.existsSync(path.join(destDir, 'gimarshallingtests.c'))) {
110
+ vlog(`sources present at ${destDir}`)
111
+ return destDir
112
+ }
113
+
114
+ fs.mkdirSync(SRC_CACHE_DIR, { recursive: true })
115
+ const tarball = path.join(SRC_CACHE_DIR, `${SOURCE_REF}.tar.gz`)
116
+ const url = `${SOURCE_REPO}/-/archive/${SOURCE_REF}/src-${SOURCE_REF}.tar.gz`
117
+
118
+ log(`downloading test sources @ ${SOURCE_REF.slice(0, 12)}`)
119
+ execFileSync('curl', ['-fsSL', url, '-o', tarball], { stdio: VERBOSE ? 'inherit' : 'pipe' })
120
+
121
+ // The tarball extracts to a single top-level dir; strip it into destDir.
122
+ fs.mkdirSync(destDir, { recursive: true })
123
+ execFileSync('tar', ['xzf', tarball, '-C', destDir, '--strip-components=1'],
124
+ { stdio: VERBOSE ? 'inherit' : 'pipe' })
125
+ fs.unlinkSync(tarball)
126
+ return destDir
127
+ }
128
+
129
+ function buildFixture(fixture, srcDir, tools) {
130
+ const sources = fixture.sources.map(s => path.join(srcDir, s))
131
+ const headers = fixture.headers.map(h => path.join(srcDir, h))
132
+ if (![...sources, ...headers].every(fs.existsSync)) {
133
+ throw new Error(`sources for ${fixture.namespace} missing in ${srcDir}`)
134
+ }
135
+
136
+ const cflags = pkgConfig(`--cflags ${fixture.packages.join(' ')}`)
137
+ const libs = pkgConfig(`--libs ${fixture.packages.join(' ')}`)
138
+ const libPath = path.join(FIXTURES_DIR, `lib${fixture.library}.so`)
139
+ const girPath = path.join(FIXTURES_DIR, `${fixture.namespace}-1.0.gir`)
140
+ const typelibPath = path.join(FIXTURES_DIR, `${fixture.namespace}-1.0.typelib`)
141
+
142
+ // 1. shared library
143
+ const cc = process.env.CC || 'cc'
144
+ const compileCmd =
145
+ `${cc} -shared -fPIC -I"${srcDir}" ${cflags} ` +
146
+ `${sources.map(s => `"${s}"`).join(' ')} ${libs} -o "${libPath}"`
147
+ vlog(compileCmd)
148
+ execSync(compileCmd, { stdio: VERBOSE ? 'inherit' : 'pipe' })
149
+
150
+ // 2. introspection data (.gir)
151
+ const scanArgs = [
152
+ ...sources, ...headers,
153
+ '--warn-all',
154
+ '--namespace', fixture.namespace,
155
+ '--nsversion', '1.0',
156
+ '--identifier-prefix', fixture.identifierPrefix,
157
+ '--symbol-prefix', fixture.symbolPrefix,
158
+ '--library', fixture.library,
159
+ '--library-path', FIXTURES_DIR,
160
+ // So that --include of an already-built local fixture (e.g. Utility-1.0,
161
+ // which Regress depends on) resolves its .gir from our output dir.
162
+ '--add-include-path', FIXTURES_DIR,
163
+ ...fixture.includes.flatMap(i => ['--include', i]),
164
+ ...fixture.packages.flatMap(p => ['--pkg', p]),
165
+ '--cflags-begin', ...cflags.split(/\s+/).filter(Boolean), `-I${srcDir}`, '--cflags-end',
166
+ '--output', girPath,
167
+ ]
168
+ vlog(tools.scanner, scanArgs.join(' '))
169
+ execFileSync(tools.scanner, scanArgs, {
170
+ stdio: VERBOSE ? 'inherit' : 'pipe',
171
+ env: { ...process.env, LD_LIBRARY_PATH: `${FIXTURES_DIR}:${process.env.LD_LIBRARY_PATH || ''}` },
172
+ })
173
+
174
+ // 3. compiled typelib (--includedir resolves locally-built included girs)
175
+ execFileSync(tools.compiler, [girPath, '--includedir', FIXTURES_DIR, '--output', typelibPath], {
176
+ stdio: VERBOSE ? 'inherit' : 'pipe',
177
+ })
178
+
179
+ log(`built ${fixture.namespace}`)
180
+ return true
181
+ }
182
+
183
+ function main() {
184
+ fs.mkdirSync(FIXTURES_DIR, { recursive: true })
185
+
186
+ const allPresent = FIXTURES.every(fixtureIsPresent)
187
+ if (!FORCE && allPresent) {
188
+ vlog('all fixtures already present, skipping (use --force to rebuild)')
189
+ log(`available fixtures: ${FIXTURES.map(f => f.namespace).join(', ')}`)
190
+ return
191
+ }
192
+
193
+ const tools = {
194
+ scanner: which('g-ir-scanner'),
195
+ compiler: which('g-ir-compiler'),
196
+ }
197
+ if (!tools.scanner || !tools.compiler || !which('curl') || !which('tar')) {
198
+ log('WARNING: g-ir-scanner/g-ir-compiler/curl/tar not all available; ' +
199
+ 'cannot build fixtures. Dependent tests will skip.')
200
+ return
201
+ }
202
+
203
+ let srcDir
204
+ try {
205
+ srcDir = ensureSources()
206
+ } catch (e) {
207
+ log(`WARNING: could not fetch test sources: ${e.message.split('\n')[0]}`)
208
+ log('dependent tests will skip')
209
+ return
210
+ }
211
+
212
+ const results = []
213
+ for (const fixture of FIXTURES) {
214
+ if (!FORCE && fixtureIsPresent(fixture)) {
215
+ vlog(`${fixture.namespace} already present, skipping`)
216
+ results.push({ fixture, ok: true })
217
+ continue
218
+ }
219
+ let ok = false
220
+ try {
221
+ ok = buildFixture(fixture, srcDir, tools)
222
+ } catch (e) {
223
+ log(`failed to build ${fixture.namespace}: ${e.message.split('\n')[0]}`)
224
+ }
225
+ if (!ok)
226
+ log(`WARNING: could not provide ${fixture.namespace}; dependent tests will skip`)
227
+ results.push({ fixture, ok })
228
+ }
229
+
230
+ const provided = results.filter(r => r.ok).map(r => r.fixture.namespace)
231
+ log(`available fixtures: ${provided.length ? provided.join(', ') : '(none)'}`)
232
+ }
233
+
234
+ if (require.main === module)
235
+ main()
236
+
237
+ module.exports = { FIXTURES_DIR, FIXTURES }
package/scripts/ci.sh CHANGED
@@ -41,13 +41,17 @@ function npm_test() {
41
41
  echo "### Running tests ###";
42
42
 
43
43
  if [[ $(uname -s) == 'Darwin' ]]; then
44
- export GST_PLUGIN_SYSTEM_PATH=/usr/local/lib/gstreamer-1.0;
44
+ export GST_PLUGIN_SYSTEM_PATH=$(brew --prefix gstreamer)/lib/gstreamer-1.0;
45
+ # This branch calls mocha directly (not `npm test`), so the pretest
46
+ # fixture build does not run automatically; do it here. Best-effort:
47
+ # marshalling tests skip if fixtures cannot be produced on macOS.
48
+ npm run build:test-fixtures || true;
45
49
  npx mocha \
46
50
  --skip=callback \
47
- --skip=error \
48
51
  tests/__run__.js
49
52
  else
50
- xvfb-run -a npm test -- --skip=callback;
53
+ xvfb-run -a npm test -- \
54
+ --skip=callback;
51
55
  fi;
52
56
  }
53
57
 
package/src/boxed.cc CHANGED
@@ -43,6 +43,17 @@ size_t Boxed::GetSize (GIBaseInfo *boxed_info) {
43
43
  }
44
44
  }
45
45
 
46
+ gpointer AllocateBoxed (GType gtype, size_t size) {
47
+ // Registered boxed types are freed via g_boxed_free, which by GLib
48
+ // convention uses g_slice_free; match that with g_slice so freeing doesn't
49
+ // corrupt the slice allocator (#290, #213). Non-registered structs are
50
+ // freed with g_free, so allocate them with g_malloc0.
51
+ if (G_TYPE_IS_BOXED(gtype))
52
+ return g_slice_alloc0(size);
53
+ else
54
+ return g_malloc0(size);
55
+ }
56
+
46
57
  static bool IsNoArgsConstructor (GIFunctionInfo *info) {
47
58
  auto flags = g_function_info_get_flags (info);
48
59
  return ((flags & GI_FUNCTION_IS_CONSTRUCTOR) != 0
@@ -162,7 +173,7 @@ static void BoxedConstructor(const Nan::FunctionCallbackInfo<Value> &info) {
162
173
  return;
163
174
  }
164
175
 
165
- GIFunctionInfo* constructorInfo = NULL;
176
+ int n_constructor_args = -1;
166
177
 
167
178
  void *boxed = NULL;
168
179
  unsigned long size = 0;
@@ -174,11 +185,11 @@ static void BoxedConstructor(const Nan::FunctionCallbackInfo<Value> &info) {
174
185
 
175
186
  if (info[0]->IsExternal ()) {
176
187
  /* The External case. This is how WrapperFromBoxed is called. */
177
- bool mustCopy = Nan::To<bool> (info[1]).ToChecked();
188
+ auto ownership = (ResourceOwnership) Nan::To<int32_t> (info[1]).ToChecked();
178
189
 
179
190
  boxed = External::Cast(*info[0])->Value();
180
191
 
181
- if (mustCopy) {
192
+ if (ownership == kCopy) {
182
193
  if (gtype != G_TYPE_NONE) {
183
194
  boxed = g_boxed_copy (gtype, boxed);
184
195
  }
@@ -192,7 +203,7 @@ static void BoxedConstructor(const Nan::FunctionCallbackInfo<Value> &info) {
192
203
  }
193
204
  }
194
205
  else {
195
- owns_memory = false;
206
+ owns_memory = ownership == kTransfer;
196
207
  }
197
208
  } else {
198
209
  /* User code calling `new Pango.AttrList()` */
@@ -201,6 +212,8 @@ static void BoxedConstructor(const Nan::FunctionCallbackInfo<Value> &info) {
201
212
 
202
213
  if (constructorInfo != NULL) {
203
214
 
215
+ n_constructor_args = g_callable_info_get_n_args(constructorInfo);
216
+
204
217
  FunctionInfo func(constructorInfo);
205
218
  GIArgument return_value;
206
219
  GError *error = NULL;
@@ -223,7 +236,7 @@ static void BoxedConstructor(const Nan::FunctionCallbackInfo<Value> &info) {
223
236
  boxed = return_value.v_pointer;
224
237
 
225
238
  } else if ((size = Boxed::GetSize(gi_info)) != 0) {
226
- boxed = g_malloc0(size);
239
+ boxed = AllocateBoxed(gtype, size);
227
240
 
228
241
  } else {
229
242
  Nan::ThrowError("Boxed allocation failed: no constructor found");
@@ -250,7 +263,7 @@ static void BoxedConstructor(const Nan::FunctionCallbackInfo<Value> &info) {
250
263
 
251
264
  SET_OBJECT_GTYPE (self, gtype);
252
265
 
253
- if (constructorInfo == NULL || g_callable_info_get_n_args(constructorInfo) == 0)
266
+ if (n_constructor_args <= 0)
254
267
  InitBoxedFromObject(self, info[0]);
255
268
  }
256
269
 
@@ -286,12 +299,12 @@ static void BoxedDestroyed(const Nan::WeakCallbackInfo<Boxed> &info) {
286
299
  delete box;
287
300
  }
288
301
 
289
- static void BoxedClassDestroyed(const v8::WeakCallbackInfo<GIBaseInfo> &info) {
302
+ static void BoxedClassDestroyed(const Nan::WeakCallbackInfo<GIBaseInfo> &info) {
290
303
  GIBaseInfo *gi_info = info.GetParameter ();
291
304
  GType gtype = g_registered_type_info_get_g_type ((GIRegisteredTypeInfo *) gi_info);
292
305
 
293
- auto *persistent_template = (Persistent<FunctionTemplate> *) g_type_get_qdata (gtype, GNodeJS::template_quark());
294
- auto *persistent_function = (Persistent<FunctionTemplate> *) g_type_get_qdata (gtype, GNodeJS::function_quark());
306
+ auto *persistent_template = (Nan::Persistent<FunctionTemplate> *) g_type_get_qdata (gtype, GNodeJS::template_quark());
307
+ auto *persistent_function = (Nan::Persistent<FunctionTemplate> *) g_type_get_qdata (gtype, GNodeJS::function_quark());
295
308
  delete persistent_template;
296
309
  delete persistent_function;
297
310
 
@@ -324,7 +337,7 @@ Local<FunctionTemplate> GetBoxedTemplate(GIBaseInfo *info, GType gtype) {
324
337
  */
325
338
 
326
339
  if (data) {
327
- auto *persistent = (Persistent<FunctionTemplate> *) data;
340
+ auto *persistent = (Nan::Persistent<FunctionTemplate> *) data;
328
341
  auto tpl = Nan::New<FunctionTemplate> (*persistent);
329
342
  return tpl;
330
343
  }
@@ -353,7 +366,7 @@ Local<FunctionTemplate> GetBoxedTemplate(GIBaseInfo *info, GType gtype) {
353
366
  if (gtype == G_TYPE_NONE)
354
367
  return tpl;
355
368
 
356
- auto *persistent = new Persistent<FunctionTemplate>(Isolate::GetCurrent(), tpl);
369
+ auto *persistent = new Nan::Persistent<FunctionTemplate>(tpl);
357
370
  persistent->SetWeak(
358
371
  g_base_info_ref(info),
359
372
  BoxedClassDestroyed,
@@ -380,7 +393,7 @@ Local<Function> GetBoxedFunction(GIBaseInfo *info, GType gtype) {
380
393
  }
381
394
 
382
395
  if (data) {
383
- auto *persistent = (Persistent<Function> *) data;
396
+ auto *persistent = (Nan::Persistent<Function> *) data;
384
397
  auto fn = Nan::New<Function> (*persistent);
385
398
  return fn;
386
399
  }
@@ -391,7 +404,7 @@ Local<Function> GetBoxedFunction(GIBaseInfo *info, GType gtype) {
391
404
  if (gtype == G_TYPE_NONE)
392
405
  return fn;
393
406
 
394
- auto *persistent = new Persistent<Function>(Isolate::GetCurrent(), fn);
407
+ auto *persistent = new Nan::Persistent<Function>(fn);
395
408
 
396
409
  g_type_set_qdata(gtype, GNodeJS::function_quark(), persistent);
397
410
 
@@ -419,15 +432,15 @@ Local<Function> MakeBoxedClass(GIBaseInfo *info) {
419
432
  return GetBoxedFunction (info, gtype);
420
433
  }
421
434
 
422
- Local<Value> WrapperFromBoxed(GIBaseInfo *info, void *data, bool mustCopy) {
435
+ Local<Value> WrapperFromBoxed(GIBaseInfo *info, void *data, ResourceOwnership ownership) {
423
436
  if (data == NULL)
424
437
  return Nan::Null();
425
438
 
426
439
  Local<Function> constructor = MakeBoxedClass (info);
427
440
 
428
- Local<Value> boxed_external = Nan::New<External> (data);
429
- Local<Value> must_copy_value = Nan::New<v8::Boolean> (mustCopy);
430
- Local<Value> args[] = { boxed_external, must_copy_value };
441
+ Local<Value> jsBoxed = Nan::New<External> (data);
442
+ Local<Value> jsOwnership = Nan::New<v8::Int32> ((int32_t) ownership);
443
+ Local<Value> args[] = { jsBoxed, jsOwnership };
431
444
 
432
445
  MaybeLocal<Object> instance = Nan::NewInstance(constructor, 2, args);
433
446
 
@@ -445,4 +458,21 @@ void* PointerFromWrapper(Local<Value> value) {
445
458
  return boxed;
446
459
  }
447
460
 
461
+ void DisownBoxed(Local<Value> value) {
462
+ if (!value->IsObject())
463
+ return;
464
+
465
+ Local<Object> object = TO_OBJECT (value);
466
+ // Boxed wrappers store the data pointer in field 0 and the Boxed* in field 1.
467
+ if (object->InternalFieldCount() < 2)
468
+ return;
469
+
470
+ Boxed *box = static_cast<Boxed *>(object->GetAlignedPointerFromInternalField(1));
471
+ if (box != NULL) {
472
+ box->owns_memory = false;
473
+ box->data = NULL;
474
+ }
475
+ object->SetAlignedPointerInInternalField(0, NULL);
476
+ }
477
+
448
478
  };
package/src/boxed.h CHANGED
@@ -1,6 +1,7 @@
1
1
 
2
2
  #pragma once
3
3
 
4
+ #include "value.h"
4
5
  #include <nan.h>
5
6
  #include <node.h>
6
7
  #include <girepository.h>
@@ -28,9 +29,22 @@ public:
28
29
  static size_t GetSize (GIBaseInfo *boxed_info) ;
29
30
  };
30
31
 
32
+ // Allocate zero-filled backing memory for a boxed/struct instance. Registered
33
+ // boxed types are freed with g_boxed_free (which, by GLib convention, uses
34
+ // g_slice_free), so they must be allocated with g_slice — allocating them with
35
+ // g_malloc0 and freeing with g_slice_free corrupts the slice allocator on
36
+ // GLib builds where GSlice is a real slab allocator (#290, #213).
37
+ gpointer AllocateBoxed (GType gtype, size_t size);
38
+
31
39
  Local<Function> MakeBoxedClass (GIBaseInfo *info);
32
40
  Local<FunctionTemplate> GetBoxedTemplate (GIBaseInfo *info, GType gtype);
33
- Local<Value> WrapperFromBoxed (GIBaseInfo *info, void *data, bool mustCopy = false);
41
+ Local<Value> WrapperFromBoxed (GIBaseInfo *info, void *data, ResourceOwnership ownership = kNone);
34
42
  void * PointerFromWrapper (Local<Value>);
35
43
 
44
+ // Relinquish ownership of a boxed wrapper's memory: clears owns_memory so the
45
+ // GC finalizer won't free it, and nulls the data pointer so later use fails
46
+ // cleanly instead of touching freed memory. Used after an introspected method
47
+ // that frees the instance itself (e.g. *_free / *_unref) — see #429.
48
+ void DisownBoxed (Local<Value>);
49
+
36
50
  };
package/src/callback.cc CHANGED
@@ -34,8 +34,18 @@ Callback::Callback(Local<Function> fn, GICallableInfo* callback_info, GIScopeTyp
34
34
  info = g_base_info_ref (callback_info);
35
35
  #ifdef GI_AVAILABLE_IN_1_72
36
36
  closure = g_callable_info_create_closure(info, &cif, Callback::Call, this);
37
+ /* On libffi 3.4+ the executable trampoline is a separate mapping from the
38
+ * writable ffi_closure, so the closure pointer is not itself callable and
39
+ * invoking it segfaults (#390, seen on Ubuntu 26 / libffi 3.5). Use the
40
+ * closure's native (executable) address instead. Fall back to the closure
41
+ * pointer if introspection can't supply one — passing NULL as the callback
42
+ * would break startup, since the bindings register callbacks at bootstrap. */
43
+ native_address = g_callable_info_get_closure_native_address(info, closure);
44
+ if (native_address == NULL)
45
+ native_address = (gpointer) closure;
37
46
  #else
38
47
  closure = g_callable_info_prepare_closure(info, &cif, Callback::Call, this);
48
+ native_address = (gpointer) closure;
39
49
  #endif
40
50
  scope_type = scope_type_;
41
51
  }
@@ -117,14 +127,14 @@ void Callback::Execute (GIArgument *result, GIArgument **args, Callback *callbac
117
127
 
118
128
  bool isOutArgument = g_arg_info_get_direction(&arg_info) == GI_DIRECTION_OUT;
119
129
  bool isPrimitive = Type::IsPrimitive(&arg_type);
120
- bool mustCopy = isOutArgument ? false : true;
130
+ ResourceOwnership ownership = isOutArgument ? kNone : kCopy;
121
131
 
122
132
  if (isPrimitive && isOutArgument) {
123
133
  n_return_values += 1;
124
134
  primitive_out_arguments_mask |= 1 << i;
125
135
  js_args[i] = Nan::Null();
126
136
  } else {
127
- js_args[i] = GIArgumentToV8 (&arg_type, args[i + args_offset], -1, mustCopy);
137
+ js_args[i] = GIArgumentToV8 (&arg_type, args[i + args_offset], -1, ownership);
128
138
  }
129
139
  }
130
140
 
@@ -171,6 +181,7 @@ void Callback::Execute (GIArgument *result, GIArgument **args, Callback *callbac
171
181
  if (jsReturnArray->Length() != n_js_return_values) {
172
182
  Throw::Error("Virtual function must return %u arguments but returned %u",
173
183
  n_js_return_values, jsReturnArray->Length());
184
+ goto out;
174
185
  }
175
186
 
176
187
  if (hasVoidReturn)
@@ -194,6 +205,7 @@ void Callback::Execute (GIArgument *result, GIArgument **args, Callback *callbac
194
205
 
195
206
  if (!success) {
196
207
  Throw::InvalidReturnValue (&return_type_info, jsReturnValue);
208
+ goto out;
197
209
  }
198
210
  }
199
211
  }
package/src/callback.h CHANGED
@@ -16,6 +16,7 @@ namespace GNodeJS {
16
16
  struct Callback {
17
17
  ffi_cif cif;
18
18
  ffi_closure *closure;
19
+ gpointer native_address; /* callable trampoline; may differ from closure on libffi 3.4+ */
19
20
  Nan::Persistent<Function> persistent;
20
21
  GICallableInfo *info;
21
22
  GIScopeType scope_type;
package/src/closure.cc CHANGED
@@ -19,6 +19,41 @@ using Nan::Persistent;
19
19
 
20
20
  namespace GNodeJS {
21
21
 
22
+ /*
23
+ * An (out)/(inout) signal parameter arrives as a GValue holding a *pointer* to
24
+ * the value (e.g. a gint* for an `(inout) (type int)` parameter), not the value
25
+ * itself. Dereference it in place so the argument holds the pointed-to value.
26
+ */
27
+ static void LoadGIArgumentFromPointer (GITypeInfo *type_info, GIArgument *arg) {
28
+ gpointer ptr = arg->v_pointer;
29
+
30
+ if (ptr == NULL) {
31
+ memset(arg, 0, sizeof(*arg));
32
+ return;
33
+ }
34
+
35
+ switch (g_type_info_get_tag(type_info)) {
36
+ case GI_TYPE_TAG_BOOLEAN: arg->v_boolean = *(gboolean*) ptr; break;
37
+ case GI_TYPE_TAG_INT8: arg->v_int8 = *(gint8*) ptr; break;
38
+ case GI_TYPE_TAG_UINT8: arg->v_uint8 = *(guint8*) ptr; break;
39
+ case GI_TYPE_TAG_INT16: arg->v_int16 = *(gint16*) ptr; break;
40
+ case GI_TYPE_TAG_UINT16: arg->v_uint16 = *(guint16*) ptr; break;
41
+ case GI_TYPE_TAG_INT32: arg->v_int32 = *(gint32*) ptr; break;
42
+ case GI_TYPE_TAG_UNICHAR:
43
+ case GI_TYPE_TAG_UINT32: arg->v_uint32 = *(guint32*) ptr; break;
44
+ case GI_TYPE_TAG_INT64: arg->v_int64 = *(gint64*) ptr; break;
45
+ case GI_TYPE_TAG_UINT64: arg->v_uint64 = *(guint64*) ptr; break;
46
+ case GI_TYPE_TAG_FLOAT: arg->v_float = *(gfloat*) ptr; break;
47
+ case GI_TYPE_TAG_DOUBLE: arg->v_double = *(gdouble*) ptr; break;
48
+ case GI_TYPE_TAG_GTYPE: arg->v_size = *(gsize*) ptr; break;
49
+ default:
50
+ // Pointer-like types (utf8, interface, array, list, ...): the value
51
+ // is itself a pointer stored at *ptr.
52
+ arg->v_pointer = *(gpointer*) ptr;
53
+ break;
54
+ }
55
+ }
56
+
22
57
  GClosure *Closure::New (Local<Function> function, GICallableInfo* info, guint signalId) {
23
58
  Closure *closure = (Closure *) g_closure_new_simple (sizeof (*closure), GUINT_TO_POINTER(signalId));
24
59
  closure->persistent.Reset(function);
@@ -63,26 +98,39 @@ void Closure::Execute(GICallableInfo *info, guint signal_id,
63
98
  g_callable_info_load_arg(info, i - 1, &arg_info);
64
99
  g_arg_info_load_type(&arg_info, &type_info);
65
100
 
66
- bool mustCopy = true;
101
+ ResourceOwnership ownership = kCopy;
67
102
 
68
103
  if (signal_query.signal_id) {
69
- mustCopy = (signal_query.param_types[i - 1] & G_SIGNAL_TYPE_STATIC_SCOPE) == 0;
104
+ if ((signal_query.param_types[i - 1] & G_SIGNAL_TYPE_STATIC_SCOPE) != 0) {
105
+ ownership = kNone;
106
+ }
70
107
  }
71
- if (g_arg_info_get_direction(&arg_info) == GI_DIRECTION_OUT) {
72
- mustCopy = false;
108
+
109
+ GIDirection direction = g_arg_info_get_direction(&arg_info);
110
+ if (direction == GI_DIRECTION_INOUT) {
111
+ // The GValue holds a pointer to the in/out value; dereference it
112
+ // so the handler receives the actual value (#405).
113
+ LoadGIArgumentFromPointer(&type_info, &argument);
114
+ ownership = kNone;
115
+ } else if (direction == GI_DIRECTION_OUT) {
116
+ // Pure out: there is no meaningful incoming value.
117
+ memset(&argument, 0, sizeof(argument));
118
+ ownership = kNone;
73
119
  }
74
120
 
75
- js_args[i - 1] = GIArgumentToV8(&type_info, &argument, -1, mustCopy);
121
+ js_args[i - 1] = GIArgumentToV8(&type_info, &argument, -1, ownership);
76
122
  }
77
123
  } else {
78
124
  /* CallableInfo is not available: use GValueToV8 */
79
125
  for (guint i = 1; i < n_param_values; i++) {
80
- bool mustCopy = true;
126
+ ResourceOwnership ownership = kCopy;
81
127
 
82
128
  if (signal_query.signal_id) {
83
- mustCopy = (signal_query.param_types[i - 1] & G_SIGNAL_TYPE_STATIC_SCOPE) == 0;
129
+ if ((signal_query.param_types[i - 1] & G_SIGNAL_TYPE_STATIC_SCOPE) != 0) {
130
+ ownership = kNone;
131
+ }
84
132
  }
85
- js_args[i - 1] = GValueToV8(&param_values[i], mustCopy);
133
+ js_args[i - 1] = GValueToV8(&param_values[i], ownership);
86
134
  }
87
135
  }
88
136
 
@@ -100,8 +148,55 @@ void Closure::Execute(GICallableInfo *info, guint signal_id,
100
148
  if (!try_catch.HasCaught()
101
149
  && result.ToLocal(&return_value)) {
102
150
 
103
- if (g_return_value) {
104
- if (!V8ToGValue (g_return_value, return_value, true))
151
+ if (info) {
152
+ // Distribute the handler's return value across the signal's return
153
+ // value and its (out)/(inout) parameters, writing each back (#405).
154
+ // When there is more than one output the handler returns an array,
155
+ // mirroring how out-arguments are returned from a function call.
156
+ GIArgInfo out_arg_info;
157
+ GITypeInfo out_type_info;
158
+
159
+ int n_outputs = (g_return_value != NULL) ? 1 : 0;
160
+ for (guint i = 1; i < n_param_values; i++) {
161
+ g_callable_info_load_arg(info, i - 1, &out_arg_info);
162
+ GIDirection d = g_arg_info_get_direction(&out_arg_info);
163
+ if (d == GI_DIRECTION_OUT || d == GI_DIRECTION_INOUT)
164
+ n_outputs++;
165
+ }
166
+
167
+ bool from_array = n_outputs > 1 && return_value->IsArray();
168
+ int output_index = 0;
169
+
170
+ #define NEXT_OUTPUT() ( \
171
+ n_outputs <= 1 \
172
+ ? return_value \
173
+ : (from_array \
174
+ ? Nan::Get(return_value.As<v8::Array>(), output_index++).ToLocalChecked() \
175
+ : (output_index++, Nan::Undefined().As<Value>())))
176
+
177
+ if (g_return_value) {
178
+ if (!V8ToGValue (g_return_value, NEXT_OUTPUT(), kCopy))
179
+ goto throw_exception;
180
+ }
181
+
182
+ for (guint i = 1; i < n_param_values; i++) {
183
+ g_callable_info_load_arg(info, i - 1, &out_arg_info);
184
+ GIDirection d = g_arg_info_get_direction(&out_arg_info);
185
+ if (d != GI_DIRECTION_OUT && d != GI_DIRECTION_INOUT)
186
+ continue;
187
+
188
+ g_arg_info_load_type(&out_arg_info, &out_type_info);
189
+
190
+ GIArgument out_arg;
191
+ memcpy(&out_arg, &param_values[i].data[0], sizeof(GIArgument));
192
+ if (out_arg.v_pointer != NULL)
193
+ V8ToOutGIArgument(&out_type_info, &out_arg, NEXT_OUTPUT(), true);
194
+ }
195
+
196
+ #undef NEXT_OUTPUT
197
+ }
198
+ else if (g_return_value) {
199
+ if (!V8ToGValue (g_return_value, return_value, kCopy))
105
200
  goto throw_exception;
106
201
  }
107
202