@gjsify/napi 0.22.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/src/cc/wrap.cc ADDED
@@ -0,0 +1,321 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // @gjsify/napi — §5d wrap/unwrap/external/type tags: the uniform
3
+ // Reference-based lifecycle over the two-tier InstanceState design.
4
+ //
5
+ // Reference: refs/node/src/js_native_api_v8.cc (Node.js contributors, MIT) —
6
+ // v8impl::Wrap/Unwrap (cc:339-374, 525-586), napi_create_external
7
+ // (cc:2679-2708), type tags (cc:2710-2799). Two-tier storage (Bun's shape):
8
+ // - FAST TIER: instances of napi_define_class classes + externals carry
9
+ // reserved slot 0 = JS::PrivateValue(InstanceState*), freed by a
10
+ // FOREGROUND finalize hook that frees memory ONLY (no JSAPI, no user
11
+ // callbacks — legal during GC, incl. nursery deaths). Pattern: GJS
12
+ // cwrapper.h reserved slots + gi/object.cpp:3404-3407 foreground
13
+ // finalize.
14
+ // - FALLBACK TIER: foreign objects get the same InstanceState via an
15
+ // env-held WeakMap (JS::NewWeakMapObject/Get/SetWeakMapEntry,
16
+ // js/WeakMap.h) whose value is a hostdata_class carrier — ephemeron
17
+ // semantics keep the carrier (and so the InstanceState) alive exactly as
18
+ // long as the wrapped object, invisible to script.
19
+ // Lifecycle is uniform and lives in the wrap Reference, not the map: death
20
+ // → §5b weak sweep → §5c queue; the class finalize hook only frees the
21
+ // InstanceState struct.
22
+
23
+ #include "common.h"
24
+
25
+ namespace gjsify_napi {
26
+
27
+ // Frees memory only — runs during GC sweeping (JSFinalizeOp gets a
28
+ // JS::GCContext*, js/Class.h:301; foreground = main thread).
29
+ static void instance_state_finalize(JS::GCContext* /* gcx */, JSObject* obj) {
30
+ const JS::Value slot = JS::GetReservedSlot(obj, kInstanceSlotState);
31
+ if (!slot.isUndefined()) {
32
+ delete static_cast<InstanceState*>(slot.toPrivate());
33
+ }
34
+ }
35
+
36
+ static const JSClassOps instance_state_class_ops = {
37
+ nullptr, // addProperty
38
+ nullptr, // delProperty
39
+ nullptr, // enumerate
40
+ nullptr, // newEnumerate
41
+ nullptr, // resolve
42
+ nullptr, // mayResolve
43
+ instance_state_finalize, // finalize
44
+ nullptr, // call
45
+ nullptr, // construct
46
+ nullptr, // trace
47
+ };
48
+
49
+ const JSClass instance_class = {
50
+ "GjsifyNapiInstance",
51
+ JSCLASS_HAS_RESERVED_SLOTS(1) | JSCLASS_FOREGROUND_FINALIZE,
52
+ &instance_state_class_ops,
53
+ };
54
+
55
+ const JSClass external_class = {
56
+ "GjsifyNapiExternal",
57
+ JSCLASS_HAS_RESERVED_SLOTS(1) | JSCLASS_FOREGROUND_FINALIZE,
58
+ &instance_state_class_ops,
59
+ };
60
+
61
+ const JSClass hostdata_class = {
62
+ "GjsifyNapiHostData",
63
+ JSCLASS_HAS_RESERVED_SLOTS(1) | JSCLASS_FOREGROUND_FINALIZE,
64
+ &instance_state_class_ops,
65
+ };
66
+
67
+ static bool has_state_slot(JSObject* obj) {
68
+ const JSClass* cls = JS::GetClass(obj);
69
+ return cls == &instance_class || cls == &external_class ||
70
+ cls == &hostdata_class;
71
+ }
72
+
73
+ static InstanceState* state_from_slot(JSObject* obj, bool create) {
74
+ const JS::Value slot = JS::GetReservedSlot(obj, kInstanceSlotState);
75
+ if (!slot.isUndefined()) {
76
+ return static_cast<InstanceState*>(slot.toPrivate());
77
+ }
78
+ if (!create) {
79
+ return nullptr;
80
+ }
81
+ auto* state = new InstanceState();
82
+ JS::SetReservedSlot(obj, kInstanceSlotState, JS::PrivateValue(state));
83
+ return state;
84
+ }
85
+
86
+ napi_status get_instance_state(napi_env env, JS::HandleObject obj,
87
+ bool create, InstanceState** out) {
88
+ *out = nullptr;
89
+ if (has_state_slot(obj)) {
90
+ *out = state_from_slot(obj, create);
91
+ return napi_ok;
92
+ }
93
+ // Foreign object → WeakMap tier.
94
+ if (!env->wrap_map) {
95
+ if (!create) {
96
+ return napi_ok;
97
+ }
98
+ JSObject* map = JS::NewWeakMapObject(env->cx);
99
+ if (map == nullptr) {
100
+ return set_last_error(env, napi_generic_failure);
101
+ }
102
+ env->wrap_map = map;
103
+ }
104
+ JS::RootedObject map(env->cx, env->wrap_map);
105
+ JS::RootedValue key(env->cx, JS::ObjectValue(*obj));
106
+ JS::RootedValue carrier_v(env->cx);
107
+ if (!JS::GetWeakMapEntry(env->cx, map, key, &carrier_v)) {
108
+ return set_last_error(env, napi_generic_failure);
109
+ }
110
+ if (carrier_v.isObject()) {
111
+ *out = state_from_slot(&carrier_v.toObject(), create);
112
+ return napi_ok;
113
+ }
114
+ if (!create) {
115
+ return napi_ok;
116
+ }
117
+ JS::RootedObject carrier(
118
+ env->cx, JS_NewObjectWithGivenProto(env->cx, &hostdata_class,
119
+ nullptr));
120
+ if (!carrier) {
121
+ return set_last_error(env, napi_generic_failure);
122
+ }
123
+ auto* state = new InstanceState();
124
+ JS::SetReservedSlot(carrier, kInstanceSlotState, JS::PrivateValue(state));
125
+ JS::RootedValue carrier_val(env->cx, JS::ObjectValue(*carrier));
126
+ if (!JS::SetWeakMapEntry(env->cx, map, key, carrier_val)) {
127
+ // Slot ownership already with the carrier; it dies unreferenced and
128
+ // its finalize hook frees the state.
129
+ return set_last_error(env, napi_generic_failure);
130
+ }
131
+ *out = state;
132
+ return napi_ok;
133
+ }
134
+
135
+ } // namespace gjsify_napi
136
+
137
+ // ---- wrap / unwrap / remove_wrap (uniform lifecycle, cc:525-586) ----
138
+
139
+ napi_status NAPI_CDECL napi_wrap(napi_env env, napi_value js_object,
140
+ void* native_object,
141
+ node_api_basic_finalize basic_finalize_cb,
142
+ void* finalize_hint, napi_ref* result) {
143
+ GJSIFY_NAPI_PREAMBLE(env);
144
+ GJSIFY_NAPI_CHECK_ARG(env, js_object);
145
+ auto finalize_cb = reinterpret_cast<napi_finalize>(basic_finalize_cb);
146
+ JS::Value v = gjsify_napi::napi_value_to_js(js_object);
147
+ GJSIFY_NAPI_RETURN_STATUS_IF_FALSE(env, v.isObject(),
148
+ napi_object_expected);
149
+ JS::RootedObject obj(env->cx, &v.toObject());
150
+ if (result != nullptr) {
151
+ // The returned reference should be deleted via napi_delete_reference
152
+ // ONLY in response to the finalize callback — so one is required
153
+ // (cc:546-556).
154
+ GJSIFY_NAPI_CHECK_ARG(env, finalize_cb);
155
+ }
156
+ gjsify_napi::InstanceState* state = nullptr;
157
+ napi_status status =
158
+ gjsify_napi::get_instance_state(env, obj, /* create = */ true, &state);
159
+ if (status != napi_ok) {
160
+ return status;
161
+ }
162
+ // Double-wrap = napi_invalid_arg (cc:540-544).
163
+ if (state->wrap != nullptr) {
164
+ return gjsify_napi::set_last_error(env, napi_invalid_arg);
165
+ }
166
+ JS::RootedValue obj_v(env->cx, v);
167
+ gjsify_napi::Reference* ref = gjsify_napi::Reference::New(
168
+ env, obj_v, 0,
169
+ result != nullptr ? gjsify_napi::ReferenceOwnership::kUserland
170
+ : gjsify_napi::ReferenceOwnership::kRuntime,
171
+ finalize_cb, native_object, finalize_hint);
172
+ state->wrap = ref;
173
+ if (result != nullptr) {
174
+ *result = reinterpret_cast<napi_ref>(ref);
175
+ }
176
+ return napi_ok; // preamble already cleared last_error
177
+ }
178
+
179
+ static napi_status unwrap_impl(napi_env env, napi_value js_object,
180
+ void** result, bool remove) {
181
+ GJSIFY_NAPI_CHECK_ENV(env);
182
+ GJSIFY_NAPI_CHECK_ARG(env, js_object);
183
+ GJSIFY_NAPI_CHECK_ARG(env, result);
184
+ JS::Value v = gjsify_napi::napi_value_to_js(js_object);
185
+ GJSIFY_NAPI_RETURN_STATUS_IF_FALSE(env, v.isObject(),
186
+ napi_object_expected);
187
+ JS::RootedObject obj(env->cx, &v.toObject());
188
+ gjsify_napi::InstanceState* state = nullptr;
189
+ napi_status status = gjsify_napi::get_instance_state(
190
+ env, obj, /* create = */ false, &state);
191
+ if (status != napi_ok) {
192
+ return status;
193
+ }
194
+ if (state == nullptr || state->wrap == nullptr) {
195
+ return gjsify_napi::set_last_error(env, napi_invalid_arg);
196
+ }
197
+ gjsify_napi::Reference* ref = state->wrap;
198
+ *result = ref->Data();
199
+ if (remove) {
200
+ state->wrap = nullptr;
201
+ // kRuntime (no napi_ref handed out): the runtime owns it — delete
202
+ // WITHOUT running the finalizer (the caller takes ownership back).
203
+ // kUserland: the ref stays; userland deletes it (cc:365-374).
204
+ if (ref->Ownership() == gjsify_napi::ReferenceOwnership::kRuntime) {
205
+ delete ref;
206
+ }
207
+ }
208
+ return gjsify_napi::clear_last_error(env);
209
+ }
210
+
211
+ napi_status NAPI_CDECL napi_unwrap(napi_env env, napi_value obj,
212
+ void** result) {
213
+ return unwrap_impl(env, obj, result, /* remove = */ false);
214
+ }
215
+
216
+ napi_status NAPI_CDECL napi_remove_wrap(napi_env env, napi_value obj,
217
+ void** result) {
218
+ return unwrap_impl(env, obj, result, /* remove = */ true);
219
+ }
220
+
221
+ // ---- externals (cc:2679-2708) ----
222
+
223
+ napi_status NAPI_CDECL napi_create_external(napi_env env, void* data,
224
+ node_api_basic_finalize
225
+ basic_finalize_cb,
226
+ void* finalize_hint,
227
+ napi_value* result) {
228
+ GJSIFY_NAPI_PREAMBLE(env);
229
+ GJSIFY_NAPI_CHECK_ARG(env, result);
230
+ auto finalize_cb = reinterpret_cast<napi_finalize>(basic_finalize_cb);
231
+ JS::RootedObject external(
232
+ env->cx, JS_NewObjectWithGivenProto(env->cx,
233
+ &gjsify_napi::external_class,
234
+ nullptr));
235
+ if (!external) {
236
+ return gjsify_napi::set_last_error(env, napi_generic_failure);
237
+ }
238
+ auto* state = new gjsify_napi::InstanceState();
239
+ state->is_external = true;
240
+ state->external_data = data;
241
+ JS::SetReservedSlot(external, gjsify_napi::kInstanceSlotState,
242
+ JS::PrivateValue(state));
243
+ if (finalize_cb != nullptr) {
244
+ // kRuntime ReferenceWithFinalizer, weak refcount 0 — the uniform
245
+ // §5b/§5c death path.
246
+ JS::RootedValue ext_v(env->cx, JS::ObjectValue(*external));
247
+ gjsify_napi::Reference::New(env, ext_v, 0,
248
+ gjsify_napi::ReferenceOwnership::kRuntime,
249
+ finalize_cb, data, finalize_hint);
250
+ }
251
+ *result = gjsify_napi::arena_push(env, JS::ObjectValue(*external));
252
+ return napi_ok;
253
+ }
254
+
255
+ napi_status NAPI_CDECL napi_get_value_external(napi_env env, napi_value value,
256
+ void** result) {
257
+ GJSIFY_NAPI_CHECK_ENV(env);
258
+ GJSIFY_NAPI_CHECK_ARG(env, value);
259
+ GJSIFY_NAPI_CHECK_ARG(env, result);
260
+ JS::Value v = gjsify_napi::napi_value_to_js(value);
261
+ GJSIFY_NAPI_RETURN_STATUS_IF_FALSE(
262
+ env,
263
+ v.isObject() &&
264
+ JS::GetClass(&v.toObject()) == &gjsify_napi::external_class,
265
+ napi_invalid_arg);
266
+ gjsify_napi::InstanceState* state =
267
+ gjsify_napi::state_from_slot(&v.toObject(), /* create = */ false);
268
+ *result = state != nullptr ? state->external_data : nullptr;
269
+ return gjsify_napi::clear_last_error(env);
270
+ }
271
+
272
+ // ---- type tags (cc:2710-2799) ----
273
+
274
+ napi_status NAPI_CDECL napi_type_tag_object(napi_env env, napi_value object,
275
+ const napi_type_tag* type_tag) {
276
+ GJSIFY_NAPI_PREAMBLE(env);
277
+ GJSIFY_NAPI_CHECK_ARG(env, object);
278
+ GJSIFY_NAPI_CHECK_ARG(env, type_tag);
279
+ JS::Value v = gjsify_napi::napi_value_to_js(object);
280
+ GJSIFY_NAPI_RETURN_STATUS_IF_FALSE(env, v.isObject(),
281
+ napi_object_expected);
282
+ JS::RootedObject obj(env->cx, &v.toObject());
283
+ gjsify_napi::InstanceState* state = nullptr;
284
+ napi_status status =
285
+ gjsify_napi::get_instance_state(env, obj, /* create = */ true, &state);
286
+ if (status != napi_ok) {
287
+ return status;
288
+ }
289
+ // Tag-once: re-tagging is napi_invalid_arg (cc:2725-2735).
290
+ if (state->has_tag) {
291
+ return gjsify_napi::set_last_error(env, napi_invalid_arg);
292
+ }
293
+ state->tag = *type_tag;
294
+ state->has_tag = true;
295
+ return napi_ok;
296
+ }
297
+
298
+ napi_status NAPI_CDECL napi_check_object_type_tag(napi_env env,
299
+ napi_value object,
300
+ const napi_type_tag* type_tag,
301
+ bool* result) {
302
+ GJSIFY_NAPI_CHECK_ENV(env);
303
+ GJSIFY_NAPI_CHECK_ARG(env, object);
304
+ GJSIFY_NAPI_CHECK_ARG(env, type_tag);
305
+ GJSIFY_NAPI_CHECK_ARG(env, result);
306
+ JS::Value v = gjsify_napi::napi_value_to_js(object);
307
+ GJSIFY_NAPI_RETURN_STATUS_IF_FALSE(env, v.isObject(),
308
+ napi_object_expected);
309
+ JS::RootedObject obj(env->cx, &v.toObject());
310
+ gjsify_napi::InstanceState* state = nullptr;
311
+ napi_status status = gjsify_napi::get_instance_state(
312
+ env, obj, /* create = */ false, &state);
313
+ if (status != napi_ok) {
314
+ return status;
315
+ }
316
+ // Compare both words; untagged → false (cc:2770-2799).
317
+ *result = state != nullptr && state->has_tag &&
318
+ state->tag.lower == type_tag->lower &&
319
+ state->tag.upper == type_tag->upper;
320
+ return gjsify_napi::clear_last_error(env);
321
+ }