node-gtk 1.0.0 → 2.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.
@@ -119,7 +119,7 @@ function findVFuncOnParents(info, name) {
119
119
  function findVFuncOnInterfaces(gtype, name) {
120
120
  const interfaces = GObject.typeInterfaces(gtype);
121
121
 
122
- for (i = 0; i < interfaces.length; i++) {
122
+ for (let i = 0; i < interfaces.length; i++) {
123
123
  const interfaceInfo = findInfoByGtype(interfaces[i])
124
124
 
125
125
  /* The interface doesn't have to exist, it could be private
package/package.json CHANGED
@@ -1,10 +1,15 @@
1
1
  {
2
2
  "name": "node-gtk",
3
- "version": "1.0.0",
3
+ "version": "2.1.0",
4
4
  "description": "GNOME Gtk+ bindings for NodeJS",
5
5
  "main": "lib/index.js",
6
+ "bin": {
7
+ "node-gtk": "bin/node-gtk.js"
8
+ },
6
9
  "scripts": {
7
10
  "install": "npx node-pre-gyp install --fallback-to-build",
11
+ "build:test-fixtures": "node scripts/build-test-fixtures.js",
12
+ "pretest": "node scripts/build-test-fixtures.js",
8
13
  "test": "mocha tests/__run__.js",
9
14
  "build": "npx node-pre-gyp build",
10
15
  "build:full": "npx node-pre-gyp rebuild",
@@ -36,7 +41,7 @@
36
41
  "lodash.camelcase": "4.3.0",
37
42
  "lodash.isequal": "4.5.0",
38
43
  "lodash.snakecase": "^4.1.1",
39
- "nan": "^2.23.0",
44
+ "nan": "^2.27.0",
40
45
  "node-gyp": "^11.2.0",
41
46
  "remove-trailing-spaces": "^1.0.7",
42
47
  "unindent": "^2.0.0"
@@ -51,9 +56,11 @@
51
56
  "node-pre-gyp-github": "^1.4.5"
52
57
  },
53
58
  "files": [
59
+ "/bin",
54
60
  "/lib",
55
61
  "/src",
56
62
  "/scripts",
63
+ "/tools",
57
64
  "binding.gyp"
58
65
  ],
59
66
  "binary": {
@@ -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
@@ -42,14 +42,16 @@ function npm_test() {
42
42
 
43
43
  if [[ $(uname -s) == 'Darwin' ]]; then
44
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=union__fields \
48
51
  tests/__run__.js
49
52
  else
50
53
  xvfb-run -a npm test -- \
51
- --skip=callback \
52
- --skip=union__fields;
54
+ --skip=callback;
53
55
  fi;
54
56
  }
55
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;
@@ -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
 
@@ -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
@@ -29,9 +29,22 @@ public:
29
29
  static size_t GetSize (GIBaseInfo *boxed_info) ;
30
30
  };
31
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
+
32
39
  Local<Function> MakeBoxedClass (GIBaseInfo *info);
33
40
  Local<FunctionTemplate> GetBoxedTemplate (GIBaseInfo *info, GType gtype);
34
41
  Local<Value> WrapperFromBoxed (GIBaseInfo *info, void *data, ResourceOwnership ownership = kNone);
35
42
  void * PointerFromWrapper (Local<Value>);
36
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
+
37
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
  }
@@ -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);
@@ -70,8 +105,27 @@ void Closure::Execute(GICallableInfo *info, guint signal_id,
70
105
  ownership = kNone;
71
106
  }
72
107
  }
73
- if (g_arg_info_get_direction(&arg_info) == GI_DIRECTION_OUT) {
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);
74
114
  ownership = kNone;
115
+ } else if (direction == GI_DIRECTION_OUT) {
116
+ if (g_arg_info_is_caller_allocates(&arg_info)) {
117
+ // Caller-allocated out (e.g. a GdkRectangle in
118
+ // GtkOverlay::get-child-position): the GValue already holds a
119
+ // pointer to caller (GLib/GTK) memory. Pass a live wrapper so the
120
+ // handler fills it in place — it is NOT written back from the
121
+ // return value (see the output loop below). #444.
122
+ ownership = kNone;
123
+ } else {
124
+ // Callee-allocated out: no meaningful incoming value; the handler
125
+ // supplies it through the return value.
126
+ memset(&argument, 0, sizeof(argument));
127
+ ownership = kNone;
128
+ }
75
129
  }
76
130
 
77
131
  js_args[i - 1] = GIArgumentToV8(&type_info, &argument, -1, ownership);
@@ -104,7 +158,61 @@ void Closure::Execute(GICallableInfo *info, guint signal_id,
104
158
  if (!try_catch.HasCaught()
105
159
  && result.ToLocal(&return_value)) {
106
160
 
107
- if (g_return_value) {
161
+ if (info) {
162
+ // Distribute the handler's return value across the signal's return
163
+ // value and its (out)/(inout) parameters, writing each back (#405).
164
+ // When there is more than one output the handler returns an array,
165
+ // mirroring how out-arguments are returned from a function call.
166
+ GIArgInfo out_arg_info;
167
+ GITypeInfo out_type_info;
168
+
169
+ // Caller-allocated out structs are filled in place via the wrapper
170
+ // passed to the handler, so they are not part of the return value.
171
+ int n_outputs = (g_return_value != NULL) ? 1 : 0;
172
+ for (guint i = 1; i < n_param_values; i++) {
173
+ g_callable_info_load_arg(info, i - 1, &out_arg_info);
174
+ GIDirection d = g_arg_info_get_direction(&out_arg_info);
175
+ if (d == GI_DIRECTION_INOUT ||
176
+ (d == GI_DIRECTION_OUT && !g_arg_info_is_caller_allocates(&out_arg_info)))
177
+ n_outputs++;
178
+ }
179
+
180
+ bool from_array = n_outputs > 1 && return_value->IsArray();
181
+ int output_index = 0;
182
+
183
+ #define NEXT_OUTPUT() ( \
184
+ n_outputs <= 1 \
185
+ ? return_value \
186
+ : (from_array \
187
+ ? Nan::Get(return_value.As<v8::Array>(), output_index++).ToLocalChecked() \
188
+ : (output_index++, Nan::Undefined().As<Value>())))
189
+
190
+ if (g_return_value) {
191
+ if (!V8ToGValue (g_return_value, NEXT_OUTPUT(), kCopy))
192
+ goto throw_exception;
193
+ }
194
+
195
+ for (guint i = 1; i < n_param_values; i++) {
196
+ g_callable_info_load_arg(info, i - 1, &out_arg_info);
197
+ GIDirection d = g_arg_info_get_direction(&out_arg_info);
198
+ if (d != GI_DIRECTION_OUT && d != GI_DIRECTION_INOUT)
199
+ continue;
200
+ // Caller-allocated out structs were filled in place by the handler
201
+ // via their wrapper; nothing to write back from the return value.
202
+ if (d == GI_DIRECTION_OUT && g_arg_info_is_caller_allocates(&out_arg_info))
203
+ continue;
204
+
205
+ g_arg_info_load_type(&out_arg_info, &out_type_info);
206
+
207
+ GIArgument out_arg;
208
+ memcpy(&out_arg, &param_values[i].data[0], sizeof(GIArgument));
209
+ if (out_arg.v_pointer != NULL)
210
+ V8ToOutGIArgument(&out_type_info, &out_arg, NEXT_OUTPUT(), true);
211
+ }
212
+
213
+ #undef NEXT_OUTPUT
214
+ }
215
+ else if (g_return_value) {
108
216
  if (!V8ToGValue (g_return_value, return_value, kCopy))
109
217
  goto throw_exception;
110
218
  }