@gjsify/node-gi 0.52.0 → 0.53.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 +2 -1
- package/binding.gyp +1 -0
- package/gi.js +34 -1
- package/gtk-runtime.d.ts +31 -0
- package/gtk-runtime.js +159 -6
- package/index.d.ts +14 -0
- package/index.js +12 -0
- package/package.json +13 -13
- 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 +10 -34
- package/src/class.cc +14 -37
- package/src/common.h +38 -0
- package/src/loop.cc +28 -4
- package/src/marshal.cc +371 -9
- package/src/opengl-win32.cc +234 -0
- package/src/toggle.cc +1 -1
- package/system-gi.js +1 -1
package/src/loop.cc
CHANGED
|
@@ -379,8 +379,9 @@ Napi::Value IterateMainContext(const Napi::CallbackInfo& info) {
|
|
|
379
379
|
struct PumpPoll {
|
|
380
380
|
uv_poll_t handle;
|
|
381
381
|
int fd;
|
|
382
|
-
int events;
|
|
383
|
-
gboolean seen;
|
|
382
|
+
int events; // currently-subscribed uv event mask
|
|
383
|
+
gboolean seen; // mark/sweep flag for SyncPumpPolls
|
|
384
|
+
gboolean started; // whether the watcher is currently armed (see PumpPollCb)
|
|
384
385
|
};
|
|
385
386
|
|
|
386
387
|
static gboolean g_pump_inited = FALSE;
|
|
@@ -398,7 +399,26 @@ static gboolean g_pump_poll_warned = FALSE;
|
|
|
398
399
|
// Wake-only callbacks: readiness/expiry just ends libuv's poll sleep; the actual
|
|
399
400
|
// GLib dispatch happens in the check phase of the same loop turn (PumpCheckCb).
|
|
400
401
|
static void PumpTimerCb(uv_timer_t* /*t*/) {}
|
|
401
|
-
|
|
402
|
+
|
|
403
|
+
// A wake-up is an EDGE, so the watcher DISARMS itself on the first fire and
|
|
404
|
+
// PumpArmWakeups re-arms it on the next libuv turn. uv_poll is level-triggered
|
|
405
|
+
// and this callback reads nothing, so a GLib fd that stays ready — a dead D-Bus
|
|
406
|
+
// socket stuck at POLLHUP, or the context's own wakeup eventfd, which
|
|
407
|
+
// `block_source()` re-signals on every dispatch — leaves uv's backend fd
|
|
408
|
+
// PERPETUALLY readable. That fd is embedded in UvLoopSource, so GLib then reads
|
|
409
|
+
// "Node has work" on every prepare, and any foreign drain loop of the shape
|
|
410
|
+
// `while (g_main_context_iteration(ctx, FALSE));` never terminates: measured in
|
|
411
|
+
// `e_source_registry_new_sync`'s failure path (no D-Bus reachable), 6.9 M
|
|
412
|
+
// dispatches in 20 s at 100 % CPU with `uv_backend_timeout()` sitting at ~4.6 s,
|
|
413
|
+
// i.e. never once because Node actually had work. The pump's own drain would
|
|
414
|
+
// have cleared the GLib side, but it no-ops at `g_main_depth() > 0` — exactly
|
|
415
|
+
// where a foreign nested iteration runs. Regression
|
|
416
|
+
// `test/foreign-context-drain.test.mjs`.
|
|
417
|
+
static void PumpPollCb(uv_poll_t* p, int /*status*/, int /*events*/) {
|
|
418
|
+
PumpPoll* pp = reinterpret_cast<PumpPoll*>(p->data);
|
|
419
|
+
uv_poll_stop(p);
|
|
420
|
+
pp->started = FALSE;
|
|
421
|
+
}
|
|
402
422
|
|
|
403
423
|
static void PumpPollCloseCb(uv_handle_t* h) {
|
|
404
424
|
delete reinterpret_cast<PumpPoll*>(h->data);
|
|
@@ -440,9 +460,12 @@ static gboolean SyncPumpPolls(int nfds) {
|
|
|
440
460
|
if (it != g_pump_polls->end()) {
|
|
441
461
|
PumpPoll* pp = it->second;
|
|
442
462
|
pp->seen = TRUE;
|
|
443
|
-
|
|
463
|
+
// Re-arm a watcher that disarmed itself on its last fire, as well as one
|
|
464
|
+
// whose event mask changed.
|
|
465
|
+
if (pp->events != ev || !pp->started) {
|
|
444
466
|
if (uv_poll_start(&pp->handle, ev, PumpPollCb) == 0) {
|
|
445
467
|
pp->events = ev;
|
|
468
|
+
pp->started = TRUE;
|
|
446
469
|
uv_unref(reinterpret_cast<uv_handle_t*>(&pp->handle));
|
|
447
470
|
} else {
|
|
448
471
|
all_ok = FALSE;
|
|
@@ -466,6 +489,7 @@ static gboolean SyncPumpPolls(int nfds) {
|
|
|
466
489
|
all_ok = FALSE;
|
|
467
490
|
continue;
|
|
468
491
|
}
|
|
492
|
+
pp->started = TRUE;
|
|
469
493
|
uv_unref(reinterpret_cast<uv_handle_t*>(&pp->handle));
|
|
470
494
|
(*g_pump_polls)[fd] = pp;
|
|
471
495
|
}
|
package/src/marshal.cc
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
#include "common.h"
|
|
5
5
|
|
|
6
6
|
#include <cmath>
|
|
7
|
+
#include <cstring>
|
|
7
8
|
#include <unordered_map>
|
|
8
9
|
#include <utility>
|
|
9
10
|
|
|
@@ -789,7 +790,7 @@ bool JsToGIArgument(Napi::Env env, Napi::Value v, GITypeInfo* type, GIArgument*
|
|
|
789
790
|
// how an application POSTS the failure the bus accessors read back —
|
|
790
791
|
// `GLib.propagate_error`, and the INOUT `err` of `GLib.prefix_error_literal`.
|
|
791
792
|
// 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
|
+
// keep (gjs refuses both for a non-nullable arg — status/open-todos/README.md).
|
|
793
794
|
if (v.IsNull() || v.IsUndefined()) {
|
|
794
795
|
out->v_pointer = nullptr;
|
|
795
796
|
return true;
|
|
@@ -1040,8 +1041,15 @@ Napi::Value GIArgumentToJs(Napi::Env env, GITypeInfo* type, GIArgument* arg,
|
|
|
1040
1041
|
// unchanged, which is right for a pointer-struct element and meaningless for a
|
|
1041
1042
|
// four-byte enum or a twenty-four-byte record. So the by-value admission is scoped to
|
|
1042
1043
|
// the one container kind that has somewhere to put the bytes.
|
|
1043
|
-
|
|
1044
|
-
|
|
1044
|
+
//
|
|
1045
|
+
// `cArrayCell` is true for a C array in EITHER direction: its elements are sized
|
|
1046
|
+
// cells the IN path writes and ReadCElement reads. A GType element is admitted only
|
|
1047
|
+
// there — a list/hash node would carry it through the hash-pointer helpers, which
|
|
1048
|
+
// this file has never exercised for that tag.
|
|
1049
|
+
static bool IsSupportedElementType(GITypeInfo* elem, std::string* why, bool byValueOk,
|
|
1050
|
+
bool cArrayCell) {
|
|
1051
|
+
GITypeTag tag = gi_type_info_get_tag(elem);
|
|
1052
|
+
switch (tag) {
|
|
1045
1053
|
case GI_TYPE_TAG_BOOLEAN:
|
|
1046
1054
|
case GI_TYPE_TAG_INT8:
|
|
1047
1055
|
case GI_TYPE_TAG_UINT8:
|
|
@@ -1096,8 +1104,21 @@ static bool IsSupportedElementType(GITypeInfo* elem, std::string* why, bool byVa
|
|
|
1096
1104
|
if (iface != nullptr) gi_base_info_unref(iface);
|
|
1097
1105
|
return ok;
|
|
1098
1106
|
}
|
|
1107
|
+
// `GdaConnection.statement_execute_select_full`'s `col_types` is the call that
|
|
1108
|
+
// needed it (@gjsify/sqlite reads 64-bit integers exactly by typing columns). It
|
|
1109
|
+
// fell to the default below and was refused as a "nested-container element".
|
|
1110
|
+
case GI_TYPE_TAG_GTYPE:
|
|
1111
|
+
if (!cArrayCell && why != nullptr) *why = "GType element outside a C array";
|
|
1112
|
+
return cArrayCell;
|
|
1099
1113
|
default:
|
|
1100
|
-
|
|
1114
|
+
// Name the tag: this label used to be "nested-container element" for EVERY
|
|
1115
|
+
// unhandled tag, which sent the GType case above looking for a nested container.
|
|
1116
|
+
if (why != nullptr) {
|
|
1117
|
+
bool nested = tag == GI_TYPE_TAG_ARRAY || tag == GI_TYPE_TAG_GLIST ||
|
|
1118
|
+
tag == GI_TYPE_TAG_GSLIST || tag == GI_TYPE_TAG_GHASH;
|
|
1119
|
+
*why = nested ? "nested-container element"
|
|
1120
|
+
: std::string(gi_type_tag_to_string(tag)) + " element";
|
|
1121
|
+
}
|
|
1101
1122
|
return false;
|
|
1102
1123
|
}
|
|
1103
1124
|
}
|
|
@@ -1121,10 +1142,10 @@ bool IsSupportedContainerType(GITypeInfo* type, std::string* why, ContainerUse u
|
|
|
1121
1142
|
// GArray is deliberately excluded alongside the lists: its elements are sized,
|
|
1122
1143
|
// but g_array_append_vals copies from a buffer this path would have to size the
|
|
1123
1144
|
// same way, and no installed typelib has a by-value-element GArray as IN.
|
|
1124
|
-
bool
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
bool ok = IsSupportedElementType(elem, why, byValueOk);
|
|
1145
|
+
bool cArray = gi_type_info_get_tag(type) == GI_TYPE_TAG_ARRAY &&
|
|
1146
|
+
gi_type_info_get_array_type(type) == GI_ARRAY_TYPE_C;
|
|
1147
|
+
bool byValueOk = use == ContainerUse::kIn && cArray;
|
|
1148
|
+
bool ok = IsSupportedElementType(elem, why, byValueOk, cArray);
|
|
1128
1149
|
gi_base_info_unref(elem);
|
|
1129
1150
|
return ok;
|
|
1130
1151
|
}
|
|
@@ -1139,7 +1160,7 @@ bool IsSupportedContainerType(GITypeInfo* type, std::string* why, ContainerUse u
|
|
|
1139
1160
|
ktag == GI_TYPE_TAG_INT8 || ktag == GI_TYPE_TAG_UINT8 ||
|
|
1140
1161
|
ktag == GI_TYPE_TAG_INT16 || ktag == GI_TYPE_TAG_UINT16 ||
|
|
1141
1162
|
ktag == GI_TYPE_TAG_INT32 || ktag == GI_TYPE_TAG_UINT32;
|
|
1142
|
-
bool vok = vt != nullptr && IsSupportedElementType(vt, nullptr, false);
|
|
1163
|
+
bool vok = vt != nullptr && IsSupportedElementType(vt, nullptr, false, false);
|
|
1143
1164
|
if (!kok && why != nullptr) *why = "unsupported GHashTable key";
|
|
1144
1165
|
else if (!vok && why != nullptr) *why = "unsupported GHashTable value";
|
|
1145
1166
|
if (kt != nullptr) gi_base_info_unref(kt);
|
|
@@ -1166,6 +1187,7 @@ size_t CElementSize(GITypeInfo* elem) {
|
|
|
1166
1187
|
case GI_TYPE_TAG_UINT64: return 8;
|
|
1167
1188
|
case GI_TYPE_TAG_FLOAT: return sizeof(gfloat);
|
|
1168
1189
|
case GI_TYPE_TAG_DOUBLE: return sizeof(gdouble);
|
|
1190
|
+
case GI_TYPE_TAG_GTYPE: return sizeof(GType);
|
|
1169
1191
|
case GI_TYPE_TAG_UTF8:
|
|
1170
1192
|
case GI_TYPE_TAG_FILENAME:
|
|
1171
1193
|
case GI_TYPE_TAG_INTERFACE: return sizeof(gpointer);
|
|
@@ -1329,6 +1351,7 @@ static Napi::Value ReadCElement(Napi::Env env, GITypeInfo* elem, const void* src
|
|
|
1329
1351
|
case GI_TYPE_TAG_UINT64: a.v_uint64 = *static_cast<const guint64*>(src); break;
|
|
1330
1352
|
case GI_TYPE_TAG_FLOAT: a.v_float = *static_cast<const gfloat*>(src); break;
|
|
1331
1353
|
case GI_TYPE_TAG_DOUBLE: a.v_double = *static_cast<const gdouble*>(src); break;
|
|
1354
|
+
case GI_TYPE_TAG_GTYPE: a.v_size = *static_cast<const GType*>(src); break;
|
|
1332
1355
|
case GI_TYPE_TAG_UTF8:
|
|
1333
1356
|
case GI_TYPE_TAG_FILENAME: a.v_string = *static_cast<char* const*>(src); break;
|
|
1334
1357
|
case GI_TYPE_TAG_INTERFACE: a.v_pointer = *static_cast<gpointer const*>(src); break;
|
|
@@ -1599,6 +1622,20 @@ static bool ElementToGIArgument(Napi::Env env, GITypeInfo* elem, Napi::Value v,
|
|
|
1599
1622
|
case GI_TYPE_TAG_UINT64: a->v_uint64 = JsValueToUint64(v); return true;
|
|
1600
1623
|
case GI_TYPE_TAG_FLOAT: a->v_float = static_cast<gfloat>(NodeGiToDouble(v)); return true;
|
|
1601
1624
|
case GI_TYPE_TAG_DOUBLE: a->v_double = NodeGiToDouble(v); return true;
|
|
1625
|
+
case GI_TYPE_TAG_GTYPE: {
|
|
1626
|
+
// Stricter than a scalar GType argument, as gjs is (`test_array_gtype_in
|
|
1627
|
+
// ([undefined])` throws): a null element would become G_TYPE_INVALID, which in
|
|
1628
|
+
// a zero-terminated array silently truncates everything after it.
|
|
1629
|
+
if (v.IsNull() || v.IsUndefined()) {
|
|
1630
|
+
Napi::TypeError::New(env, "expected a GType handle (e.g. Class.$gtype) as an array element")
|
|
1631
|
+
.ThrowAsJavaScriptException();
|
|
1632
|
+
return false;
|
|
1633
|
+
}
|
|
1634
|
+
GType gt = 0;
|
|
1635
|
+
if (!UnwrapGTypeArg(env, v, >)) return false;
|
|
1636
|
+
a->v_size = static_cast<gsize>(gt);
|
|
1637
|
+
return true;
|
|
1638
|
+
}
|
|
1602
1639
|
case GI_TYPE_TAG_UTF8:
|
|
1603
1640
|
case GI_TYPE_TAG_FILENAME:
|
|
1604
1641
|
a->v_string = (v.IsEmpty() || v.IsNull() || v.IsUndefined())
|
|
@@ -1679,6 +1716,330 @@ static size_t BoxedInfoSize(GIBaseInfo* info) {
|
|
|
1679
1716
|
return 0;
|
|
1680
1717
|
}
|
|
1681
1718
|
|
|
1719
|
+
static bool ByValueRecordToCell(Napi::Env env, GITypeInfo* elem, Napi::Value v, void* dst,
|
|
1720
|
+
size_t elemSize, gpointer* writeBackTo);
|
|
1721
|
+
static bool JsToCArray(Napi::Env env, Napi::Value v, GITypeInfo* type, GITransfer transfer,
|
|
1722
|
+
gpointer* outPtr, long* outCount, std::vector<gpointer>* writeBack);
|
|
1723
|
+
|
|
1724
|
+
// ---- a call C makes INTO JS: OUT / INOUT parameters and the answer ---------
|
|
1725
|
+
//
|
|
1726
|
+
// C calls a JS `vfunc_*` override or a GI callback with a POINTER for each OUT/INOUT
|
|
1727
|
+
// parameter and reads the answer back through it. Both trampolines used to hand those
|
|
1728
|
+
// pointers to JS as if they were IN values and never wrote anything back, so every
|
|
1729
|
+
// override with an OUT parameter answered zeros: `Gtk.LayoutManager.vfunc_measure`
|
|
1730
|
+
// returned `[min, nat, minBaseline, natBaseline]` from JS and GTK read `0, 0, 0, 0`.
|
|
1731
|
+
// That is what left a React Native `<ScrollView horizontal>` rail zero-height under
|
|
1732
|
+
// node-gi while the same code measured correctly on gjs. One class serves both
|
|
1733
|
+
// trampolines so the two cannot answer in different shapes.
|
|
1734
|
+
//
|
|
1735
|
+
// The shape is gjs's (gi/function.cpp callback_closure_inner): a pure OUT is not a
|
|
1736
|
+
// JS argument, an INOUT passes its current value, a `void`-typed OUT is skipped
|
|
1737
|
+
// entirely; one output (the return, or a lone OUT) is the JS return value itself,
|
|
1738
|
+
// several are an array — the return first, then the OUT/INOUT in declaration order.
|
|
1739
|
+
//
|
|
1740
|
+
// OWNERSHIP follows the parameter's transfer, through the same JsToGIArgument the IN
|
|
1741
|
+
// path uses: on EVERYTHING the C caller adopts the value, so an object gets its own
|
|
1742
|
+
// ref and a boxed its own copy; on NOTHING it borrows the wrapper's. Only the slot's
|
|
1743
|
+
// own bytes are copied out of the union (CInElementSize), never all eight, because the
|
|
1744
|
+
// destination is the caller's `int`/`gboolean`/enum variable and not a GIArgument.
|
|
1745
|
+
|
|
1746
|
+
namespace {
|
|
1747
|
+
|
|
1748
|
+
// How many bytes one scalar-shaped OUT/INOUT slot's variable holds, or 0 when it is
|
|
1749
|
+
// not one: a by-value record not marked caller-allocates, a GType and the containers
|
|
1750
|
+
// have no element write here (a self-delimiting OUT array has its own, in WriteOut),
|
|
1751
|
+
// and are refused with the parameter's name rather than half-written.
|
|
1752
|
+
size_t OutSlotSize(GITypeInfo* ti) {
|
|
1753
|
+
if (IsByValueRecordElement(ti)) return 0;
|
|
1754
|
+
size_t size = CInElementSize(ti);
|
|
1755
|
+
return size <= sizeof(GIArgument) ? size : 0;
|
|
1756
|
+
}
|
|
1757
|
+
|
|
1758
|
+
// How many bytes to zero in an unanswered pure OUT: its scalar size, or one pointer
|
|
1759
|
+
// for a container, whose variable is the pointer the caller would free.
|
|
1760
|
+
size_t OutZeroSize(GITypeInfo* ti) {
|
|
1761
|
+
switch (gi_type_info_get_tag(ti)) {
|
|
1762
|
+
case GI_TYPE_TAG_ARRAY:
|
|
1763
|
+
case GI_TYPE_TAG_GLIST:
|
|
1764
|
+
case GI_TYPE_TAG_GSLIST:
|
|
1765
|
+
case GI_TYPE_TAG_GHASH:
|
|
1766
|
+
case GI_TYPE_TAG_ERROR: return sizeof(gpointer);
|
|
1767
|
+
case GI_TYPE_TAG_GTYPE: return sizeof(GType);
|
|
1768
|
+
default: return OutSlotSize(ti);
|
|
1769
|
+
}
|
|
1770
|
+
}
|
|
1771
|
+
|
|
1772
|
+
// GObject qdata quark for the GPtrArray of transfer-none string answers tied to an
|
|
1773
|
+
// object's lifetime (AssociateStringWithObject below). A function-local static is
|
|
1774
|
+
// safe here — g_quark_from_static_string is idempotent and already how this file's
|
|
1775
|
+
// other qdata keys are declared (see class.cc's NodeGiClassDataQuark).
|
|
1776
|
+
static GQuark NodeGiInstanceStringsQuark() {
|
|
1777
|
+
static GQuark q = g_quark_from_static_string("node-gi-instance-strings");
|
|
1778
|
+
return q;
|
|
1779
|
+
}
|
|
1780
|
+
|
|
1781
|
+
// Record `str` (already g_strdup'd) to be freed when `obj` is finalized. Mirrors
|
|
1782
|
+
// gjs's ObjectInstance::associate_string (refs/gjs/gi/object.cpp): a GPtrArray hung
|
|
1783
|
+
// off the object's own qdata, created on first use and freed automatically by
|
|
1784
|
+
// g_object_set_qdata_full's destroy notify.
|
|
1785
|
+
static void AssociateStringWithObject(GObject* obj, char* str) {
|
|
1786
|
+
auto* strings = static_cast<GPtrArray*>(g_object_get_qdata(obj, NodeGiInstanceStringsQuark()));
|
|
1787
|
+
if (strings == nullptr) {
|
|
1788
|
+
strings = g_ptr_array_new_with_free_func(g_free);
|
|
1789
|
+
g_object_set_qdata_full(obj, NodeGiInstanceStringsQuark(), strings,
|
|
1790
|
+
reinterpret_cast<GDestroyNotify>(g_ptr_array_unref));
|
|
1791
|
+
}
|
|
1792
|
+
g_ptr_array_add(strings, str);
|
|
1793
|
+
}
|
|
1794
|
+
|
|
1795
|
+
// A string C keeps. A transfer-full one is C's to free, so it gets its own copy. A
|
|
1796
|
+
// transfer-none one must outlive this frame with NOBODY freeing it — a JsToGIArgument
|
|
1797
|
+
// string would point into a std::string that dies here.
|
|
1798
|
+
//
|
|
1799
|
+
// `owner` (the vfunc's instance, when there is one) gets first refusal: tying the
|
|
1800
|
+
// string to ITS lifetime bounds the cost to that object's lifetime, exactly as gjs
|
|
1801
|
+
// does. Without an owner (a plain GI callback has none) g_intern_string is the only
|
|
1802
|
+
// option — valid forever, one copy per distinct value — and that IS gjs's own
|
|
1803
|
+
// fallback there too. Interning unconditionally (this file's previous approach) is
|
|
1804
|
+
// the wrong default for the common vfunc case: a JS override that answers a FRESH
|
|
1805
|
+
// transfer-none string every call (e.g. Gtk.Editable's vfunc_get_text on a
|
|
1806
|
+
// live-updating buffer) would intern one string PER CALL, forever, for the life of
|
|
1807
|
+
// the process — unbounded growth a real widget hits on every keystroke.
|
|
1808
|
+
char* StringForC(Napi::Value v, GITransfer transfer, GObject* owner) {
|
|
1809
|
+
if (!v.IsString()) return nullptr;
|
|
1810
|
+
std::string s = v.As<Napi::String>().Utf8Value();
|
|
1811
|
+
if (transfer != GI_TRANSFER_NOTHING) return g_strdup(s.c_str());
|
|
1812
|
+
if (owner != nullptr) {
|
|
1813
|
+
char* dup = g_strdup(s.c_str());
|
|
1814
|
+
AssociateStringWithObject(owner, dup);
|
|
1815
|
+
return dup;
|
|
1816
|
+
}
|
|
1817
|
+
return const_cast<char*>(g_intern_string(s.c_str()));
|
|
1818
|
+
}
|
|
1819
|
+
|
|
1820
|
+
bool JsToAnswerArgument(Napi::Env env, Napi::Value v, GITypeInfo* ti, GITransfer transfer,
|
|
1821
|
+
GIArgument* out, const char* argName, GObject* owner) {
|
|
1822
|
+
memset(out, 0, sizeof(*out));
|
|
1823
|
+
GITypeTag tag = gi_type_info_get_tag(ti);
|
|
1824
|
+
if (tag == GI_TYPE_TAG_UTF8 || tag == GI_TYPE_TAG_FILENAME) {
|
|
1825
|
+
out->v_string = StringForC(v, transfer, owner);
|
|
1826
|
+
return true;
|
|
1827
|
+
}
|
|
1828
|
+
std::string held;
|
|
1829
|
+
return JsToGIArgument(env, v, ti, out, &held, transfer, nullptr, nullptr, nullptr, nullptr,
|
|
1830
|
+
nullptr, argName);
|
|
1831
|
+
}
|
|
1832
|
+
|
|
1833
|
+
} // namespace
|
|
1834
|
+
|
|
1835
|
+
CToJsCall::CToJsCall(Napi::Env env, GICallableInfo* ci, std::string label, GObject* owner)
|
|
1836
|
+
: env_(env), ci_(ci), label_(std::move(label)), owner_(owner) {}
|
|
1837
|
+
|
|
1838
|
+
CToJsCall::~CToJsCall() {
|
|
1839
|
+
for (const Slot& s : outs_) {
|
|
1840
|
+
gi_base_info_unref(s.ti);
|
|
1841
|
+
gi_base_info_unref(s.ai);
|
|
1842
|
+
}
|
|
1843
|
+
}
|
|
1844
|
+
|
|
1845
|
+
void CToJsCall::ThrowUnsupported(const Slot& s) const {
|
|
1846
|
+
Napi::TypeError::New(env_, label_ + ": OUT/INOUT parameter '" +
|
|
1847
|
+
gi_base_info_get_name(reinterpret_cast<GIBaseInfo*>(s.ai)) +
|
|
1848
|
+
"' has a type node-gi cannot write back from JS")
|
|
1849
|
+
.ThrowAsJavaScriptException();
|
|
1850
|
+
}
|
|
1851
|
+
|
|
1852
|
+
bool CToJsCall::MarshalArgs(void** args, unsigned int offset, std::vector<napi_value>* jsArgs) {
|
|
1853
|
+
unsigned int n = gi_callable_info_get_n_args(ci_);
|
|
1854
|
+
jsArgs->reserve(n);
|
|
1855
|
+
// Every arg's C value, indexed like the callable's args, so a length-annotated
|
|
1856
|
+
// C-array arg can read its companion length (ReadOutOrReturn's `slots`). Without
|
|
1857
|
+
// it such an array is not zero-terminated and reads as EMPTY — measured: a JS
|
|
1858
|
+
// Gio.OutputStream's vfunc_write_fn(buffer) got [] and returned 0 where gjs
|
|
1859
|
+
// 1.88.1 hands it all 3 bytes, a silent short write (write_all would spin).
|
|
1860
|
+
// An ffi slot holds the value at its own width; OutSlotSize is that width for a
|
|
1861
|
+
// scalar and 0 for anything that can never be a length.
|
|
1862
|
+
// `isLength` marks those companions: gjs never hands one to JS (the array carries
|
|
1863
|
+
// its length), so the handler's later args keep their gjs positions —
|
|
1864
|
+
// vfunc_write_fn(buffer, cancellable), not (buffer, count, cancellable).
|
|
1865
|
+
std::vector<GIArgument> slots(n);
|
|
1866
|
+
std::vector<bool> isLength(n, false);
|
|
1867
|
+
for (unsigned int i = 0; i < n; i++) {
|
|
1868
|
+
GIArgInfo* ai = gi_callable_info_get_arg(ci_, i);
|
|
1869
|
+
GITypeInfo* ti = gi_arg_info_get_type_info(ai);
|
|
1870
|
+
GIDirection dir = gi_arg_info_get_direction(ai);
|
|
1871
|
+
unsigned int lenIdx = 0;
|
|
1872
|
+
if (gi_type_info_get_tag(ti) == GI_TYPE_TAG_ARRAY &&
|
|
1873
|
+
gi_type_info_get_array_length_index(ti, &lenIdx) && lenIdx < n)
|
|
1874
|
+
isLength[lenIdx] = true;
|
|
1875
|
+
size_t size = gi_type_info_get_tag(ti) == GI_TYPE_TAG_VOID ? 0 : OutSlotSize(ti);
|
|
1876
|
+
void* src = args[i + offset];
|
|
1877
|
+
if (dir == GI_DIRECTION_INOUT && src != nullptr && !gi_arg_info_is_caller_allocates(ai))
|
|
1878
|
+
src = static_cast<GIArgument*>(src)->v_pointer;
|
|
1879
|
+
else if (dir == GI_DIRECTION_OUT)
|
|
1880
|
+
src = nullptr;
|
|
1881
|
+
if (src != nullptr && size > 0) memcpy(&slots[i], src, size);
|
|
1882
|
+
gi_base_info_unref(ti);
|
|
1883
|
+
gi_base_info_unref(ai);
|
|
1884
|
+
}
|
|
1885
|
+
// After a failed conversion the loop keeps going, recording only the OUT slots, so
|
|
1886
|
+
// WriteAnswer can still zero every one of them — JS will not run to answer.
|
|
1887
|
+
bool failed = false;
|
|
1888
|
+
for (unsigned int i = 0; i < n; i++) {
|
|
1889
|
+
GIArgInfo* ai = gi_callable_info_get_arg(ci_, i);
|
|
1890
|
+
GITypeInfo* ti = gi_arg_info_get_type_info(ai);
|
|
1891
|
+
GIArgument* ffiSlot = static_cast<GIArgument*>(args[i + offset]);
|
|
1892
|
+
GIDirection dir = gi_arg_info_get_direction(ai);
|
|
1893
|
+
if (dir == GI_DIRECTION_IN || gi_type_info_get_tag(ti) == GI_TYPE_TAG_VOID) {
|
|
1894
|
+
if (dir == GI_DIRECTION_IN && !failed && !isLength[i]) {
|
|
1895
|
+
// ReadOutOrReturn, not GIArgumentToJs directly: a C-invoked callback's
|
|
1896
|
+
// IN argument can be a container too (Soup.ServerCallback's trailing
|
|
1897
|
+
// `query` is a GHashTable, GI_TYPE_TAG_GHASH) — GIArgumentToJs's switch
|
|
1898
|
+
// has no ARRAY/GLIST/GSLIST/GHASH arm and threw "Unsupported return
|
|
1899
|
+
// type tag 19" on EVERY invocation, aborting the callback (and, for a
|
|
1900
|
+
// scope=call libsoup request handler, the connection) before the JS
|
|
1901
|
+
// side ever ran. `slots` resolves a length-annotated C array (above).
|
|
1902
|
+
Napi::Value v = ReadOutOrReturn(env_, ci_, ti, ffiSlot, GI_TRANSFER_NOTHING, &slots);
|
|
1903
|
+
failed = env_.IsExceptionPending();
|
|
1904
|
+
if (!failed) jsArgs->push_back(v);
|
|
1905
|
+
}
|
|
1906
|
+
gi_base_info_unref(ti);
|
|
1907
|
+
gi_base_info_unref(ai);
|
|
1908
|
+
continue;
|
|
1909
|
+
}
|
|
1910
|
+
// The ffi slot of an OUT/INOUT holds the caller's pointer: to its variable, or for
|
|
1911
|
+
// a caller-allocates record to the record itself.
|
|
1912
|
+
outs_.push_back({ai, ti, ffiSlot->v_pointer});
|
|
1913
|
+
if (dir != GI_DIRECTION_INOUT || failed) continue;
|
|
1914
|
+
const Slot& s = outs_.back();
|
|
1915
|
+
GIArgument cur;
|
|
1916
|
+
memset(&cur, 0, sizeof(cur));
|
|
1917
|
+
if (s.dest == nullptr) {
|
|
1918
|
+
jsArgs->push_back(env_.Null());
|
|
1919
|
+
continue;
|
|
1920
|
+
}
|
|
1921
|
+
if (gi_arg_info_is_caller_allocates(ai)) {
|
|
1922
|
+
cur.v_pointer = s.dest;
|
|
1923
|
+
} else {
|
|
1924
|
+
size_t size = OutSlotSize(ti);
|
|
1925
|
+
if (size == 0) {
|
|
1926
|
+
ThrowUnsupported(s);
|
|
1927
|
+
failed = true;
|
|
1928
|
+
continue;
|
|
1929
|
+
}
|
|
1930
|
+
memcpy(&cur, s.dest, size);
|
|
1931
|
+
}
|
|
1932
|
+
// Same container gap as the IN-arg read above (an INOUT container arg is
|
|
1933
|
+
// rarer, but the fix is identical): ReadOutOrReturn, not GIArgumentToJs.
|
|
1934
|
+
Napi::Value v = ReadOutOrReturn(env_, ci_, ti, &cur, GI_TRANSFER_NOTHING, &slots);
|
|
1935
|
+
failed = env_.IsExceptionPending();
|
|
1936
|
+
if (!failed) jsArgs->push_back(v);
|
|
1937
|
+
}
|
|
1938
|
+
return !failed;
|
|
1939
|
+
}
|
|
1940
|
+
|
|
1941
|
+
bool CToJsCall::WriteOut(const Slot& s, Napi::Value v) {
|
|
1942
|
+
// A nullable OUT the caller passed NULL for: it did not ask, so there is nothing to
|
|
1943
|
+
// write and nothing to refuse.
|
|
1944
|
+
if (s.dest == nullptr) return true;
|
|
1945
|
+
const char* name = gi_base_info_get_name(reinterpret_cast<GIBaseInfo*>(s.ai));
|
|
1946
|
+
if (gi_arg_info_is_caller_allocates(s.ai)) {
|
|
1947
|
+
// The caller owns the storage and wants the record's BYTES in it. The by-value
|
|
1948
|
+
// array-cell writer already does exactly that and checks the handle is the SAME
|
|
1949
|
+
// record type first — copying the destination's size out of a smaller handle of
|
|
1950
|
+
// another type would read past its end. A GValue is initialised and deep-copied
|
|
1951
|
+
// rather than aliased, which is also gjs's one caller-allocates case here.
|
|
1952
|
+
GIBaseInfo* iface = gi_type_info_get_tag(s.ti) == GI_TYPE_TAG_INTERFACE
|
|
1953
|
+
? gi_type_info_get_interface(s.ti)
|
|
1954
|
+
: nullptr;
|
|
1955
|
+
size_t size = BoxedInfoSize(iface);
|
|
1956
|
+
if (iface != nullptr) gi_base_info_unref(iface);
|
|
1957
|
+
if (size == 0) {
|
|
1958
|
+
ThrowUnsupported(s);
|
|
1959
|
+
return false;
|
|
1960
|
+
}
|
|
1961
|
+
return ByValueRecordToCell(env_, s.ti, v, s.dest, size, nullptr);
|
|
1962
|
+
}
|
|
1963
|
+
GITransfer transfer = gi_arg_info_get_ownership_transfer(s.ai);
|
|
1964
|
+
unsigned int lengthIndex = 0;
|
|
1965
|
+
if (gi_type_info_get_tag(s.ti) == GI_TYPE_TAG_ARRAY &&
|
|
1966
|
+
!gi_type_info_get_array_length_index(s.ti, &lengthIndex)) {
|
|
1967
|
+
// A self-delimiting array (zero-terminated, fixed-size, or a GArray/GPtrArray/
|
|
1968
|
+
// GByteArray) is one pointer the caller reads, built the way an IN array is. One
|
|
1969
|
+
// with a separate length parameter is refused below: that length is a second OUT
|
|
1970
|
+
// gjs would expect in the answer too, and writing it from the array is its own
|
|
1971
|
+
// decision, not a side effect of this one.
|
|
1972
|
+
gpointer arr = nullptr;
|
|
1973
|
+
long count = 0;
|
|
1974
|
+
if (!JsToCArray(env_, v, s.ti, transfer, &arr, &count, nullptr)) return false;
|
|
1975
|
+
*static_cast<gpointer*>(s.dest) = arr;
|
|
1976
|
+
return true;
|
|
1977
|
+
}
|
|
1978
|
+
size_t size = OutSlotSize(s.ti);
|
|
1979
|
+
if (size == 0) {
|
|
1980
|
+
ThrowUnsupported(s);
|
|
1981
|
+
return false;
|
|
1982
|
+
}
|
|
1983
|
+
GIArgument a;
|
|
1984
|
+
if (!JsToAnswerArgument(env_, v, s.ti, transfer, &a, name, owner_)) return false;
|
|
1985
|
+
memcpy(s.dest, &a, size);
|
|
1986
|
+
return true;
|
|
1987
|
+
}
|
|
1988
|
+
|
|
1989
|
+
void CToJsCall::WriteAnswer(napi_value ret, void* result) {
|
|
1990
|
+
GITypeInfo* retType = gi_callable_info_get_return_type(ci_);
|
|
1991
|
+
GITypeTag rtag = gi_type_info_get_tag(retType);
|
|
1992
|
+
bool hasRet = rtag != GI_TYPE_TAG_VOID;
|
|
1993
|
+
size_t nAnswers = outs_.size() + (hasRet ? 1 : 0);
|
|
1994
|
+
size_t written = 0;
|
|
1995
|
+
if (nAnswers > 0 && ret != nullptr && !env_.IsExceptionPending()) {
|
|
1996
|
+
Napi::Value rv(env_, ret);
|
|
1997
|
+
std::vector<Napi::Value> answers;
|
|
1998
|
+
if (nAnswers == 1) {
|
|
1999
|
+
answers.push_back(rv);
|
|
2000
|
+
} else if (rv.IsArray() && rv.As<Napi::Array>().Length() == nAnswers) {
|
|
2001
|
+
Napi::Array arr = rv.As<Napi::Array>();
|
|
2002
|
+
for (uint32_t k = 0; k < nAnswers && !env_.IsExceptionPending(); k++)
|
|
2003
|
+
answers.push_back(arr.Get(k));
|
|
2004
|
+
} else {
|
|
2005
|
+
Napi::TypeError::New(env_, label_ + " must return an array of " +
|
|
2006
|
+
std::to_string(nAnswers) +
|
|
2007
|
+
" values (its return value and OUT parameters)")
|
|
2008
|
+
.ThrowAsJavaScriptException();
|
|
2009
|
+
}
|
|
2010
|
+
if (answers.size() == nAnswers && !env_.IsExceptionPending()) {
|
|
2011
|
+
size_t k = 0;
|
|
2012
|
+
bool ok = true;
|
|
2013
|
+
if (hasRet) {
|
|
2014
|
+
GIArgument a;
|
|
2015
|
+
ok = JsToAnswerArgument(env_, answers[k++], retType,
|
|
2016
|
+
gi_callable_info_get_caller_owns(ci_), &a, "return value",
|
|
2017
|
+
owner_);
|
|
2018
|
+
// The ffi return slot is at least ffi_arg wide and was zeroed by the caller,
|
|
2019
|
+
// so the whole union can go in: libffi reads the low bytes it needs.
|
|
2020
|
+
if (ok && result != nullptr) *static_cast<GIArgument*>(result) = a;
|
|
2021
|
+
}
|
|
2022
|
+
while (ok && written < outs_.size()) {
|
|
2023
|
+
ok = WriteOut(outs_[written], answers[k++]);
|
|
2024
|
+
if (ok) written++;
|
|
2025
|
+
}
|
|
2026
|
+
}
|
|
2027
|
+
}
|
|
2028
|
+
// Whatever was not answered — JS threw, returned the wrong shape, or a value did
|
|
2029
|
+
// not convert — must not leave C reading an uninitialised variable: a pure OUT is
|
|
2030
|
+
// one the caller need not have initialised. Zero it (the return slot already is);
|
|
2031
|
+
// the JS exception stays pending for the trampoline to surface or propagate.
|
|
2032
|
+
for (size_t i = written; i < outs_.size(); i++) {
|
|
2033
|
+
const Slot& s = outs_[i];
|
|
2034
|
+
if (s.dest == nullptr || gi_arg_info_get_direction(s.ai) != GI_DIRECTION_OUT ||
|
|
2035
|
+
gi_arg_info_is_caller_allocates(s.ai))
|
|
2036
|
+
continue;
|
|
2037
|
+
size_t size = OutZeroSize(s.ti);
|
|
2038
|
+
if (size != 0) memset(s.dest, 0, size);
|
|
2039
|
+
}
|
|
2040
|
+
gi_base_info_unref(retType);
|
|
2041
|
+
}
|
|
2042
|
+
|
|
1682
2043
|
// Write ONE by-value record element straight into its C-array cell.
|
|
1683
2044
|
//
|
|
1684
2045
|
// WHY THIS IS NOT A GIArgument, which is the whole reason the function exists. The
|
|
@@ -2381,6 +2742,7 @@ static bool ElementsAreReadable(GITypeInfo* type) {
|
|
|
2381
2742
|
case GI_TYPE_TAG_UINT64:
|
|
2382
2743
|
case GI_TYPE_TAG_FLOAT:
|
|
2383
2744
|
case GI_TYPE_TAG_DOUBLE:
|
|
2745
|
+
case GI_TYPE_TAG_GTYPE:
|
|
2384
2746
|
case GI_TYPE_TAG_UTF8:
|
|
2385
2747
|
case GI_TYPE_TAG_FILENAME:
|
|
2386
2748
|
readable = true;
|