@gjsify/rolldown-native 0.4.43 → 0.5.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 +35 -0
- package/lib/esm/plugins.js +1 -1
- package/package.json +3 -3
- package/prebuilds/linux-aarch64/GjsifyRolldown-1.0.gir +10 -0
- package/prebuilds/linux-aarch64/GjsifyRolldown-1.0.typelib +0 -0
- package/prebuilds/linux-aarch64/libgjsify_rolldown.so +0 -0
- package/prebuilds/linux-aarch64/libgjsifyrolldown.so +0 -0
- package/prebuilds/linux-x86_64/GjsifyRolldown-1.0.gir +10 -0
- package/prebuilds/linux-x86_64/GjsifyRolldown-1.0.typelib +0 -0
- package/prebuilds/linux-x86_64/libgjsify_rolldown.so +0 -0
- package/prebuilds/linux-x86_64/libgjsifyrolldown.so +0 -0
- package/src/rust/src/plugin_proxy.rs +5 -10
- package/src/rust/src/session.rs +35 -38
- package/src/vala/gjsify-rolldown-glue.c +11 -2
- package/src/vala/rolldown.vala +42 -0
package/README.md
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# @gjsify/rolldown-native
|
|
2
|
+
|
|
3
|
+
A native Rust cdylib + Vala/GObject bridge that wraps the Rust `rolldown` bundler and exposes it to GJS via `gi://`. This is the default bundler engine used by `gjsify build` under GJS — npm's `rolldown` is an N-API addon that cannot load in GJS, so this bridge is how gjsify bundles without a Node runtime. Includes a complete plugin bridge (`bundleWithPlugins`) for load, transform, resolveId, and render-chunk hooks. Ships prebuilt `.so` + `.typelib` for Linux.
|
|
4
|
+
|
|
5
|
+
Part of the [gjsify](https://github.com/gjsify/gjsify) project — Node.js and Web APIs for GJS (GNOME JavaScript).
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
gjsify install @gjsify/rolldown-native
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
import { hasNativeRolldown, bundle, bundleWithPlugins } from '@gjsify/rolldown-native';
|
|
17
|
+
|
|
18
|
+
if (hasNativeRolldown()) {
|
|
19
|
+
// Simple bundle
|
|
20
|
+
const result = bundle({
|
|
21
|
+
input: [{ import: 'src/index.ts' }],
|
|
22
|
+
format: 'esm',
|
|
23
|
+
minify: false,
|
|
24
|
+
});
|
|
25
|
+
for (const item of result.output) {
|
|
26
|
+
if (item.type === 'chunk') console.log(item.fileName, item.code.length, 'bytes');
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Under normal usage `@gjsify/rolldown-native` is consumed automatically by the gjsify CLI (`gjsify build`) — direct use is only needed when embedding the bundler in custom build tooling.
|
|
32
|
+
|
|
33
|
+
## License
|
|
34
|
+
|
|
35
|
+
MIT
|
package/lib/esm/plugins.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import"./_virtual/_rolldown/runtime.js";import{loadNativeRolldown as e}from"./index.js";function getGLib(){let e=globalThis.imports?.gi;if(!e||!e.GLib)throw Error(`@gjsify/rolldown-native: GLib not available (not running under GJS?)`);return e.GLib}const enc=e=>getGLib().Bytes.new(new TextEncoder().encode(e)),dec=e=>new TextDecoder().decode(e.get_data()??new Uint8Array),t=[`load`,`transform`,`resolveId`,`renderChunk`,`banner`,`footer`,`intro`,`outro`,`buildStart`,`buildEnd`,`generateBundle`,`writeBundle`,`closeBundle`];function pluginMeta(e){let n=t.filter(t=>typeof e[t]==`function`),r={name:e.name,hooks:n};return e.idFilter&&(r.idFilter=e.idFilter),r}function bundleWithPlugins(t,
|
|
1
|
+
import"./_virtual/_rolldown/runtime.js";import{loadNativeRolldown as e}from"./index.js";function getGLib(){let e=globalThis.imports?.gi;if(!e||!e.GLib)throw Error(`@gjsify/rolldown-native: GLib not available (not running under GJS?)`);return e.GLib}const enc=e=>getGLib().Bytes.new(new TextEncoder().encode(e)),dec=e=>new TextDecoder().decode(e.get_data()??new Uint8Array),t=[`load`,`transform`,`resolveId`,`renderChunk`,`banner`,`footer`,`intro`,`outro`,`buildStart`,`buildEnd`,`generateBundle`,`writeBundle`,`closeBundle`];function pluginMeta(e){let n=t.filter(t=>typeof e[t]==`function`),r={name:e.name,hooks:n};return e.idFilter&&(r.idFilter=e.idFilter),r}const n=new Set;function bundleWithPlugins(t,r){let i=e();if(!i)return Promise.reject(Error(`@gjsify/rolldown-native: prebuild not available`));let a=new i.BundlerSession;n.add(a);let o=new Map;a.connect(`context_response`,(e,t,n)=>{let r=o.get(t);if(!r)return;o.delete(t);let i;try{i=JSON.parse(dec(n))}catch(e){r.reject(e instanceof Error?e:Error(String(e)));return}i.error?r.reject(Error(i.error)):i.id?r.resolve({id:i.id,external:i.external??!1}):r.resolve(null)});function makeContext(e){return{resolve(t,n,r){return new Promise((i,s)=>{let c=a.context_resolve(e,enc(JSON.stringify({specifier:t,importer:n,skipSelf:r?.skipSelf??!0,isEntry:r?.isEntry??!1})));if(c===0){s(Error(`@gjsify/rolldown-native: ctx.resolve('${t}') failed — parent req_id ${e} unknown`));return}o.set(c,{resolve:i,reject:s})})},warn(e){a.context_warn(enc(e))},error(e){throw Error(e)}}}a.connect(`hook_requested`,(e,t,n,r,i)=>{dispatchHook(t,n,r,i)});async function dispatchHook(e,t,n,i){let o;try{o=JSON.parse(dec(i))}catch(e){respondError(t,e);return}let s=r[n];if(!s){respondError(t,Error(`@gjsify/rolldown-native: unknown plugin index ${n}`));return}let c=makeContext(t);try{if(e===`transform`&&s.transform){let e=o,n=a.take_request_payload(t);if(n===null)throw Error(`@gjsify/rolldown-native: transform request for ${e.id} missing payload bytes`);let r=dec(n);respondTransform(t,await s.transform.call(c,r,e.id,e.moduleType));return}respondOk(t,await runHook(s,e,o,c))}catch(e){respondError(t,e)}}function respondOk(e,t){let n=t==null?{kind:`skip`}:{kind:`ok`,value:t};a.respond(e,enc(JSON.stringify(n)))}function respondTransform(e,t){if(t==null){a.respond(e,enc(JSON.stringify({kind:`skip`})));return}let n,r;if(typeof t==`string`)n=t;else if(typeof t==`object`){let e=t;n=e.code,r=e.moduleType}if(typeof n!=`string`){a.respond(e,enc(JSON.stringify({kind:`skip`})));return}let i=getGLib().Bytes.new(new TextEncoder().encode(n));if(!a.set_response_payload(e,i)){respondError(e,Error(`@gjsify/rolldown-native: failed to stash transform response payload for reqId ${e}`));return}a.respond(e,enc(JSON.stringify({kind:`ok`,value:r===void 0?{hasCodeBytes:!0}:{hasCodeBytes:!0,moduleType:r}})))}function respondError(e,t){let n=t instanceof Error?t:Error(String(t));a.respond(e,enc(JSON.stringify({kind:`error`,message:n.message,stack:n.stack})))}let closeSession=()=>{n.delete(a);let e=a;try{e.close?.()}catch{}};return new Promise((e,n)=>{a.connect(`completed`,(t,r)=>{try{e(JSON.parse(dec(r)))}catch(e){n(e instanceof Error?e:Error(String(e)))}}),a.connect(`error_occurred`,(e,t)=>{n(Error(t))});try{a.start(enc(JSON.stringify({options:t,plugins:r.map(pluginMeta)})))}catch(e){n(e instanceof Error?e:Error(String(e)))}}).finally(closeSession)}async function runHook(e,t,n,r){switch(t){case`load`:return e.load?normalizeLoadResult(await e.load.call(r,n.id)):null;case`transform`:return null;case`resolveId`:{if(!e.resolveId)return null;let t=n;return normalizeResolveIdResult(await e.resolveId.call(r,t.specifier,t.importer,{isEntry:t.isEntry}))}case`renderChunk`:{if(!e.renderChunk)return null;let t=n;return normalizeRenderChunkResult(await e.renderChunk.call(r,t.code,{fileName:t.fileName,name:t.name,isEntry:t.isEntry}))}case`banner`:case`footer`:case`intro`:case`outro`:{let i=e[t];if(!i)return null;let a=n;return normalizeAddonResult(await i.call(r,{fileName:a.fileName,name:a.name,isEntry:a.isEntry}))}case`buildStart`:case`buildEnd`:case`generateBundle`:case`writeBundle`:case`closeBundle`:{let n=e[t];return n&&await n.call(r),null}default:throw Error(`@gjsify/rolldown-native: unknown hook '${t}'`)}}function normalizeLoadResult(e){return e==null?null:typeof e==`string`?{code:e}:{code:e.code,moduleType:e.moduleType}}function normalizeResolveIdResult(e){return e==null?null:typeof e==`string`?{id:e}:{id:e.id,external:e.external}}function normalizeRenderChunkResult(e){return e==null?null:typeof e==`string`?{code:e}:{code:e.code}}function normalizeAddonResult(e){return e==null?null:typeof e==`string`?e:e.text}export{bundleWithPlugins};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gjsify/rolldown-native",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Phase D-2 POC: Vala/GObject + Rust cdylib bridge to rolldown for GJS. Wraps `rolldown::Bundler::generate()` (async, tokio) in a per-call current-thread runtime so JS sees a sync `bundle()` call. JSON-encoded BundlerOptions in, JSON-encoded BundleOutput out — the boundary stays small even though rolldown's option surface is enormous. POC scope: no JS plugins (Phase B), no watch/HMR, no incremental builds. Companion to `@gjsify/lightningcss-native`; together they remove the last two Rust-crate runtime blockers from the gjsify build pipeline (Phase D-3 unblock).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/esm/index.js",
|
|
@@ -47,8 +47,8 @@
|
|
|
47
47
|
"@girs/gobject-2.0": "2.88.0-4.0.4"
|
|
48
48
|
},
|
|
49
49
|
"devDependencies": {
|
|
50
|
-
"@gjsify/cli": "^0.
|
|
51
|
-
"@types/node": "^25.9.
|
|
50
|
+
"@gjsify/cli": "^0.5.0",
|
|
51
|
+
"@types/node": "^25.9.2",
|
|
52
52
|
"typescript": "^6.0.3"
|
|
53
53
|
},
|
|
54
54
|
"author": "Pascal Garber <pascal@artandcode.studio>",
|
|
@@ -61,6 +61,16 @@
|
|
|
61
61
|
</parameter>
|
|
62
62
|
</parameters>
|
|
63
63
|
</method>
|
|
64
|
+
<method name="close" c:identifier="gjsify_rolldown_bundler_session_close">
|
|
65
|
+
<return-value transfer-ownership="full">
|
|
66
|
+
<type name="none" c:type="void"/>
|
|
67
|
+
</return-value>
|
|
68
|
+
<parameters>
|
|
69
|
+
<instance-parameter name="self" transfer-ownership="none">
|
|
70
|
+
<type name="GjsifyRolldown.BundlerSession" c:type="GjsifyRolldownBundlerSession*"/>
|
|
71
|
+
</instance-parameter>
|
|
72
|
+
</parameters>
|
|
73
|
+
</method>
|
|
64
74
|
<method name="respond" c:identifier="gjsify_rolldown_bundler_session_respond">
|
|
65
75
|
<return-value transfer-ownership="full">
|
|
66
76
|
<type name="none" c:type="void"/>
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -61,6 +61,16 @@
|
|
|
61
61
|
</parameter>
|
|
62
62
|
</parameters>
|
|
63
63
|
</method>
|
|
64
|
+
<method name="close" c:identifier="gjsify_rolldown_bundler_session_close">
|
|
65
|
+
<return-value transfer-ownership="full">
|
|
66
|
+
<type name="none" c:type="void"/>
|
|
67
|
+
</return-value>
|
|
68
|
+
<parameters>
|
|
69
|
+
<instance-parameter name="self" transfer-ownership="none">
|
|
70
|
+
<type name="GjsifyRolldown.BundlerSession" c:type="GjsifyRolldownBundlerSession*"/>
|
|
71
|
+
</instance-parameter>
|
|
72
|
+
</parameters>
|
|
73
|
+
</method>
|
|
64
74
|
<method name="respond" c:identifier="gjsify_rolldown_bundler_session_respond">
|
|
65
75
|
<return-value transfer-ownership="full">
|
|
66
76
|
<type name="none" c:type="void"/>
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -368,8 +368,10 @@ pub struct JsPluginProxy {
|
|
|
368
368
|
/// the same session via Arc clone in the session constructor.
|
|
369
369
|
pub next_request_id: std::sync::Arc<AtomicU64>,
|
|
370
370
|
/// Wakeup pipe written after each request.send() so the Vala
|
|
371
|
-
/// source on the main loop can react.
|
|
372
|
-
|
|
371
|
+
/// source on the main loop can react. Owned handle (see
|
|
372
|
+
/// `SessionShared`): keeps the fd number reserved even when this
|
|
373
|
+
/// proxy outlives the session's runtime shutdown.
|
|
374
|
+
pub request_eventfd: std::sync::Arc<std::os::fd::OwnedFd>,
|
|
373
375
|
/// Maximum time we wait for a JS response before failing the
|
|
374
376
|
/// build with a timeout error. Defaults to 60s.
|
|
375
377
|
pub response_timeout: Duration,
|
|
@@ -467,14 +469,7 @@ impl JsPluginProxy {
|
|
|
467
469
|
return Err(anyhow!("rolldown: plugin channel closed (session aborted)"));
|
|
468
470
|
}
|
|
469
471
|
|
|
470
|
-
|
|
471
|
-
unsafe {
|
|
472
|
-
libc::write(
|
|
473
|
-
self.request_eventfd,
|
|
474
|
-
&one as *const u64 as *const libc::c_void,
|
|
475
|
-
8,
|
|
476
|
-
);
|
|
477
|
-
}
|
|
472
|
+
crate::session::wake_eventfd(&self.request_eventfd);
|
|
478
473
|
|
|
479
474
|
let result = tokio::time::timeout(self.response_timeout, reply_rx).await;
|
|
480
475
|
// Clear the context registration regardless of outcome; the
|
package/src/rust/src/session.rs
CHANGED
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
//! eventfd, see Phase B.2 — for B.1 we expose `wait()` only).
|
|
18
18
|
|
|
19
19
|
use std::ffi::CString;
|
|
20
|
+
use std::os::fd::{AsRawFd, FromRawFd, OwnedFd};
|
|
20
21
|
use std::os::raw::{c_char, c_int};
|
|
21
22
|
use std::ptr;
|
|
22
23
|
use std::sync::Arc;
|
|
@@ -69,7 +70,10 @@ pub struct SessionShared {
|
|
|
69
70
|
pub contexts: Mutex<std::collections::HashMap<u64, PluginContext>>,
|
|
70
71
|
pub next_child_id: AtomicU64,
|
|
71
72
|
pub context_response_tx: Sender<ContextResolveResponse>,
|
|
72
|
-
|
|
73
|
+
/// Owned handle — the fd stays reserved for as long as any clone
|
|
74
|
+
/// (incl. leaked post-shutdown tasks) is alive, so a stale write
|
|
75
|
+
/// can never land in a recycled fd number of a later session.
|
|
76
|
+
pub context_response_eventfd: Arc<OwnedFd>,
|
|
73
77
|
pub context_warnings: Mutex<Vec<String>>,
|
|
74
78
|
/// Phase B.4 — parallel bytes-payload slots keyed by req_id.
|
|
75
79
|
/// `request_payloads` holds Rust → JS payloads (e.g. transform
|
|
@@ -93,11 +97,11 @@ pub struct BundleSession {
|
|
|
93
97
|
/// Pending requests waiting on JS reply. Keyed by req_id.
|
|
94
98
|
pub pending: Mutex<std::collections::HashMap<u64, oneshot::Sender<HookResponse>>>,
|
|
95
99
|
/// eventfd for "request available" wakeup → GLib main loop watches.
|
|
96
|
-
pub request_eventfd:
|
|
100
|
+
pub request_eventfd: Arc<OwnedFd>,
|
|
97
101
|
/// Result of the bundle task once complete. None while still running.
|
|
98
102
|
pub result: Mutex<Option<Result<String, String>>>,
|
|
99
103
|
/// Completion eventfd: written exactly once when `result` is set.
|
|
100
|
-
pub complete_eventfd:
|
|
104
|
+
pub complete_eventfd: Arc<OwnedFd>,
|
|
101
105
|
/// Cancel flag — set by the C side via `cancel()`. tokio task
|
|
102
106
|
/// observes via shared atomic and aborts.
|
|
103
107
|
pub cancelled: Arc<std::sync::atomic::AtomicBool>,
|
|
@@ -167,26 +171,34 @@ pub extern "C" fn gjsify_rolldown_session_start(
|
|
|
167
171
|
// Create the eventfd pair. EFD_NONBLOCK so writes never block;
|
|
168
172
|
// EFD_SEMAPHORE off (we use counter-mode so JS can read once
|
|
169
173
|
// per drain cycle and process all pending requests in a loop).
|
|
174
|
+
// Each fd is wrapped in Arc<OwnedFd> immediately: every holder
|
|
175
|
+
// (proxies, the bundle task, SessionShared, the session itself)
|
|
176
|
+
// keeps a clone, so the fd number stays reserved until the LAST
|
|
177
|
+
// user — including tasks leaked past the runtime shutdown
|
|
178
|
+
// timeout — has dropped it. Closing eagerly in session_free while
|
|
179
|
+
// such tasks still hold the raw number let their deferred
|
|
180
|
+
// `libc::write` land in whatever fd the kernel recycled the
|
|
181
|
+
// number into (issue #501's stale-fd hazard).
|
|
170
182
|
let request_eventfd = unsafe { libc::eventfd(0, libc::EFD_NONBLOCK | libc::EFD_CLOEXEC) };
|
|
171
183
|
if request_eventfd < 0 {
|
|
172
184
|
unsafe { *err_out = err_to_cstr("rolldown: eventfd(request) failed".to_string()) };
|
|
173
185
|
return ptr::null_mut();
|
|
174
186
|
}
|
|
187
|
+
let request_eventfd = Arc::new(unsafe { OwnedFd::from_raw_fd(request_eventfd) });
|
|
175
188
|
let complete_eventfd = unsafe { libc::eventfd(0, libc::EFD_NONBLOCK | libc::EFD_CLOEXEC) };
|
|
176
189
|
if complete_eventfd < 0 {
|
|
177
|
-
unsafe { libc::close(request_eventfd) };
|
|
178
190
|
unsafe { *err_out = err_to_cstr("rolldown: eventfd(complete) failed".to_string()) };
|
|
179
191
|
return ptr::null_mut();
|
|
180
192
|
}
|
|
193
|
+
let complete_eventfd = Arc::new(unsafe { OwnedFd::from_raw_fd(complete_eventfd) });
|
|
181
194
|
|
|
182
195
|
let context_response_eventfd =
|
|
183
196
|
unsafe { libc::eventfd(0, libc::EFD_NONBLOCK | libc::EFD_CLOEXEC) };
|
|
184
197
|
if context_response_eventfd < 0 {
|
|
185
|
-
unsafe { libc::close(request_eventfd) };
|
|
186
|
-
unsafe { libc::close(complete_eventfd) };
|
|
187
198
|
unsafe { *err_out = err_to_cstr("rolldown: eventfd(context_response) failed".to_string()) };
|
|
188
199
|
return ptr::null_mut();
|
|
189
200
|
}
|
|
201
|
+
let context_response_eventfd = Arc::new(unsafe { OwnedFd::from_raw_fd(context_response_eventfd) });
|
|
190
202
|
|
|
191
203
|
let cancelled = Arc::new(std::sync::atomic::AtomicBool::new(false));
|
|
192
204
|
let (request_tx, request_rx) = unbounded::<HookRequest>();
|
|
@@ -197,7 +209,7 @@ pub extern "C" fn gjsify_rolldown_session_start(
|
|
|
197
209
|
contexts: Mutex::new(std::collections::HashMap::new()),
|
|
198
210
|
next_child_id: AtomicU64::new(1),
|
|
199
211
|
context_response_tx,
|
|
200
|
-
context_response_eventfd,
|
|
212
|
+
context_response_eventfd: context_response_eventfd.clone(),
|
|
201
213
|
context_warnings: Mutex::new(Vec::new()),
|
|
202
214
|
request_payloads: Mutex::new(std::collections::HashMap::new()),
|
|
203
215
|
response_payloads: Mutex::new(std::collections::HashMap::new()),
|
|
@@ -222,7 +234,7 @@ pub extern "C" fn gjsify_rolldown_session_start(
|
|
|
222
234
|
hooks: meta.hooks.clone(),
|
|
223
235
|
request_tx: request_tx.clone(),
|
|
224
236
|
next_request_id: next_request_id.clone(),
|
|
225
|
-
request_eventfd,
|
|
237
|
+
request_eventfd: request_eventfd.clone(),
|
|
226
238
|
response_timeout: Duration::from_secs(60),
|
|
227
239
|
load_id_filter,
|
|
228
240
|
transform_id_filter,
|
|
@@ -234,9 +246,6 @@ pub extern "C" fn gjsify_rolldown_session_start(
|
|
|
234
246
|
{
|
|
235
247
|
Ok(v) => v,
|
|
236
248
|
Err(msg) => {
|
|
237
|
-
unsafe { libc::close(request_eventfd) };
|
|
238
|
-
unsafe { libc::close(complete_eventfd) };
|
|
239
|
-
unsafe { libc::close(context_response_eventfd) };
|
|
240
249
|
unsafe { *err_out = err_to_cstr(msg) };
|
|
241
250
|
return ptr::null_mut();
|
|
242
251
|
}
|
|
@@ -254,9 +263,6 @@ pub extern "C" fn gjsify_rolldown_session_start(
|
|
|
254
263
|
{
|
|
255
264
|
Ok(r) => r,
|
|
256
265
|
Err(e) => {
|
|
257
|
-
unsafe { libc::close(request_eventfd) };
|
|
258
|
-
unsafe { libc::close(complete_eventfd) };
|
|
259
|
-
unsafe { libc::close(context_response_eventfd) };
|
|
260
266
|
unsafe { *err_out = err_to_cstr(format!("rolldown: tokio init: {e}")) };
|
|
261
267
|
return ptr::null_mut();
|
|
262
268
|
}
|
|
@@ -264,7 +270,7 @@ pub extern "C" fn gjsify_rolldown_session_start(
|
|
|
264
270
|
|
|
265
271
|
let result_slot: Arc<Mutex<Option<Result<String, String>>>> = Arc::new(Mutex::new(None));
|
|
266
272
|
let result_slot_clone = result_slot.clone();
|
|
267
|
-
let complete_eventfd_for_task = complete_eventfd;
|
|
273
|
+
let complete_eventfd_for_task = complete_eventfd.clone();
|
|
268
274
|
let cancelled_for_task = cancelled.clone();
|
|
269
275
|
let shared_for_task = shared.clone();
|
|
270
276
|
|
|
@@ -276,14 +282,7 @@ pub extern "C" fn gjsify_rolldown_session_start(
|
|
|
276
282
|
};
|
|
277
283
|
*result_slot_clone.lock().unwrap() = Some(final_json);
|
|
278
284
|
// Signal completion to the GLib main loop.
|
|
279
|
-
|
|
280
|
-
unsafe {
|
|
281
|
-
libc::write(
|
|
282
|
-
complete_eventfd_for_task,
|
|
283
|
-
&one as *const u64 as *const libc::c_void,
|
|
284
|
-
8,
|
|
285
|
-
);
|
|
286
|
-
}
|
|
285
|
+
wake_eventfd(&complete_eventfd_for_task);
|
|
287
286
|
});
|
|
288
287
|
|
|
289
288
|
let session = Box::new(BundleSession {
|
|
@@ -390,7 +389,7 @@ pub extern "C" fn gjsify_rolldown_session_request_fd(session: *mut BundleSession
|
|
|
390
389
|
if session.is_null() {
|
|
391
390
|
return -1;
|
|
392
391
|
}
|
|
393
|
-
unsafe { (*session).request_eventfd }
|
|
392
|
+
unsafe { (*session).request_eventfd.as_raw_fd() }
|
|
394
393
|
}
|
|
395
394
|
|
|
396
395
|
#[unsafe(no_mangle)]
|
|
@@ -398,7 +397,7 @@ pub extern "C" fn gjsify_rolldown_session_complete_fd(session: *mut BundleSessio
|
|
|
398
397
|
if session.is_null() {
|
|
399
398
|
return -1;
|
|
400
399
|
}
|
|
401
|
-
unsafe { (*session).complete_eventfd }
|
|
400
|
+
unsafe { (*session).complete_eventfd.as_raw_fd() }
|
|
402
401
|
}
|
|
403
402
|
|
|
404
403
|
/// Drain one pending request. Returns NULL when the channel is
|
|
@@ -541,14 +540,12 @@ pub extern "C" fn gjsify_rolldown_session_free(session: *mut BundleSession) {
|
|
|
541
540
|
}
|
|
542
541
|
unregister_result_slot(session);
|
|
543
542
|
let boxed = unsafe { Box::from_raw(session) };
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
//
|
|
550
|
-
// gives them up to 500ms to drain; any remaining workers are
|
|
551
|
-
// terminated. Prevents the GJS process exit from hanging.
|
|
543
|
+
// Shut the runtime down FIRST (up to 500ms drain; workers that
|
|
544
|
+
// don't finish in time are leaked), THEN let the eventfds drop.
|
|
545
|
+
// The fds are Arc<OwnedFd>, so even a task leaked past the
|
|
546
|
+
// shutdown timeout keeps its clone alive — the fd number stays
|
|
547
|
+
// reserved and a deferred stale write can never corrupt whatever
|
|
548
|
+
// fd the kernel would otherwise have recycled the number into.
|
|
552
549
|
boxed.runtime.shutdown_timeout(Duration::from_millis(500));
|
|
553
550
|
}
|
|
554
551
|
|
|
@@ -612,7 +609,7 @@ pub extern "C" fn gjsify_rolldown_session_context_resolve(
|
|
|
612
609
|
error: Some(format!("rolldown: malformed context_resolve args JSON: {e}")),
|
|
613
610
|
};
|
|
614
611
|
let _ = session.shared.context_response_tx.send(resp);
|
|
615
|
-
wake_eventfd(session.shared.context_response_eventfd);
|
|
612
|
+
wake_eventfd(&session.shared.context_response_eventfd);
|
|
616
613
|
return child_id;
|
|
617
614
|
}
|
|
618
615
|
};
|
|
@@ -657,7 +654,7 @@ pub extern "C" fn gjsify_rolldown_session_context_resolve(
|
|
|
657
654
|
},
|
|
658
655
|
};
|
|
659
656
|
let _ = shared.context_response_tx.send(resp);
|
|
660
|
-
wake_eventfd(shared.context_response_eventfd);
|
|
657
|
+
wake_eventfd(&shared.context_response_eventfd);
|
|
661
658
|
});
|
|
662
659
|
|
|
663
660
|
child_id
|
|
@@ -693,7 +690,7 @@ pub extern "C" fn gjsify_rolldown_session_context_response_fd(
|
|
|
693
690
|
return -1;
|
|
694
691
|
}
|
|
695
692
|
let session = unsafe { &*session };
|
|
696
|
-
session.shared.context_response_eventfd
|
|
693
|
+
session.shared.context_response_eventfd.as_raw_fd()
|
|
697
694
|
}
|
|
698
695
|
|
|
699
696
|
/// FFI: drain one pending context-resolve response. Returns NULL when
|
|
@@ -723,10 +720,10 @@ pub extern "C" fn gjsify_rolldown_session_next_context_response(
|
|
|
723
720
|
CString::new(json).ok().map(|c| c.into_raw()).unwrap_or(ptr::null_mut())
|
|
724
721
|
}
|
|
725
722
|
|
|
726
|
-
fn wake_eventfd(fd:
|
|
723
|
+
pub(crate) fn wake_eventfd(fd: &OwnedFd) {
|
|
727
724
|
let one: u64 = 1;
|
|
728
725
|
unsafe {
|
|
729
|
-
libc::write(fd, &one as *const u64 as *const libc::c_void, 8);
|
|
726
|
+
libc::write(fd.as_raw_fd(), &one as *const u64 as *const libc::c_void, 8);
|
|
730
727
|
}
|
|
731
728
|
}
|
|
732
729
|
|
|
@@ -175,9 +175,18 @@ gjsify_rolldown_glue_session_take_request_payload (BundleSession *session,
|
|
|
175
175
|
if (session == NULL) return NULL;
|
|
176
176
|
size_t len = 0;
|
|
177
177
|
uint8_t *buf = gjsify_rolldown_session_take_request_payload (session, req_id, &len);
|
|
178
|
-
|
|
178
|
+
/* NULL buf = no payload was stashed for this req_id (genuine miss) → NULL.
|
|
179
|
+
* A non-NULL buf with len == 0 is an EMPTY-but-present payload (e.g. the
|
|
180
|
+
* `transform` hook on rolldown's virtual `\0rolldown/empty.js` module, whose
|
|
181
|
+
* code is ""). It MUST yield an empty GBytes, NOT NULL — otherwise the JS
|
|
182
|
+
* adapter reads `take_request_payload() === null` and throws "missing payload
|
|
183
|
+
* bytes", which is exactly what broke the CLI self-build. Don't fold len==0
|
|
184
|
+
* into the miss case. */
|
|
185
|
+
if (buf == NULL) return NULL;
|
|
179
186
|
/* Copy into GLib heap and free the Rust allocation immediately so
|
|
180
|
-
* GBytes refcount/lifetime stays inside the GLib side.
|
|
187
|
+
* GBytes refcount/lifetime stays inside the GLib side. g_bytes_new copies
|
|
188
|
+
* `len` bytes; for len == 0 it never dereferences `buf`, so the Rust
|
|
189
|
+
* dangling-but-aligned zero-length pointer is safe here. */
|
|
181
190
|
GBytes *out = g_bytes_new (buf, len);
|
|
182
191
|
gjsify_rolldown_session_free_payload (buf, len);
|
|
183
192
|
return out;
|
package/src/vala/rolldown.vala
CHANGED
|
@@ -213,17 +213,33 @@ namespace GjsifyRolldown {
|
|
|
213
213
|
// Watch the eventfds on the GLib main loop. add_full hands
|
|
214
214
|
// the source priority + condition; we own the fd lifetime
|
|
215
215
|
// via Rust, so don't pass close_fd:true.
|
|
216
|
+
//
|
|
217
|
+
// IMPORTANT: each watch source holds a strong GObject ref on
|
|
218
|
+
// the session (the explicit `this.ref ()` below, released 1:1
|
|
219
|
+
// in teardown_sources()/close() when the source is removed).
|
|
220
|
+
// `add_watch (cond, on_request_ready)` alone passes `this` as
|
|
221
|
+
// raw user_data WITHOUT a ref, so a session whose JS wrapper
|
|
222
|
+
// became unreachable mid-build (it is only kept alive through
|
|
223
|
+
// its own signal-handler closures — a self cycle) gets
|
|
224
|
+
// collected by the SpiderMonkey GC: dispose() removes the
|
|
225
|
+
// watch sources while the Rust build is still running, every
|
|
226
|
+
// later eventfd wake lands on an unwatched fd, and the build
|
|
227
|
+
// stalls forever (issue #501). The extra refs also flip GJS's
|
|
228
|
+
// toggle-ref into "keep the JS wrapper rooted" mode, so the
|
|
229
|
+
// signal closures survive until the sources are dropped.
|
|
216
230
|
var req_chan = new GLib.IOChannel.unix_new (req_fd);
|
|
217
231
|
req_chan.set_close_on_unref (false);
|
|
218
232
|
req_chan.set_encoding (null);
|
|
219
233
|
req_chan.set_buffered (false);
|
|
220
234
|
_request_source_id = req_chan.add_watch (GLib.IOCondition.IN, on_request_ready);
|
|
235
|
+
this.ref ();
|
|
221
236
|
|
|
222
237
|
var comp_chan = new GLib.IOChannel.unix_new (comp_fd);
|
|
223
238
|
comp_chan.set_close_on_unref (false);
|
|
224
239
|
comp_chan.set_encoding (null);
|
|
225
240
|
comp_chan.set_buffered (false);
|
|
226
241
|
_complete_source_id = comp_chan.add_watch (GLib.IOCondition.IN, on_complete_ready);
|
|
242
|
+
this.ref ();
|
|
227
243
|
|
|
228
244
|
int ctx_fd = _session_context_response_fd (_handle);
|
|
229
245
|
var ctx_chan = new GLib.IOChannel.unix_new (ctx_fd);
|
|
@@ -231,6 +247,25 @@ namespace GjsifyRolldown {
|
|
|
231
247
|
ctx_chan.set_encoding (null);
|
|
232
248
|
ctx_chan.set_buffered (false);
|
|
233
249
|
_ctx_response_source_id = ctx_chan.add_watch (GLib.IOCondition.IN, on_ctx_response_ready);
|
|
250
|
+
this.ref ();
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* close — release the session's resources.
|
|
255
|
+
*
|
|
256
|
+
* Removes the three GLib eventfd watch sources and drops the owned
|
|
257
|
+
* Rust handle (whose `free_function`, gjsify_rolldown_session_free,
|
|
258
|
+
* tears down the bundler + tokio runtime + eventfds). Without this,
|
|
259
|
+
* every bundle leaked its session for the process lifetime — the
|
|
260
|
+
* watch sources hold a ref on the session, so neither the GObject
|
|
261
|
+
* nor the Rust side could ever be collected. Idempotent; safe to
|
|
262
|
+
* call before start() (no-op) and from a JS signal-handler
|
|
263
|
+
* continuation (the watches are removed by id, not from within
|
|
264
|
+
* their own dispatch).
|
|
265
|
+
*/
|
|
266
|
+
public void close () {
|
|
267
|
+
teardown_sources ();
|
|
268
|
+
_handle = null;
|
|
234
269
|
}
|
|
235
270
|
|
|
236
271
|
private bool on_ctx_response_ready (GLib.IOChannel source, GLib.IOCondition cond) {
|
|
@@ -363,17 +398,24 @@ namespace GjsifyRolldown {
|
|
|
363
398
|
}
|
|
364
399
|
|
|
365
400
|
private void teardown_sources () {
|
|
401
|
+
// Each removal releases the strong ref taken in start()
|
|
402
|
+
// when the source was added — strictly 1:1 (the id is
|
|
403
|
+
// zeroed before unref, so a re-entrant call can never
|
|
404
|
+
// double-release).
|
|
366
405
|
if (_request_source_id != 0) {
|
|
367
406
|
GLib.Source.remove (_request_source_id);
|
|
368
407
|
_request_source_id = 0;
|
|
408
|
+
this.unref ();
|
|
369
409
|
}
|
|
370
410
|
if (_complete_source_id != 0) {
|
|
371
411
|
GLib.Source.remove (_complete_source_id);
|
|
372
412
|
_complete_source_id = 0;
|
|
413
|
+
this.unref ();
|
|
373
414
|
}
|
|
374
415
|
if (_ctx_response_source_id != 0) {
|
|
375
416
|
GLib.Source.remove (_ctx_response_source_id);
|
|
376
417
|
_ctx_response_source_id = 0;
|
|
418
|
+
this.unref ();
|
|
377
419
|
}
|
|
378
420
|
}
|
|
379
421
|
|