@gjsify/node-gi 0.45.0 → 0.46.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.
- package/README.md +23 -9
- package/gi.js +53 -2
- package/gtk-runtime.js +14 -0
- package/index.d.ts +15 -1
- package/index.js +16 -0
- package/native-prebuilds.js +510 -0
- package/package.json +2 -1
- package/prebuilds/darwin-arm64/node_gi.node +0 -0
- package/prebuilds/darwin-x64/node_gi.node +0 -0
- package/prebuilds/linux-arm64/node_gi.node +0 -0
- package/prebuilds/linux-x64/node_gi.node +0 -0
- package/prebuilds/win32-x64/node_gi.node +0 -0
- package/src/addon.cc +1 -0
- package/src/calls.cc +102 -26
- package/src/class.cc +22 -9
- package/src/common.h +70 -20
- package/src/marshal.cc +561 -28
- package/src/object.cc +26 -0
- package/src/repo.cc +15 -4
- package/src/toggle.cc +208 -53
- package/system-gi.d.ts +4 -0
- package/system-gi.js +65 -11
package/src/marshal.cc
CHANGED
|
@@ -296,6 +296,28 @@ bool TryGetBoxedPtr(Napi::Value v, gpointer* out) {
|
|
|
296
296
|
return true;
|
|
297
297
|
}
|
|
298
298
|
|
|
299
|
+
// __boxedAddress(value) -> the address a boxed handle wraps, as a hex string, or
|
|
300
|
+
// null when `value` is not one. TEST-ONLY (the `__` prefix marks it, as on the
|
|
301
|
+
// __stressRefUnref* pair) — nothing on the runtime path may branch on it.
|
|
302
|
+
//
|
|
303
|
+
// It exists because the IN-transfer ownership rules are statements about POINTER
|
|
304
|
+
// IDENTITY ("a transfer-full boxed IN arg gets an independent copy, never the
|
|
305
|
+
// block this handle still owns"), and nothing else could observe one. The tests
|
|
306
|
+
// guarding them ran the call many times and waited for a double free to abort —
|
|
307
|
+
// which only happens if the allocator has NOT recycled the block between the two
|
|
308
|
+
// frees. Measured while reviewing #1496: with `TransferBoxedIn` replaced by a bare
|
|
309
|
+
// borrow — a real double free on every round — `gerror-out.test.mjs` passed 5 runs
|
|
310
|
+
// out of 5, and the same program under valgrind reported 0 errors. An identity
|
|
311
|
+
// assertion fails on the first round instead.
|
|
312
|
+
Napi::Value BoxedAddress(const Napi::CallbackInfo& info) {
|
|
313
|
+
Napi::Env env = info.Env();
|
|
314
|
+
gpointer ptr = nullptr;
|
|
315
|
+
if (info.Length() < 1 || !TryGetBoxedPtr(info[0], &ptr)) return env.Null();
|
|
316
|
+
char buf[2 + sizeof(gpointer) * 2 + 1];
|
|
317
|
+
g_snprintf(buf, sizeof(buf), "%p", ptr);
|
|
318
|
+
return Napi::String::New(env, buf);
|
|
319
|
+
}
|
|
320
|
+
|
|
299
321
|
// boxedTypeName(value) -> the boxed handle's GType name (e.g. "GBytes"), or null
|
|
300
322
|
// when the handle carries no registered GType. Lets the L1 layer attach a
|
|
301
323
|
// type-specific convenience (GLib.Bytes.toArray) without a per-type wrapper.
|
|
@@ -441,6 +463,55 @@ static const char* InformalValueTypeName(const Napi::Value& v) {
|
|
|
441
463
|
return "Object";
|
|
442
464
|
}
|
|
443
465
|
|
|
466
|
+
// The refusal a GError-typed IN arg raises, named after the argument the way gjs
|
|
467
|
+
// names its marshalling errors.
|
|
468
|
+
static void ThrowExpectedGLibError(Napi::Env env, const char* argName, const std::string& got) {
|
|
469
|
+
std::string where = argName != nullptr ? std::string("argument '") + argName + "'" : "argument";
|
|
470
|
+
Napi::TypeError::New(env, "expected a GLib.Error for " + where + ", got " + got)
|
|
471
|
+
.ThrowAsJavaScriptException();
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// Whether `v` is an instance of the L1 `GLib.Error` class — the shape a `catch`
|
|
475
|
+
// around a failed GI invoke hands you, and what `new GLib.Error(domain, code,
|
|
476
|
+
// message)` builds. Recognition is `instanceof` against the class L1 registered
|
|
477
|
+
// alongside its error builder, never duck-typing: a plain object carrying a
|
|
478
|
+
// `code` must still be refused.
|
|
479
|
+
static bool IsL1GLibError(Napi::Env env, Napi::Value v) {
|
|
480
|
+
if (!v.IsObject()) return false;
|
|
481
|
+
NodeGiEnvData* d = EnvData(env);
|
|
482
|
+
if (d == nullptr || d->errorClass == nullptr) return false;
|
|
483
|
+
napi_value cls = nullptr;
|
|
484
|
+
if (napi_get_reference_value(env, d->errorClass, &cls) != napi_ok || cls == nullptr) return false;
|
|
485
|
+
Napi::Value clsValue(env, cls);
|
|
486
|
+
return clsValue.IsFunction() && v.As<Napi::Object>().InstanceOf(clsValue.As<Napi::Function>());
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// Build a real GError out of an L1 `GLib.Error`, which carries no `GError*` of
|
|
490
|
+
// its own. Throws + returns nullptr when its domain is unusable.
|
|
491
|
+
//
|
|
492
|
+
// `.domainQuark` is the numeric GQuark the engine read off the original GError;
|
|
493
|
+
// `.domain` is its NAME (the node-gi divergence documented in
|
|
494
|
+
// docs/node-gi-gjs-surface.md), which is what an error constructed in JS from a
|
|
495
|
+
// quark name carries. Either resolves to the same quark; a value with neither is
|
|
496
|
+
// not a usable GError — quark 0 would produce an error no `matches()` can ever
|
|
497
|
+
// answer for, so it is refused rather than marshalled into a silent mismatch.
|
|
498
|
+
static GError* ErrorFromL1GLibError(Napi::Env env, Napi::Value v, const char* argName) {
|
|
499
|
+
Napi::Object err = v.As<Napi::Object>();
|
|
500
|
+
Napi::Value quark = err.Get("domainQuark");
|
|
501
|
+
Napi::Value domain = err.Get("domain");
|
|
502
|
+
GQuark gq = 0;
|
|
503
|
+
if (quark.IsNumber()) gq = static_cast<GQuark>(NodeGiToUint32(quark));
|
|
504
|
+
else if (domain.IsString())
|
|
505
|
+
gq = g_quark_from_string(domain.As<Napi::String>().Utf8Value().c_str());
|
|
506
|
+
if (gq == 0) {
|
|
507
|
+
ThrowExpectedGLibError(env, argName, "a GLib.Error with no domain");
|
|
508
|
+
return nullptr;
|
|
509
|
+
}
|
|
510
|
+
Napi::Value message = err.Get("message");
|
|
511
|
+
std::string text = message.IsString() ? message.As<Napi::String>().Utf8Value() : std::string();
|
|
512
|
+
return g_error_new_literal(gq, NodeGiToInt32(err.Get("code")), text.c_str());
|
|
513
|
+
}
|
|
514
|
+
|
|
444
515
|
// "Gtk.Box" for an introspected GType, g_type_name() otherwise (a registerClass
|
|
445
516
|
// subtype has no GI info of its own; its C name is still exact). The gjs twin is
|
|
446
517
|
// GIWrapperBase::format_name() (refs/gjs/gi/wrapperutils.h).
|
|
@@ -469,6 +540,7 @@ bool JsToGIArgument(Napi::Env env, Napi::Value v, GITypeInfo* type, GIArgument*
|
|
|
469
540
|
CreatedClosures* closures,
|
|
470
541
|
CreatedBytes* bytes,
|
|
471
542
|
CreatedValues* values,
|
|
543
|
+
CreatedErrors* errors,
|
|
472
544
|
const char* argName) {
|
|
473
545
|
if (v.IsEmpty()) {
|
|
474
546
|
// Residue of a swallowed napi failure (a fallible Get()/coercion upstream
|
|
@@ -712,6 +784,54 @@ bool JsToGIArgument(Napi::Env env, Napi::Value v, GITypeInfo* type, GIArgument*
|
|
|
712
784
|
out->v_size = static_cast<gsize>(gt);
|
|
713
785
|
return true;
|
|
714
786
|
}
|
|
787
|
+
case GI_TYPE_TAG_ERROR: {
|
|
788
|
+
// A GError IN/INOUT argument: `Gst.Message.new_error(src, error, debug)` —
|
|
789
|
+
// how an application POSTS the failure the bus accessors read back —
|
|
790
|
+
// `GLib.propagate_error`, and the INOUT `err` of `GLib.prefix_error_literal`.
|
|
791
|
+
// null/undefined → a NULL GError, the same leniency the object/boxed arms
|
|
792
|
+
// keep (gjs refuses both for a non-nullable arg — status/open-todos.md).
|
|
793
|
+
if (v.IsNull() || v.IsUndefined()) {
|
|
794
|
+
out->v_pointer = nullptr;
|
|
795
|
+
return true;
|
|
796
|
+
}
|
|
797
|
+
// Shape 1: the boxed handle a GError-typed return/OUT produced. Ownership is
|
|
798
|
+
// the ordinary boxed one and TransferBoxedIn supplies it: transfer-none is a
|
|
799
|
+
// borrow, transfer-full hands the callee an independent g_error_copy
|
|
800
|
+
// (g_propagate_error ADOPTS and frees its `src`) so the JS handle's finalizer
|
|
801
|
+
// cannot double-free it. Measured against gjs: after `GLib.propagate_error(e)`
|
|
802
|
+
// the original `e` is still readable there too.
|
|
803
|
+
//
|
|
804
|
+
// Type-safety is UNCONDITIONAL here, unlike the struct/boxed arm's "compare
|
|
805
|
+
// only when both sides carry a registered GType": the expected type is known
|
|
806
|
+
// (G_TYPE_ERROR, always), so a handle without it is a wrong pointer — and a
|
|
807
|
+
// wrong pointer in a GError slot is a wild dereference inside GLib.
|
|
808
|
+
BoxedHandle* h = TryGetBoxedHandle(v);
|
|
809
|
+
if (h != nullptr) {
|
|
810
|
+
if (h->gtype == G_TYPE_INVALID || !g_type_is_a(h->gtype, G_TYPE_ERROR)) {
|
|
811
|
+
ThrowExpectedGLibError(env, argName,
|
|
812
|
+
h->gtype != G_TYPE_INVALID ? g_type_name(h->gtype)
|
|
813
|
+
: InformalValueTypeName(v));
|
|
814
|
+
return false;
|
|
815
|
+
}
|
|
816
|
+
out->v_pointer = TransferBoxedIn(h, transfer);
|
|
817
|
+
return true;
|
|
818
|
+
}
|
|
819
|
+
// Shape 2: the L1 `GLib.Error` JS class — a caught error, or one built with
|
|
820
|
+
// `new GLib.Error(…)`. It carries no GError*, so build one and release it per
|
|
821
|
+
// transfer (CreatedErrors, the rule GBytes and GValue already follow). gjs has
|
|
822
|
+
// ONE object for both shapes and accepts either; refusing this one left the
|
|
823
|
+
// hand-back the OUT direction exists for closed to exactly the errors an
|
|
824
|
+
// application has in hand.
|
|
825
|
+
if (errors != nullptr && IsL1GLibError(env, v)) {
|
|
826
|
+
GError* built = ErrorFromL1GLibError(env, v, argName);
|
|
827
|
+
if (built == nullptr) return false; // ErrorFromL1GLibError already threw
|
|
828
|
+
out->v_pointer = built;
|
|
829
|
+
errors->Record(built, transfer);
|
|
830
|
+
return true;
|
|
831
|
+
}
|
|
832
|
+
ThrowExpectedGLibError(env, argName, InformalValueTypeName(v));
|
|
833
|
+
return false;
|
|
834
|
+
}
|
|
715
835
|
default:
|
|
716
836
|
Napi::TypeError::New(
|
|
717
837
|
env, "Unsupported IN argument type tag " + std::to_string(static_cast<int>(tag)) +
|
|
@@ -892,10 +1012,35 @@ Napi::Value GIArgumentToJs(Napi::Env env, GITypeInfo* type, GIArgument* arg,
|
|
|
892
1012
|
// pass it, and free it (container + any g_strdup'd strings) AFTER the invoke.
|
|
893
1013
|
// TRANSFER_EVERYTHING / TRANSFER_CONTAINER means the callee adopts what we
|
|
894
1014
|
// built — we must NOT free it (that would UAF / double-free).
|
|
1015
|
+
// * IN, PER ELEMENT: the container transfer also decides who owns the ELEMENTS,
|
|
1016
|
+
// and the two answers differ. CONTAINER hands over only the container, so the
|
|
1017
|
+
// elements stay ours and a borrowed pointer is right. EVERYTHING hands over the
|
|
1018
|
+
// elements TOO — a borrowed object/boxed pointer there is freed by the callee
|
|
1019
|
+
// AND by the JS handle that still owns it. Measured on the pointer-struct case
|
|
1020
|
+
// the moment it was admitted: `Pango.Font.descriptions_free([desc])` (`descs` is
|
|
1021
|
+
// `(transfer full)`) read back garbage and died in `free(): invalid size`. So
|
|
1022
|
+
// ElementToGIArgument takes the ELEMENT transfer — NOTHING for a borrow (incl.
|
|
1023
|
+
// CONTAINER), EVERYTHING for adoption, exactly the rule the read side already
|
|
1024
|
+
// uses — and refs/copies on EVERYTHING. Strings were always right (g_strdup'd
|
|
1025
|
+
// per element either way).
|
|
1026
|
+
// * IN, ROOTING: a borrowed element pointer stays valid across the invoke because
|
|
1027
|
+
// the JS array is an ARGUMENT of the running native call — it and everything
|
|
1028
|
+
// reachable from it are held by that call's handle scope, which is not popped
|
|
1029
|
+
// until we return. So nothing can be collected between building the container
|
|
1030
|
+
// and the callee reading it, not even an element a getter/Proxy synthesised.
|
|
895
1031
|
|
|
896
1032
|
// Element-type support shared by every container kind. *why receives a short
|
|
897
1033
|
// label on refusal (so the caller can throw a precise deferral message).
|
|
898
|
-
|
|
1034
|
+
//
|
|
1035
|
+
// `byValueOk` is true only where the element is a SIZED CELL the IN path lays out
|
|
1036
|
+
// itself — a C array being built for an IN argument. It is false for a read-back
|
|
1037
|
+
// (ReadCElement still dereferences an interface element as a pointer) and false for
|
|
1038
|
+
// GList/GSList/GHashTable, whose elements are POINTER slots filled through
|
|
1039
|
+
// gi_type_info_hash_pointer_from_argument: that helper passes a pointer through
|
|
1040
|
+
// unchanged, which is right for a pointer-struct element and meaningless for a
|
|
1041
|
+
// four-byte enum or a twenty-four-byte record. So the by-value admission is scoped to
|
|
1042
|
+
// the one container kind that has somewhere to put the bytes.
|
|
1043
|
+
static bool IsSupportedElementType(GITypeInfo* elem, std::string* why, bool byValueOk) {
|
|
899
1044
|
switch (gi_type_info_get_tag(elem)) {
|
|
900
1045
|
case GI_TYPE_TAG_BOOLEAN:
|
|
901
1046
|
case GI_TYPE_TAG_INT8:
|
|
@@ -914,6 +1059,39 @@ static bool IsSupportedElementType(GITypeInfo* elem, std::string* why) {
|
|
|
914
1059
|
case GI_TYPE_TAG_INTERFACE: {
|
|
915
1060
|
GIBaseInfo* iface = gi_type_info_get_interface(elem);
|
|
916
1061
|
bool ok = iface != nullptr && (GI_IS_OBJECT_INFO(iface) || GI_IS_INTERFACE_INFO(iface));
|
|
1062
|
+
// A struct element that is a POINTER occupies exactly one pointer slot, which is
|
|
1063
|
+
// the same shape an object element already has: CElementSize answers
|
|
1064
|
+
// sizeof(gpointer) and the write loop memcpy's the low 8 bytes.
|
|
1065
|
+
// `GLib.Variant.new_tuple` is the call this unblocks: measured against the
|
|
1066
|
+
// installed typelib, its `children` element is `tag=interface ptr=1 kind=STRUCT`.
|
|
1067
|
+
//
|
|
1068
|
+
// Admission alone was NOT enough, and the reading that said it was is worth
|
|
1069
|
+
// keeping: "FreeInContainer's non-string branch already frees the buffer without
|
|
1070
|
+
// touching what the pointers point at" is true and beside the point, because
|
|
1071
|
+
// FreeInContainer runs only for TRANSFER_NOTHING. On a (transfer full) container
|
|
1072
|
+
// the CALLEE frees the elements — see the per-element half of the OWNERSHIP note
|
|
1073
|
+
// above and ElementToGIArgument, which is where that is now handled.
|
|
1074
|
+
//
|
|
1075
|
+
// A BY-VALUE struct element stays refused, and the distinction is the whole point
|
|
1076
|
+
// of testing is_pointer here rather than the kind alone: `Gtk.Accessible
|
|
1077
|
+
// .update_property`'s values are `ptr=0 kind=STRUCT size=24`, so admitting them
|
|
1078
|
+
// would lay 24-byte GValues out at an 8-byte stride and hand the callee garbage.
|
|
1079
|
+
// That case needs gi_struct_info_get_size plus an ownership rule (each in-place
|
|
1080
|
+
// g_value_init would need unsetting, and FreeInContainer has no branch for it) —
|
|
1081
|
+
// the same deferred work this file already records at its CALLER_ALLOCATES site.
|
|
1082
|
+
if (!ok && iface != nullptr && GI_IS_STRUCT_INFO(iface) && gi_type_info_is_pointer(elem)) {
|
|
1083
|
+
ok = true;
|
|
1084
|
+
}
|
|
1085
|
+
// BY-VALUE elements, admitted only where there is a sized cell to write them
|
|
1086
|
+
// into (see `byValueOk`). An enum/flags element is its storage integer; a
|
|
1087
|
+
// record/union element is its own bytes, copied out of a boxed handle. Both go
|
|
1088
|
+
// through CInElementSize + ElementToCArrayCell, which is the write path that
|
|
1089
|
+
// does NOT run the GIArgument union through memcpy — see the note there.
|
|
1090
|
+
if (!ok && byValueOk && iface != nullptr && !gi_type_info_is_pointer(elem) &&
|
|
1091
|
+
(GI_IS_ENUM_INFO(iface) || GI_IS_FLAGS_INFO(iface) || GI_IS_STRUCT_INFO(iface) ||
|
|
1092
|
+
GI_IS_UNION_INFO(iface))) {
|
|
1093
|
+
ok = true;
|
|
1094
|
+
}
|
|
917
1095
|
if (!ok && why != nullptr) *why = "struct/union/enum element";
|
|
918
1096
|
if (iface != nullptr) gi_base_info_unref(iface);
|
|
919
1097
|
return ok;
|
|
@@ -929,7 +1107,7 @@ static bool IsSupportedElementType(GITypeInfo* elem, std::string* why) {
|
|
|
929
1107
|
// up front. Array-type kind is intentionally NOT restricted here — the read side
|
|
930
1108
|
// (GIArrayToJs) handles C/BYTE_ARRAY/GArray/GPtrArray; the IN side rejects
|
|
931
1109
|
// GArray/GPtrArray separately (they're rare as IN in headless GLib).
|
|
932
|
-
bool IsSupportedContainerType(GITypeInfo* type, std::string* why) {
|
|
1110
|
+
bool IsSupportedContainerType(GITypeInfo* type, std::string* why, ContainerUse use) {
|
|
933
1111
|
switch (gi_type_info_get_tag(type)) {
|
|
934
1112
|
case GI_TYPE_TAG_ARRAY:
|
|
935
1113
|
case GI_TYPE_TAG_GLIST:
|
|
@@ -939,7 +1117,14 @@ bool IsSupportedContainerType(GITypeInfo* type, std::string* why) {
|
|
|
939
1117
|
if (why != nullptr) *why = "untyped container";
|
|
940
1118
|
return false;
|
|
941
1119
|
}
|
|
942
|
-
|
|
1120
|
+
// A by-value element needs a sized cell, which only a C array being BUILT has.
|
|
1121
|
+
// GArray is deliberately excluded alongside the lists: its elements are sized,
|
|
1122
|
+
// but g_array_append_vals copies from a buffer this path would have to size the
|
|
1123
|
+
// same way, and no installed typelib has a by-value-element GArray as IN.
|
|
1124
|
+
bool byValueOk = use == ContainerUse::kIn &&
|
|
1125
|
+
gi_type_info_get_tag(type) == GI_TYPE_TAG_ARRAY &&
|
|
1126
|
+
gi_type_info_get_array_type(type) == GI_ARRAY_TYPE_C;
|
|
1127
|
+
bool ok = IsSupportedElementType(elem, why, byValueOk);
|
|
943
1128
|
gi_base_info_unref(elem);
|
|
944
1129
|
return ok;
|
|
945
1130
|
}
|
|
@@ -954,7 +1139,7 @@ bool IsSupportedContainerType(GITypeInfo* type, std::string* why) {
|
|
|
954
1139
|
ktag == GI_TYPE_TAG_INT8 || ktag == GI_TYPE_TAG_UINT8 ||
|
|
955
1140
|
ktag == GI_TYPE_TAG_INT16 || ktag == GI_TYPE_TAG_UINT16 ||
|
|
956
1141
|
ktag == GI_TYPE_TAG_INT32 || ktag == GI_TYPE_TAG_UINT32;
|
|
957
|
-
bool vok = vt != nullptr && IsSupportedElementType(vt, nullptr);
|
|
1142
|
+
bool vok = vt != nullptr && IsSupportedElementType(vt, nullptr, false);
|
|
958
1143
|
if (!kok && why != nullptr) *why = "unsupported GHashTable key";
|
|
959
1144
|
else if (!vok && why != nullptr) *why = "unsupported GHashTable value";
|
|
960
1145
|
if (kt != nullptr) gi_base_info_unref(kt);
|
|
@@ -988,6 +1173,88 @@ size_t CElementSize(GITypeInfo* elem) {
|
|
|
988
1173
|
}
|
|
989
1174
|
}
|
|
990
1175
|
|
|
1176
|
+
// Storage size of one C-array element on the IN path, INCLUDING the by-value
|
|
1177
|
+
// interface elements CElementSize declines to size.
|
|
1178
|
+
//
|
|
1179
|
+
// Deliberately NOT folded into CElementSize, and the split is a scope statement rather
|
|
1180
|
+
// than duplication: CElementSize is also the READ stride (GIArrayToJs), where a
|
|
1181
|
+
// by-value interface element is still unreadable — ReadCElement dereferences it as a
|
|
1182
|
+
// pointer, which is why ElementsAreReadable exists. Teaching CElementSize the true
|
|
1183
|
+
// size there would turn a wrong stride in front of a wrong read into a RIGHT stride in
|
|
1184
|
+
// front of a wrong read: more plausible output, the same defect. Opening the read side
|
|
1185
|
+
// is separate work (this file records it at the inline-record field path and calls.cc
|
|
1186
|
+
// records it at CALLER_ALLOCATES); until then the IN path answers for itself. Every
|
|
1187
|
+
// non-interface tag delegates, so the two answers cannot drift.
|
|
1188
|
+
//
|
|
1189
|
+
// An enum's size is its OWN storage type, not a hardcoded 4: gi_enum_info_get_storage
|
|
1190
|
+
// _type is what the typelib records for the C enum, and gjs's sizeof(unsigned int) is
|
|
1191
|
+
// an assumption this does not have to inherit.
|
|
1192
|
+
static size_t CInElementSize(GITypeInfo* elem) {
|
|
1193
|
+
if (gi_type_info_get_tag(elem) != GI_TYPE_TAG_INTERFACE) return CElementSize(elem);
|
|
1194
|
+
if (gi_type_info_is_pointer(elem)) return sizeof(gpointer);
|
|
1195
|
+
GIBaseInfo* iface = gi_type_info_get_interface(elem);
|
|
1196
|
+
if (iface == nullptr) return sizeof(gpointer);
|
|
1197
|
+
size_t size;
|
|
1198
|
+
if (GI_IS_ENUM_INFO(iface) || GI_IS_FLAGS_INFO(iface)) {
|
|
1199
|
+
switch (gi_enum_info_get_storage_type(reinterpret_cast<GIEnumInfo*>(iface))) {
|
|
1200
|
+
case GI_TYPE_TAG_INT8:
|
|
1201
|
+
case GI_TYPE_TAG_UINT8: size = 1; break;
|
|
1202
|
+
case GI_TYPE_TAG_INT16:
|
|
1203
|
+
case GI_TYPE_TAG_UINT16: size = 2; break;
|
|
1204
|
+
case GI_TYPE_TAG_INT64:
|
|
1205
|
+
case GI_TYPE_TAG_UINT64: size = 8; break;
|
|
1206
|
+
default: size = 4; break; // int32/uint32 and anything unannotated
|
|
1207
|
+
}
|
|
1208
|
+
} else if (GI_IS_STRUCT_INFO(iface)) {
|
|
1209
|
+
size = gi_struct_info_get_size(reinterpret_cast<GIStructInfo*>(iface));
|
|
1210
|
+
} else if (GI_IS_UNION_INFO(iface)) {
|
|
1211
|
+
size = gi_union_info_get_size(reinterpret_cast<GIUnionInfo*>(iface));
|
|
1212
|
+
} else {
|
|
1213
|
+
size = sizeof(gpointer); // object/interface instance: one pointer slot
|
|
1214
|
+
}
|
|
1215
|
+
gi_base_info_unref(iface);
|
|
1216
|
+
return size;
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
// Whether an element is a by-value RECORD (struct or union) — the one kind that must
|
|
1220
|
+
// not travel through a GIArgument. See ByValueRecordToCell.
|
|
1221
|
+
static bool IsByValueRecordElement(GITypeInfo* elem) {
|
|
1222
|
+
if (gi_type_info_get_tag(elem) != GI_TYPE_TAG_INTERFACE) return false;
|
|
1223
|
+
if (gi_type_info_is_pointer(elem)) return false;
|
|
1224
|
+
GIBaseInfo* iface = gi_type_info_get_interface(elem);
|
|
1225
|
+
if (iface == nullptr) return false;
|
|
1226
|
+
bool record = GI_IS_STRUCT_INFO(iface) || GI_IS_UNION_INFO(iface);
|
|
1227
|
+
gi_base_info_unref(iface);
|
|
1228
|
+
return record;
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
// The GType of a by-value record element, or G_TYPE_INVALID for an unregistered one.
|
|
1232
|
+
static GType ByValueRecordGType(GITypeInfo* elem) {
|
|
1233
|
+
GIBaseInfo* iface = gi_type_info_get_interface(elem);
|
|
1234
|
+
if (iface == nullptr) return G_TYPE_INVALID;
|
|
1235
|
+
GType gtype = GI_IS_REGISTERED_TYPE_INFO(iface)
|
|
1236
|
+
? gi_registered_type_info_get_g_type(
|
|
1237
|
+
reinterpret_cast<GIRegisteredTypeInfo*>(iface))
|
|
1238
|
+
: G_TYPE_INVALID;
|
|
1239
|
+
gi_base_info_unref(iface);
|
|
1240
|
+
return gtype == G_TYPE_NONE ? G_TYPE_INVALID : gtype;
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
// Whether a by-value record element array is one this path OWNS the contents of —
|
|
1244
|
+
// true only for GValue, the one element kind written by initialising in place rather
|
|
1245
|
+
// than by copying a caller's bytes. FreeInContainer needs the same answer, so it is
|
|
1246
|
+
// derived from the type both times instead of carried in InContainer: a flag would be
|
|
1247
|
+
// a second truth that a later element kind could get wrong in only one of the places.
|
|
1248
|
+
static bool ElementsAreOwnedGValues(GITypeInfo* type) {
|
|
1249
|
+
if (gi_type_info_get_tag(type) != GI_TYPE_TAG_ARRAY) return false;
|
|
1250
|
+
if (gi_type_info_get_array_type(type) != GI_ARRAY_TYPE_C) return false;
|
|
1251
|
+
GITypeInfo* elem = gi_type_info_get_param_type(type, 0);
|
|
1252
|
+
if (elem == nullptr) return false;
|
|
1253
|
+
bool owned = IsByValueRecordElement(elem) && ByValueRecordGType(elem) == G_TYPE_VALUE;
|
|
1254
|
+
gi_base_info_unref(elem);
|
|
1255
|
+
return owned;
|
|
1256
|
+
}
|
|
1257
|
+
|
|
991
1258
|
// Write the array's element count into a length arg's GIArgument slot (IN length
|
|
992
1259
|
// autofill). The field is selected by the length arg's own tag; the slot is
|
|
993
1260
|
// zero-initialised so writing the matching field is correct on little-endian
|
|
@@ -1006,6 +1273,29 @@ void WriteLengthValue(GITypeInfo* lenType, GIArgument* slot, long n) {
|
|
|
1006
1273
|
}
|
|
1007
1274
|
}
|
|
1008
1275
|
|
|
1276
|
+
// Two array arguments can name the SAME length argument — `Gtk.Accessible
|
|
1277
|
+
// .update_property(n, properties[], values[])` is the shape, and `update_state` and
|
|
1278
|
+
// `update_relation` repeat it. The autofill writes that argument once per array, so the
|
|
1279
|
+
// LAST array silently decides the count the callee then reads BOTH arrays by; a shorter
|
|
1280
|
+
// one is read past its end, inside the callee, with nothing on this side to attribute it
|
|
1281
|
+
// to afterwards.
|
|
1282
|
+
//
|
|
1283
|
+
// gjs does not check this — measured on 1.88.1: `update_property([LABEL, DESCRIPTION],
|
|
1284
|
+
// ['only one'])` is accepted and reads out of bounds. Refusing it is therefore a
|
|
1285
|
+
// deliberate divergence, and the cheaper side of one: the caller passed two arrays that
|
|
1286
|
+
// cannot both be right, and the alternative to a TypeError is an overread.
|
|
1287
|
+
bool RecordLengthWrite(std::vector<LengthWrite>* seen, unsigned int index, long count,
|
|
1288
|
+
long* other) {
|
|
1289
|
+
for (const LengthWrite& w : *seen) {
|
|
1290
|
+
if (w.index != index) continue;
|
|
1291
|
+
if (w.count == count) return true;
|
|
1292
|
+
if (other != nullptr) *other = w.count;
|
|
1293
|
+
return false;
|
|
1294
|
+
}
|
|
1295
|
+
seen->push_back(LengthWrite{index, count});
|
|
1296
|
+
return true;
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1009
1299
|
// Read a length value the callee wrote into a length arg's slot (OUT length).
|
|
1010
1300
|
static long ReadLengthValue(GITypeInfo* lenType, GIArgument* slot) {
|
|
1011
1301
|
switch (gi_type_info_get_tag(lenType)) {
|
|
@@ -1285,10 +1575,14 @@ Napi::Value ReadOutOrReturn(Napi::Env env, GICallableInfo* callable, GITypeInfo*
|
|
|
1285
1575
|
}
|
|
1286
1576
|
}
|
|
1287
1577
|
|
|
1288
|
-
// Fill a GIArgument for a single list/hash element from a JS value. Strings
|
|
1289
|
-
// g_strdup'd (the container owns them)
|
|
1290
|
-
//
|
|
1291
|
-
|
|
1578
|
+
// Fill a GIArgument for a single array/list/hash element from a JS value. Strings
|
|
1579
|
+
// are g_strdup'd (the container owns them). `elemTransfer` is the ELEMENT half of
|
|
1580
|
+
// the container's transfer (see the OWNERSHIP note above): NOTHING → an object /
|
|
1581
|
+
// boxed handle contributes its BORROWED pointer; EVERYTHING → the callee adopts
|
|
1582
|
+
// the element, so it gets a ref (objects) or an independent copy (boxed) instead.
|
|
1583
|
+
// Throws + returns false on an unsupported element.
|
|
1584
|
+
static bool ElementToGIArgument(Napi::Env env, GITypeInfo* elem, Napi::Value v, GIArgument* a,
|
|
1585
|
+
GITransfer elemTransfer) {
|
|
1292
1586
|
memset(a, 0, sizeof(*a));
|
|
1293
1587
|
switch (gi_type_info_get_tag(elem)) {
|
|
1294
1588
|
// Scalar coercions via the terminate-safe helpers (common.h): a swallowed
|
|
@@ -1312,15 +1606,61 @@ static bool ElementToGIArgument(Napi::Env env, GITypeInfo* elem, Napi::Value v,
|
|
|
1312
1606
|
: g_strdup(NodeGiToUtf8(v).c_str());
|
|
1313
1607
|
return true;
|
|
1314
1608
|
case GI_TYPE_TAG_INTERFACE: {
|
|
1609
|
+
// A BY-VALUE enum/flags element is a plain JS number, and it is the one interface
|
|
1610
|
+
// element that legitimately reaches this function without a handle. It writes the
|
|
1611
|
+
// member's integer into the union's low bytes; JsToCArray then copies exactly
|
|
1612
|
+
// CInElementSize bytes, which for an enum is its own storage size (1/2/4/8) and
|
|
1613
|
+
// therefore never more than the union holds. Flags take the unsigned field for
|
|
1614
|
+
// the same reason g_value_set_flags does — a mask with the top bit set must not
|
|
1615
|
+
// become negative on its way through.
|
|
1616
|
+
if (!gi_type_info_is_pointer(elem)) {
|
|
1617
|
+
GIBaseInfo* enumIface = gi_type_info_get_interface(elem);
|
|
1618
|
+
if (enumIface != nullptr &&
|
|
1619
|
+
(GI_IS_ENUM_INFO(enumIface) || GI_IS_FLAGS_INFO(enumIface))) {
|
|
1620
|
+
bool isFlags = GI_IS_FLAGS_INFO(enumIface);
|
|
1621
|
+
gi_base_info_unref(enumIface);
|
|
1622
|
+
if (isFlags) {
|
|
1623
|
+
a->v_uint32 = NodeGiToUint32(v);
|
|
1624
|
+
} else {
|
|
1625
|
+
a->v_int32 = NodeGiToInt32(v);
|
|
1626
|
+
}
|
|
1627
|
+
return true;
|
|
1628
|
+
}
|
|
1629
|
+
if (enumIface != nullptr) gi_base_info_unref(enumIface);
|
|
1630
|
+
}
|
|
1315
1631
|
if (v.IsNull() || v.IsUndefined()) {
|
|
1316
1632
|
a->v_pointer = nullptr;
|
|
1317
1633
|
return true;
|
|
1318
1634
|
}
|
|
1319
1635
|
if (v.IsExternal() && v.As<Napi::External<GObject>>().CheckTypeTag(&kGObjectHandleTag)) {
|
|
1320
|
-
|
|
1636
|
+
GObject* obj = v.As<Napi::External<GObject>>().Data();
|
|
1637
|
+
// The callee adopts one ref PER ELEMENT on EVERYTHING (Gdk.ContentProvider
|
|
1638
|
+
// .new_union's `providers`, Gtk.ClosureExpression.new's `params`, the
|
|
1639
|
+
// Gst/GstPbutils `*_list_free` helpers): hand it one it may drop, never the
|
|
1640
|
+
// wrapper's own — that would under-ref and free the object out from under JS.
|
|
1641
|
+
if (obj != nullptr && elemTransfer == GI_TRANSFER_EVERYTHING) g_object_ref(obj);
|
|
1642
|
+
a->v_pointer = obj;
|
|
1321
1643
|
return true;
|
|
1322
1644
|
}
|
|
1323
|
-
|
|
1645
|
+
// A boxed/struct handle contributes its pointer, exactly as an object handle
|
|
1646
|
+
// does. Only reachable for a POINTER struct element — IsSupportedElementType
|
|
1647
|
+
// refuses the by-value kind before the invoke, so no by-value record can arrive
|
|
1648
|
+
// here and be silently treated as a pointer. TransferBoxedIn is the SAME
|
|
1649
|
+
// adopt-or-borrow helper the scalar boxed IN arg uses (g_variant_ref /
|
|
1650
|
+
// g_boxed_copy on EVERYTHING, relinquish for a struct with no copy function),
|
|
1651
|
+
// so a container element and a plain argument cannot drift apart.
|
|
1652
|
+
if (BoxedHandle* boxed = TryGetBoxedHandle(v); boxed != nullptr) {
|
|
1653
|
+
a->v_pointer = TransferBoxedIn(boxed, elemTransfer);
|
|
1654
|
+
return true;
|
|
1655
|
+
}
|
|
1656
|
+
// Everything else refuses LOUDLY here rather than silently contributing a
|
|
1657
|
+
// nullptr. Two element kinds reach this on purpose: a GObject FUNDAMENTAL
|
|
1658
|
+
// (GskRenderNode — object info, so the predicate admits it, but its handle
|
|
1659
|
+
// carries kFundamentalHandleTag) and a FOREIGN struct (cairo, kCairoHandleTag),
|
|
1660
|
+
// neither of which owns its pointer the way the two branches above assume. No
|
|
1661
|
+
// installed typelib has a foreign-struct container element today; if one is
|
|
1662
|
+
// added, route it through ForeignStructOps::to rather than widening this test.
|
|
1663
|
+
Napi::TypeError::New(env, "expected a GObject or boxed handle as a container element")
|
|
1324
1664
|
.ThrowAsJavaScriptException();
|
|
1325
1665
|
return false;
|
|
1326
1666
|
}
|
|
@@ -1331,20 +1671,132 @@ static bool ElementToGIArgument(Napi::Env env, GITypeInfo* elem, Napi::Value v,
|
|
|
1331
1671
|
}
|
|
1332
1672
|
}
|
|
1333
1673
|
|
|
1674
|
+
// The introspected size of a record handle's own type, or 0 when it has none.
|
|
1675
|
+
static size_t BoxedInfoSize(GIBaseInfo* info) {
|
|
1676
|
+
if (info == nullptr) return 0;
|
|
1677
|
+
if (GI_IS_STRUCT_INFO(info)) return gi_struct_info_get_size(reinterpret_cast<GIStructInfo*>(info));
|
|
1678
|
+
if (GI_IS_UNION_INFO(info)) return gi_union_info_get_size(reinterpret_cast<GIUnionInfo*>(info));
|
|
1679
|
+
return 0;
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
// Write ONE by-value record element straight into its C-array cell.
|
|
1683
|
+
//
|
|
1684
|
+
// WHY THIS IS NOT A GIArgument, which is the whole reason the function exists. The
|
|
1685
|
+
// array write loop copies `elemSize` bytes out of a GIArgument — a union, eight bytes
|
|
1686
|
+
// wide. A by-value record is its own size: GValue is 24 on this ABI, Gio.ActionEntry
|
|
1687
|
+
// 64. Sizing the loop without moving the record off the union would read sixteen bytes
|
|
1688
|
+
// past it, off the stack, once per element, and would produce a green build with
|
|
1689
|
+
// plausible-looking output. So a record never enters a GIArgument; its bytes are
|
|
1690
|
+
// written where they finally live.
|
|
1691
|
+
//
|
|
1692
|
+
// TWO SOURCES, and only one of them leaves us owning anything:
|
|
1693
|
+
// * GObject.Value — INITIALISED IN PLACE, either from any JS value (gjs 1.88.1
|
|
1694
|
+
// accepts `update_property([Gtk.AccessibleProperty.LABEL], ['text'])`, measured)
|
|
1695
|
+
// or by copying an existing GObject.Value handle. Both end with a GValue THIS call
|
|
1696
|
+
// constructed, so it is unset after the invoke — see ElementsAreOwnedGValues and
|
|
1697
|
+
// FreeInContainer. Copying rather than memcpy'ing a handle's GValue is what makes
|
|
1698
|
+
// that rule uniform: a bitwise copy would share the string or boxed the original
|
|
1699
|
+
// holds, and unsetting it would free the handle's contents underneath JS.
|
|
1700
|
+
// * every other record — COPIED from a boxed handle, which is also what gjs requires
|
|
1701
|
+
// (measured: a plain object is refused there with "not a subclass of
|
|
1702
|
+
// GObject_Struct"). The copy shares whatever the source points at, so nothing here
|
|
1703
|
+
// is freed and the handle keeps owning its own storage across the invoke.
|
|
1704
|
+
//
|
|
1705
|
+
// THE TYPE CHECK IS A SAFETY PROPERTY rather than ergonomics: copying `elemSize` bytes
|
|
1706
|
+
// out of a handle of a DIFFERENT record type reads past the end of the source whenever
|
|
1707
|
+
// the source is smaller. So the source must be the element's own type — compared by
|
|
1708
|
+
// GType where the element has one, and by introspected size where it does not, an
|
|
1709
|
+
// unregistered struct having no GType to compare.
|
|
1710
|
+
static bool ByValueRecordToCell(Napi::Env env, GITypeInfo* elem, Napi::Value v, void* dst,
|
|
1711
|
+
size_t elemSize, gpointer* writeBackTo) {
|
|
1712
|
+
if (writeBackTo != nullptr) *writeBackTo = nullptr;
|
|
1713
|
+
GType want = ByValueRecordGType(elem);
|
|
1714
|
+
if (want == G_TYPE_VALUE) {
|
|
1715
|
+
GValue* cell = static_cast<GValue*>(dst);
|
|
1716
|
+
memset(cell, 0, elemSize);
|
|
1717
|
+
if (NodeGiIsGValueHandle(v)) {
|
|
1718
|
+
BoxedHandle* h = TryGetBoxedHandle(v);
|
|
1719
|
+
GValue* src = h == nullptr ? nullptr : static_cast<GValue*>(h->ptr);
|
|
1720
|
+
if (src == nullptr || !G_IS_VALUE(src)) {
|
|
1721
|
+
Napi::TypeError::New(env,
|
|
1722
|
+
"an uninitialised GObject.Value cannot be an array element")
|
|
1723
|
+
.ThrowAsJavaScriptException();
|
|
1724
|
+
return false;
|
|
1725
|
+
}
|
|
1726
|
+
g_value_init(cell, G_VALUE_TYPE(src));
|
|
1727
|
+
g_value_copy(src, cell);
|
|
1728
|
+
if (writeBackTo != nullptr) *writeBackTo = src;
|
|
1729
|
+
return true;
|
|
1730
|
+
}
|
|
1731
|
+
return JsToFreshGValue(env, v, cell);
|
|
1732
|
+
}
|
|
1733
|
+
|
|
1734
|
+
BoxedHandle* h = TryGetBoxedHandle(v);
|
|
1735
|
+
if (h == nullptr || h->ptr == nullptr) {
|
|
1736
|
+
Napi::TypeError::New(
|
|
1737
|
+
env, std::string("expected a ") +
|
|
1738
|
+
(want == G_TYPE_INVALID ? "record" : g_type_name(want)) +
|
|
1739
|
+
" handle as a by-value array element")
|
|
1740
|
+
.ThrowAsJavaScriptException();
|
|
1741
|
+
return false;
|
|
1742
|
+
}
|
|
1743
|
+
bool sameType = want != G_TYPE_INVALID
|
|
1744
|
+
? (h->gtype != G_TYPE_INVALID && g_type_is_a(h->gtype, want))
|
|
1745
|
+
: (BoxedInfoSize(h->info) == elemSize && elemSize > 0);
|
|
1746
|
+
if (!sameType) {
|
|
1747
|
+
Napi::TypeError::New(
|
|
1748
|
+
env, std::string("a by-value array element must be a ") +
|
|
1749
|
+
(want == G_TYPE_INVALID ? "record of the same size" : g_type_name(want)) +
|
|
1750
|
+
", got " +
|
|
1751
|
+
(h->gtype == G_TYPE_INVALID ? "an unregistered record" : g_type_name(h->gtype)))
|
|
1752
|
+
.ThrowAsJavaScriptException();
|
|
1753
|
+
return false;
|
|
1754
|
+
}
|
|
1755
|
+
memcpy(dst, h->ptr, elemSize);
|
|
1756
|
+
return true;
|
|
1757
|
+
}
|
|
1758
|
+
|
|
1334
1759
|
// Build a C array / GStrv / GByteArray / GArray / GPtrArray from a JS value.
|
|
1335
1760
|
// *outCount = element count (for the IN length autofill + later free). Throws +
|
|
1336
1761
|
// returns false on refusal (BEFORE the invoke).
|
|
1337
|
-
static bool JsToCArray(Napi::Env env, Napi::Value v, GITypeInfo* type,
|
|
1338
|
-
long* outCount) {
|
|
1762
|
+
static bool JsToCArray(Napi::Env env, Napi::Value v, GITypeInfo* type, GITransfer transfer,
|
|
1763
|
+
gpointer* outPtr, long* outCount, std::vector<gpointer>* writeBack) {
|
|
1764
|
+
GITransfer elemTransfer =
|
|
1765
|
+
transfer == GI_TRANSFER_EVERYTHING ? GI_TRANSFER_EVERYTHING : GI_TRANSFER_NOTHING;
|
|
1339
1766
|
GIArrayType at = gi_type_info_get_array_type(type);
|
|
1340
1767
|
GITypeInfo* elem = gi_type_info_get_param_type(type, 0);
|
|
1341
1768
|
GITypeTag etag = gi_type_info_get_tag(elem);
|
|
1342
1769
|
bool zt = gi_type_info_is_zero_terminated(type);
|
|
1343
|
-
|
|
1770
|
+
// CInElementSize, not CElementSize: this is the write side, and it is the side that
|
|
1771
|
+
// knows how wide a by-value element's cell is (see the note on that function).
|
|
1772
|
+
size_t elemSize = CInElementSize(elem);
|
|
1773
|
+
bool byValueRecord = IsByValueRecordElement(elem);
|
|
1344
1774
|
bool isByte = etag == GI_TYPE_TAG_UINT8 || etag == GI_TYPE_TAG_INT8;
|
|
1345
1775
|
*outPtr = nullptr;
|
|
1346
1776
|
*outCount = 0;
|
|
1347
1777
|
|
|
1778
|
+
// A by-value record element is a BITWISE copy of a handle's storage, which shares
|
|
1779
|
+
// whatever that record points at. That is safe exactly while WE free the buffer:
|
|
1780
|
+
// on TRANSFER_EVERYTHING the callee frees the elements too, and it would be freeing
|
|
1781
|
+
// strings and boxeds the caller's handles still own — the same use-after-free the
|
|
1782
|
+
// pointer-element half of this work shipped once and had to fix, wearing a different
|
|
1783
|
+
// shape. There is no general remedy: an arbitrary record has no copy function to
|
|
1784
|
+
// deep-copy with.
|
|
1785
|
+
//
|
|
1786
|
+
// Measured across every installed typelib rather than guessed: of 140 IN parameters
|
|
1787
|
+
// with a by-value element, 139 are `transfer=none` and ONE is not — Gsf
|
|
1788
|
+
// .property_settings_free's `GObject.Parameter[]`, which exists to free what it is
|
|
1789
|
+
// given. So this refusal costs one callable, and that callable is the one where
|
|
1790
|
+
// handing over borrowed contents would be exactly wrong.
|
|
1791
|
+
if (byValueRecord && transfer != GI_TRANSFER_NOTHING) {
|
|
1792
|
+
gi_base_info_unref(elem);
|
|
1793
|
+
Napi::TypeError::New(
|
|
1794
|
+
env, "a by-value record array with transfer other than none is not yet supported: the "
|
|
1795
|
+
"callee would free element contents the caller's handles still own")
|
|
1796
|
+
.ThrowAsJavaScriptException();
|
|
1797
|
+
return false;
|
|
1798
|
+
}
|
|
1799
|
+
|
|
1348
1800
|
// null/undefined → a NULL array (count 0), as GJS marshals a null array arg
|
|
1349
1801
|
// (refs/gjs/gi/arg.cpp gjs_array_to_explicit_array: null in → NULL out).
|
|
1350
1802
|
// Exposing call: `Gst.init(null)` — a NULLABLE (inout) argv array every GJS
|
|
@@ -1454,18 +1906,47 @@ static bool JsToCArray(Napi::Env env, Napi::Value v, GITypeInfo* type, gpointer*
|
|
|
1454
1906
|
// pdata memcpy (refs/gjs/gi/arg.cpp gjs_value_to_basic_array_gi_argument).
|
|
1455
1907
|
void* buf = g_malloc0(elemSize * (count + (zt ? 1 : 0)));
|
|
1456
1908
|
bool ok = true;
|
|
1909
|
+
long written = 0;
|
|
1457
1910
|
for (long i = 0; i < count && ok; i++) {
|
|
1911
|
+
void* dst = static_cast<char*>(buf) + i * elemSize;
|
|
1912
|
+
if (byValueRecord) {
|
|
1913
|
+
// The record writes ITSELF into the cell — it never passes through a GIArgument,
|
|
1914
|
+
// because the union is eight bytes and the record is its own size.
|
|
1915
|
+
gpointer source = nullptr;
|
|
1916
|
+
ok = ByValueRecordToCell(env, elem, arr.Get(static_cast<uint32_t>(i)), dst, elemSize,
|
|
1917
|
+
&source);
|
|
1918
|
+
if (ok) {
|
|
1919
|
+
if (writeBack != nullptr) {
|
|
1920
|
+
writeBack->resize(static_cast<size_t>(i) + 1, nullptr);
|
|
1921
|
+
(*writeBack)[static_cast<size_t>(i)] = source;
|
|
1922
|
+
}
|
|
1923
|
+
written++;
|
|
1924
|
+
}
|
|
1925
|
+
continue;
|
|
1926
|
+
}
|
|
1458
1927
|
GIArgument a;
|
|
1459
|
-
ok = ElementToGIArgument(env, elem, arr.Get(static_cast<uint32_t>(i)), &a);
|
|
1928
|
+
ok = ElementToGIArgument(env, elem, arr.Get(static_cast<uint32_t>(i)), &a, elemTransfer);
|
|
1460
1929
|
if (!ok) break;
|
|
1461
|
-
void* dst = static_cast<char*>(buf) + i * elemSize;
|
|
1462
1930
|
// Copy the element's storage bytes (LE: the low elemSize bytes of the union
|
|
1463
1931
|
// alias the active field — v_string/v_pointer for pointers, the scalar
|
|
1464
|
-
// otherwise).
|
|
1932
|
+
// otherwise). elemSize never exceeds the union here: the one element kind that is
|
|
1933
|
+
// wider took the branch above.
|
|
1465
1934
|
memcpy(dst, &a, elemSize);
|
|
1935
|
+
written++;
|
|
1466
1936
|
}
|
|
1467
1937
|
if (!ok) {
|
|
1468
|
-
|
|
1938
|
+
// The GValues already initialised are ours and are released here — the container is
|
|
1939
|
+
// never recorded for cleanup on this path, so nothing else will. (Partial
|
|
1940
|
+
// g_strdup'd strings, and on an EVERYTHING container the refs/copies taken for
|
|
1941
|
+
// earlier elements, still leak: pre-checked and rare, needing a non-handle value
|
|
1942
|
+
// mid-array. A leak, never a double free.)
|
|
1943
|
+
if (byValueRecord && ElementsAreOwnedGValues(type)) {
|
|
1944
|
+
for (long i = 0; i < written; i++) {
|
|
1945
|
+
GValue* cell = reinterpret_cast<GValue*>(static_cast<char*>(buf) + i * elemSize);
|
|
1946
|
+
if (G_IS_VALUE(cell)) g_value_unset(cell);
|
|
1947
|
+
}
|
|
1948
|
+
}
|
|
1949
|
+
g_free(buf);
|
|
1469
1950
|
gi_base_info_unref(elem);
|
|
1470
1951
|
return false;
|
|
1471
1952
|
}
|
|
@@ -1498,7 +1979,9 @@ static bool JsToCArray(Napi::Env env, Napi::Value v, GITypeInfo* type, gpointer*
|
|
|
1498
1979
|
|
|
1499
1980
|
// Build a GList / GSList from a JS array.
|
|
1500
1981
|
static bool JsToGListLike(Napi::Env env, Napi::Value v, GITypeInfo* type, bool isSList,
|
|
1501
|
-
gpointer* outPtr) {
|
|
1982
|
+
GITransfer transfer, gpointer* outPtr) {
|
|
1983
|
+
GITransfer elemTransfer =
|
|
1984
|
+
transfer == GI_TRANSFER_EVERYTHING ? GI_TRANSFER_EVERYTHING : GI_TRANSFER_NOTHING;
|
|
1502
1985
|
*outPtr = nullptr;
|
|
1503
1986
|
if (!v.IsArray()) {
|
|
1504
1987
|
Napi::TypeError::New(env, "expected an array for the list argument").ThrowAsJavaScriptException();
|
|
@@ -1511,8 +1994,12 @@ static bool JsToGListLike(Napi::Env env, Napi::Value v, GITypeInfo* type, bool i
|
|
|
1511
1994
|
bool ok = true;
|
|
1512
1995
|
for (uint32_t i = 0; i < arr.Length() && ok; i++) {
|
|
1513
1996
|
GIArgument a;
|
|
1514
|
-
ok = ElementToGIArgument(env, elem, arr.Get(i), &a);
|
|
1997
|
+
ok = ElementToGIArgument(env, elem, arr.Get(i), &a, elemTransfer);
|
|
1515
1998
|
if (!ok) break;
|
|
1999
|
+
// Measured, not assumed: for an INTERFACE element that is a pointer this helper
|
|
2000
|
+
// passes v_pointer through unchanged and gi_type_info_argument_from_hash_pointer
|
|
2001
|
+
// reads the same address back, so a pointer-struct element fills a node's data
|
|
2002
|
+
// slot exactly as an object element does.
|
|
1516
2003
|
gpointer p = gi_type_info_hash_pointer_from_argument(elem, &a);
|
|
1517
2004
|
if (!isSList) glist = g_list_prepend(glist, p);
|
|
1518
2005
|
else gslist = g_slist_prepend(gslist, p);
|
|
@@ -1534,7 +2021,14 @@ static bool JsToGListLike(Napi::Env env, Napi::Value v, GITypeInfo* type, bool i
|
|
|
1534
2021
|
// Key/value GDestroyNotify funcs are attached so a transfer-none teardown
|
|
1535
2022
|
// (g_hash_table_unref in FreeInContainer) releases everything; a transfer-full
|
|
1536
2023
|
// hash is adopted by the callee, which frees it (and thus runs the notifies).
|
|
1537
|
-
static bool JsToGHashIn(Napi::Env env, Napi::Value v, GITypeInfo* type,
|
|
2024
|
+
static bool JsToGHashIn(Napi::Env env, Napi::Value v, GITypeInfo* type, GITransfer transfer,
|
|
2025
|
+
gpointer* outPtr) {
|
|
2026
|
+
// A hash's VALUE destroy-notify is set below for the string/heap kinds only, so an
|
|
2027
|
+
// EVERYTHING hash of object/boxed values leaks the copies rather than double-freeing
|
|
2028
|
+
// the originals. That is the safe side of the trade and the only one reachable: no
|
|
2029
|
+
// installed typelib has an EVERYTHING GHashTable IN with a POINTER-STRUCT value.
|
|
2030
|
+
GITransfer elemTransfer =
|
|
2031
|
+
transfer == GI_TRANSFER_EVERYTHING ? GI_TRANSFER_EVERYTHING : GI_TRANSFER_NOTHING;
|
|
1538
2032
|
*outPtr = nullptr;
|
|
1539
2033
|
if (!v.IsObject() || v.IsArray()) {
|
|
1540
2034
|
Napi::TypeError::New(env, "expected an object for the GHashTable argument")
|
|
@@ -1578,7 +2072,7 @@ static bool JsToGHashIn(Napi::Env env, Napi::Value v, GITypeInfo* type, gpointer
|
|
|
1578
2072
|
} else {
|
|
1579
2073
|
GIArgument ka;
|
|
1580
2074
|
Napi::Value keyNum = Napi::Number::New(env, static_cast<double>(g_ascii_strtoll(ks.c_str(), nullptr, 10)));
|
|
1581
|
-
ok = ElementToGIArgument(env, kt, keyNum, &ka);
|
|
2075
|
+
ok = ElementToGIArgument(env, kt, keyNum, &ka, GI_TRANSFER_NOTHING);
|
|
1582
2076
|
if (!ok) break;
|
|
1583
2077
|
kp = gi_type_info_hash_pointer_from_argument(kt, &ka);
|
|
1584
2078
|
}
|
|
@@ -1589,7 +2083,7 @@ static bool JsToGHashIn(Napi::Env env, Napi::Value v, GITypeInfo* type, gpointer
|
|
|
1589
2083
|
Napi::Value val = obj.Get(key);
|
|
1590
2084
|
gpointer vp = nullptr;
|
|
1591
2085
|
GIArgument va;
|
|
1592
|
-
ok = ElementToGIArgument(env, vt, val, &va);
|
|
2086
|
+
ok = ElementToGIArgument(env, vt, val, &va, elemTransfer);
|
|
1593
2087
|
if (!ok) {
|
|
1594
2088
|
g_free(kp);
|
|
1595
2089
|
break;
|
|
@@ -1661,6 +2155,44 @@ void FreeInContainer(const InContainer& c) {
|
|
|
1661
2155
|
g_free(s);
|
|
1662
2156
|
}
|
|
1663
2157
|
} else {
|
|
2158
|
+
// A GValue element array is the one C array whose CELLS hold something beyond
|
|
2159
|
+
// their own bytes: JsToCArray initialised each one (from a JS value or by
|
|
2160
|
+
// copying a handle's GValue), so each one holds a string, a boxed or a ref that
|
|
2161
|
+
// g_free on the buffer would strand. Only this transfer reaches here — on
|
|
2162
|
+
// EVERYTHING/CONTAINER the callee owns the buffer and unsets them itself.
|
|
2163
|
+
if (ElementsAreOwnedGValues(c.type)) {
|
|
2164
|
+
size_t elemSize = elem != nullptr ? CInElementSize(elem) : 0;
|
|
2165
|
+
for (long i = 0; elemSize > 0 && i < c.count; i++) {
|
|
2166
|
+
GValue* cell = reinterpret_cast<GValue*>(static_cast<char*>(c.ptr) + i * elemSize);
|
|
2167
|
+
if (!G_IS_VALUE(cell)) continue;
|
|
2168
|
+
// WRITE-BACK, and the reason it is not optional. Some callees FILL these
|
|
2169
|
+
// cells rather than read them — `GObject.Object.getv`, `Gst.Object
|
|
2170
|
+
// .get_g_value_array`, `Gst.ControlBinding.get_g_value_array` — and the
|
|
2171
|
+
// typelib gives NO way to tell them apart: measured on `g_object_getv`,
|
|
2172
|
+
// its `values` parameter reports direction=IN, caller-allocates=false,
|
|
2173
|
+
// transfer=none, byte for byte the same flags as `GLib.parse_debug_string`'s
|
|
2174
|
+
// read-only `keys`. Without this, such a call silently does nothing, which
|
|
2175
|
+
// is worse than the refusal it replaced. (gjs has the same gap: `getv`
|
|
2176
|
+
// there also leaves the caller's GValue empty — measured on 1.88.1. This is
|
|
2177
|
+
// a deliberate divergence, and the annotation is an upstream candidate.)
|
|
2178
|
+
//
|
|
2179
|
+
// `g_value_copy` rather than a memcpy: the cell is unset immediately after,
|
|
2180
|
+
// and a bitwise copy would leave the caller's GValue pointing at a string
|
|
2181
|
+
// this line is about to free. The type is matched first because g_value_copy
|
|
2182
|
+
// requires it, and a callee may have set a type the caller did not.
|
|
2183
|
+
GValue* dest = i < static_cast<long>(c.writeBack.size())
|
|
2184
|
+
? static_cast<GValue*>(c.writeBack[static_cast<size_t>(i)])
|
|
2185
|
+
: nullptr;
|
|
2186
|
+
if (dest != nullptr) {
|
|
2187
|
+
if (!G_IS_VALUE(dest) || G_VALUE_TYPE(dest) != G_VALUE_TYPE(cell)) {
|
|
2188
|
+
if (G_IS_VALUE(dest)) g_value_unset(dest);
|
|
2189
|
+
g_value_init(dest, G_VALUE_TYPE(cell));
|
|
2190
|
+
}
|
|
2191
|
+
g_value_copy(cell, dest);
|
|
2192
|
+
}
|
|
2193
|
+
g_value_unset(cell);
|
|
2194
|
+
}
|
|
2195
|
+
}
|
|
1664
2196
|
g_free(c.ptr);
|
|
1665
2197
|
}
|
|
1666
2198
|
}
|
|
@@ -1694,23 +2226,24 @@ bool JsToInContainer(Napi::Env env, Napi::Value v, GITypeInfo* type, GITransfer
|
|
|
1694
2226
|
GITypeTag tag = gi_type_info_get_tag(type);
|
|
1695
2227
|
*outCount = 0;
|
|
1696
2228
|
bool ok = false;
|
|
2229
|
+
std::vector<gpointer> writeBack;
|
|
1697
2230
|
if (tag == GI_TYPE_TAG_ARRAY) {
|
|
1698
|
-
ok = JsToCArray(env, v, type, outPtr, outCount);
|
|
2231
|
+
ok = JsToCArray(env, v, type, transfer, outPtr, outCount, &writeBack);
|
|
1699
2232
|
} else if (tag == GI_TYPE_TAG_GLIST || tag == GI_TYPE_TAG_GSLIST) {
|
|
1700
|
-
ok = JsToGListLike(env, v, type, tag == GI_TYPE_TAG_GSLIST, outPtr);
|
|
2233
|
+
ok = JsToGListLike(env, v, type, tag == GI_TYPE_TAG_GSLIST, transfer, outPtr);
|
|
1701
2234
|
} else if (tag == GI_TYPE_TAG_GHASH) {
|
|
1702
|
-
ok = JsToGHashIn(env, v, type, outPtr);
|
|
2235
|
+
ok = JsToGHashIn(env, v, type, transfer, outPtr);
|
|
1703
2236
|
}
|
|
1704
2237
|
if (ok && *outPtr != nullptr) {
|
|
1705
|
-
containers->push_back(InContainer{
|
|
1706
|
-
|
|
2238
|
+
containers->push_back(InContainer{reinterpret_cast<GITypeInfo*>(gi_base_info_ref(type)),
|
|
2239
|
+
*outPtr, transfer, *outCount, std::move(writeBack)});
|
|
1707
2240
|
} else if (!ok && *outPtr != nullptr) {
|
|
1708
2241
|
// An element conversion threw mid-build (JsToGListLike / JsToGHashIn still
|
|
1709
2242
|
// published the partial container). It was never recorded for cleanup, so
|
|
1710
2243
|
// the nodes (and any g_strdup'd keys/string values) would leak. Free it now
|
|
1711
2244
|
// with NOTHING semantics — we still own all of it; the callee never saw it.
|
|
1712
2245
|
InContainer partial{reinterpret_cast<GITypeInfo*>(gi_base_info_ref(type)), *outPtr,
|
|
1713
|
-
GI_TRANSFER_NOTHING, *outCount};
|
|
2246
|
+
GI_TRANSFER_NOTHING, *outCount, {}};
|
|
1714
2247
|
FreeInContainer(partial); // unrefs the type ref taken above
|
|
1715
2248
|
*outPtr = nullptr;
|
|
1716
2249
|
}
|