node-gtk 1.0.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
@@ -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,7 +105,16 @@ 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);
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));
74
118
  ownership = kNone;
75
119
  }
76
120
 
@@ -104,7 +148,54 @@ void Closure::Execute(GICallableInfo *info, guint signal_id,
104
148
  if (!try_catch.HasCaught()
105
149
  && result.ToLocal(&return_value)) {
106
150
 
107
- if (g_return_value) {
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) {
108
199
  if (!V8ToGValue (g_return_value, return_value, kCopy))
109
200
  goto throw_exception;
110
201
  }
package/src/function.cc CHANGED
@@ -55,7 +55,9 @@ static void* AllocateArgument (GIBaseInfo *arg_info) {
55
55
 
56
56
  GIBaseInfo* base_info = g_type_info_get_interface (&arg_type);
57
57
  size_t size = Boxed::GetSize (base_info);
58
- void* pointer = g_malloc0(size);
58
+ GType gtype = g_registered_type_info_get_g_type (base_info);
59
+ // Match g_boxed_free's allocator for registered boxed types (#290, #213).
60
+ void* pointer = AllocateBoxed(gtype, size);
59
61
 
60
62
  g_base_info_unref(base_info);
61
63
  return pointer;
@@ -67,6 +69,35 @@ static bool IsMethod (GIBaseInfo *info) {
67
69
  (flags & GI_FUNCTION_IS_CONSTRUCTOR) == 0);
68
70
  }
69
71
 
72
+ static bool EndsWith (const char *str, const char *suffix) {
73
+ size_t str_len = strlen (str);
74
+ size_t suffix_len = strlen (suffix);
75
+ return str_len >= suffix_len && strcmp (str + str_len - suffix_len, suffix) == 0;
76
+ }
77
+
78
+ /* True for an instance method that deallocates the instance itself — a *_free
79
+ * or *_unref on a boxed/struct/union. node-gtk's wrapper owns the memory and
80
+ * frees it on GC, so after such a call the wrapper must disown it or GC will
81
+ * double-free (#429). Restricted to boxed-like containers: GObject lifetime is
82
+ * handled separately, and gtk_widget_destroy() etc. don't free the wrapper. */
83
+ static bool FreesInstance (GIFunctionInfo *info) {
84
+ if (!IsMethod (info))
85
+ return false;
86
+
87
+ const char *symbol = g_function_info_get_symbol (info);
88
+ if (symbol == NULL || !(EndsWith (symbol, "_free") || EndsWith (symbol, "_unref")))
89
+ return false;
90
+
91
+ GIBaseInfo *container = g_base_info_get_container (info);
92
+ if (container == NULL)
93
+ return false;
94
+
95
+ GIInfoType type = g_base_info_get_type (container);
96
+ return type == GI_INFO_TYPE_STRUCT
97
+ || type == GI_INFO_TYPE_UNION
98
+ || type == GI_INFO_TYPE_BOXED;
99
+ }
100
+
70
101
  static bool ShouldSkipReturn(GIBaseInfo *info, GITypeInfo *return_type) {
71
102
  return g_type_info_get_tag(return_type) == GI_TYPE_TAG_VOID
72
103
  || g_callable_info_skip_return(info) == TRUE;
@@ -134,6 +165,7 @@ bool FunctionInfo::Init() {
134
165
 
135
166
  is_method = IsMethod(info);
136
167
  can_throw = g_callable_info_can_throw_gerror (info);
168
+ frees_instance = FreesInstance(info);
137
169
 
138
170
  n_callable_args = g_callable_info_get_n_args (info);
139
171
  n_total_args = n_callable_args;
@@ -208,14 +240,14 @@ bool FunctionInfo::Init() {
208
240
  if (closure_i >= 0 && closure_i < n_callable_args)
209
241
  call_parameters[closure_i].type = ParameterType::kSKIP;
210
242
 
211
- if (destroy_i < i) {
243
+ if (destroy_i >= 0 && destroy_i < i) {
212
244
  if (IsDirectionIn(call_parameters[destroy_i].direction))
213
245
  n_in_args--;
214
246
  if (IsDirectionOut(call_parameters[destroy_i].direction))
215
247
  n_out_args--;
216
248
  }
217
249
 
218
- if (closure_i < i) {
250
+ if (closure_i >= 0 && closure_i < i) {
219
251
  if (IsDirectionIn(call_parameters[closure_i].direction))
220
252
  n_in_args--;
221
253
  if (IsDirectionOut(call_parameters[closure_i].direction))
@@ -388,16 +420,16 @@ Local<Value> FunctionCall (
388
420
  }
389
421
  else if (param.type == ParameterType::kCALLBACK) {
390
422
  Callback *callback;
391
- ffi_closure *closure;
423
+ gpointer callable; /* executable trampoline address (see Callback) */
392
424
 
393
425
  if (info[in_arg]->IsNullOrUndefined()) {
394
- closure = nullptr;
426
+ callable = nullptr;
395
427
  callback = nullptr;
396
428
  } else {
397
429
  GICallableInfo *callback_info = g_type_info_get_interface (&type_info);
398
430
  GIScopeType scope_type = g_arg_info_get_scope(&arg_info);
399
431
  callback = new Callback(info[in_arg].As<Function>(), callback_info, scope_type);
400
- closure = callback->closure;
432
+ callable = callback->native_address;
401
433
  g_base_info_unref (callback_info);
402
434
  }
403
435
 
@@ -414,7 +446,7 @@ Local<Value> FunctionCall (
414
446
  callable_arg_values[closure_i].v_pointer = callback;
415
447
  }
416
448
 
417
- callable_arg_values[i].v_pointer = closure;
449
+ callable_arg_values[i].v_pointer = callable;
418
450
  func->call_parameters[i].data.v_pointer = callback;
419
451
  }
420
452
 
@@ -440,6 +472,22 @@ Local<Value> FunctionCall (
440
472
  param.data.v_pointer = callable_arg_values[i].v_pointer;
441
473
  callable_arg_values[i].v_pointer = &param.data;
442
474
  }
475
+ // For a transfer-container IN GList/GSList/GHashTable the callee
476
+ // frees the container; snapshot the (caller-owned) element
477
+ // pointers now so they can be freed after the call (#399).
478
+ else if (IsTransferContainerInList(&type_info,
479
+ g_arg_info_get_ownership_transfer(&arg_info), direction)) {
480
+ param.data = {};
481
+ param.data.v_pointer = CaptureTransferContainerElements(
482
+ &type_info, callable_arg_values[i].v_pointer);
483
+ }
484
+ // For a transfer-full IN boxed (or array of boxed) the callee
485
+ // frees the memory; hand it a copy so the JS wrapper's own
486
+ // memory isn't double-freed when it's finalized (#409).
487
+ else if (direction == GI_DIRECTION_IN
488
+ && g_arg_info_get_ownership_transfer(&arg_info) == GI_TRANSFER_EVERYTHING) {
489
+ CopyBoxedForTransferFullIn(&type_info, &callable_arg_values[i], param.length);
490
+ }
443
491
  }
444
492
 
445
493
  in_arg++;
@@ -538,6 +586,11 @@ Local<Value> FunctionCall (
538
586
  delete callback;
539
587
  }
540
588
  }
589
+ else if (IsTransferContainerInList (&arg_type, transfer, direction)) {
590
+ // The callee already freed the container structure; free only the
591
+ // captured (caller-owned) elements, never the freed container (#399).
592
+ FreeTransferContainerElements (&arg_type, param.data.v_pointer);
593
+ }
541
594
  else {
542
595
  if (direction == GI_DIRECTION_INOUT || (direction == GI_DIRECTION_OUT && !g_arg_info_is_caller_allocates (&arg_info)))
543
596
  FreeGIArgument (&arg_type, (GIArgument*)arg_value.v_pointer, transfer, direction);
@@ -546,6 +599,11 @@ Local<Value> FunctionCall (
546
599
  }
547
600
  }
548
601
 
602
+ // If this method freed the instance, drop node-gtk's ownership of it so the
603
+ // GC finalizer won't free it a second time (#429).
604
+ if (func->frees_instance && !didThrow)
605
+ DisownBoxed (info.This());
606
+
549
607
  #ifndef __linux__
550
608
  delete[] total_arg_values;
551
609
  #endif
package/src/function.h CHANGED
@@ -33,6 +33,7 @@ struct FunctionInfo {
33
33
 
34
34
  bool is_method;
35
35
  bool can_throw;
36
+ bool frees_instance; /* a *_free/*_unref method that deallocates the instance (#429) */
36
37
 
37
38
  int n_callable_args;
38
39
  int n_total_args;