fulgur_chart 0.7.0 → 0.8.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.
- checksums.yaml +4 -4
- data/ext/fulgur_chart/Cargo.toml +6 -2
- data/ext/fulgur_chart/src/lib.rs +125 -22
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d1b29d4b88f577b96f4d181a1483c1c2218b184f9d9b14f6c7979ef97991ec00
|
|
4
|
+
data.tar.gz: d5f38314eff6a0ac89cbb9926b63e31c55fe84da602f270509fb9cf74228d31b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: fec503464a27652d3a16d7dd4695b0d1aec1b039ae4185e44b960c6e09a121a088954d74e03668cc4f84453c7bbba49e15a1b7a22c85be3a9c6e6469fc7cd8cb
|
|
7
|
+
data.tar.gz: 1b856d3e3179eef76f38706f2b48e965c56c06a064c33b8c88fa884cf444919bd330eb11489cfae14bb3b2c89ad6e0c6822d4691dbdde6957ba2496b76b0571a
|
data/ext/fulgur_chart/Cargo.toml
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[package]
|
|
2
2
|
name = "fulgur_chart"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.8.0"
|
|
4
4
|
edition = "2021"
|
|
5
5
|
publish = false
|
|
6
6
|
|
|
@@ -9,7 +9,11 @@ crate-type = ["cdylib"]
|
|
|
9
9
|
|
|
10
10
|
[dependencies]
|
|
11
11
|
magnus = "0.7"
|
|
12
|
-
|
|
12
|
+
# Raw libruby bindings for rb_thread_call_without_gvl (magnus 0.7 does not wrap it).
|
|
13
|
+
# Already pulled in transitively by magnus; declaring it directly unifies to the same
|
|
14
|
+
# version and makes the symbol importable.
|
|
15
|
+
rb-sys = "0.9"
|
|
16
|
+
fulgur-chart = "0.8.0"
|
|
13
17
|
serde = { version = "1", features = ["derive"] }
|
|
14
18
|
serde_json = "1"
|
|
15
19
|
schemars = { version = "1.2", features = ["preserve_order"] }
|
data/ext/fulgur_chart/src/lib.rs
CHANGED
|
@@ -167,6 +167,112 @@ fn build_ir(
|
|
|
167
167
|
Ok(ir)
|
|
168
168
|
}
|
|
169
169
|
|
|
170
|
+
// --- GVL release ---
|
|
171
|
+
|
|
172
|
+
/// Run `func` with the GVL released, returning `Some(value)` — or `None` if a pending
|
|
173
|
+
/// interrupt prevented `func` from running at all (see below).
|
|
174
|
+
///
|
|
175
|
+
/// Uses `rb_thread_call_without_gvl2`, NOT `rb_thread_call_without_gvl`. The v2 variant does
|
|
176
|
+
/// NOT process interrupts on GVL re-acquisition, so control ALWAYS returns to this Rust frame
|
|
177
|
+
/// after `func` runs: it never raises / longjmps across the `extern "C"` boundary. The v1
|
|
178
|
+
/// variant checks (and handles) pending interrupts right after `func` returns — that handling
|
|
179
|
+
/// can raise, longjmp-ing past this frame and the caller's, skipping our box reclamation and
|
|
180
|
+
/// the owned Rust data's Drop glue (a leak on every interrupted render, and an unsound
|
|
181
|
+
/// longjmp across Rust frames). v2 avoids that: the render runs uninterrupted and a pending
|
|
182
|
+
/// interrupt is honored by Ruby at its next checkpoint, after this call returns.
|
|
183
|
+
///
|
|
184
|
+
/// `func` MUST NOT call any Ruby C API — it executes while this thread does not hold the GVL,
|
|
185
|
+
/// so building VALUEs / raising / allocating Ruby objects would be unsound. (If a global
|
|
186
|
+
/// allocator that calls back into Ruby's GC — e.g. rb-sys's tracking allocator — were ever
|
|
187
|
+
/// installed, even plain Rust heap allocation here would become unsound. None is today.)
|
|
188
|
+
///
|
|
189
|
+
/// `None`: v2 checks for a pending interrupt BEFORE releasing the GVL and, if one is set,
|
|
190
|
+
/// returns without ever calling `func`. Callers fall back to running the work under the GVL,
|
|
191
|
+
/// leaving the still-pending interrupt for Ruby to raise once control returns to it.
|
|
192
|
+
///
|
|
193
|
+
/// A panic inside `func` is caught and re-raised here so it never crosses `extern "C"` (UB).
|
|
194
|
+
fn nogvl<F, R>(func: F) -> Option<R>
|
|
195
|
+
where
|
|
196
|
+
F: FnOnce() -> R,
|
|
197
|
+
{
|
|
198
|
+
use std::ffi::c_void;
|
|
199
|
+
|
|
200
|
+
unsafe extern "C" fn call<F, R>(arg: *mut c_void) -> *mut c_void
|
|
201
|
+
where
|
|
202
|
+
F: FnOnce() -> R,
|
|
203
|
+
{
|
|
204
|
+
// `arg` is the Box<F> leaked below; reconstitute and consume it exactly once.
|
|
205
|
+
let func = *unsafe { Box::from_raw(arg as *mut F) };
|
|
206
|
+
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(func));
|
|
207
|
+
Box::into_raw(Box::new(result)) as *mut c_void
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
let arg = Box::into_raw(Box::new(func)) as *mut c_void;
|
|
211
|
+
// SAFETY: `call::<F, R>` has the rb_thread_call_without_gvl2 callback signature; `arg` is
|
|
212
|
+
// a valid Box<F>. NULL ubf → the render itself is never interrupted mid-flight.
|
|
213
|
+
let ret = unsafe {
|
|
214
|
+
rb_sys::rb_thread_call_without_gvl2(Some(call::<F, R>), arg, None, std::ptr::null_mut())
|
|
215
|
+
};
|
|
216
|
+
if ret.is_null() {
|
|
217
|
+
// v2 declined to run `func` (interrupt pending at entry), so `call` never ran and the
|
|
218
|
+
// closure box was not consumed — reclaim it here to avoid leaking it. (`func` always
|
|
219
|
+
// returns a non-null box pointer, so a null return unambiguously means "did not run".)
|
|
220
|
+
drop(unsafe { Box::from_raw(arg as *mut F) });
|
|
221
|
+
return None;
|
|
222
|
+
}
|
|
223
|
+
// `ret` is the Box<thread::Result<R>> leaked at the end of `call`.
|
|
224
|
+
match *unsafe { Box::from_raw(ret as *mut std::thread::Result<R>) } {
|
|
225
|
+
Ok(value) => Some(value),
|
|
226
|
+
Err(payload) => std::panic::resume_unwind(payload),
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/// Output of the GVL-free render region. Carries only plain data (no Ruby VALUEs) so it
|
|
231
|
+
/// can cross back into the VM once the GVL is re-acquired.
|
|
232
|
+
enum Rendered {
|
|
233
|
+
Svg(String),
|
|
234
|
+
Png(Vec<u8>),
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/// How a render failure is classified once back under the GVL. Mirrors the original
|
|
238
|
+
/// call-site classification: SVG font/render failures → ParseError, raster failures →
|
|
239
|
+
/// RenderError, unknown format → ParseError.
|
|
240
|
+
enum RenderFail {
|
|
241
|
+
Parse(String),
|
|
242
|
+
Render(String),
|
|
243
|
+
UnsupportedFormat(String),
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/// The heavy, Ruby-free rendering. Called inside `nogvl`, so it must touch ONLY the
|
|
247
|
+
/// owned/borrowed plain Rust inputs (`ir`, `font`, `scale`) and never the Ruby C API.
|
|
248
|
+
fn render_pure(
|
|
249
|
+
ir: &fulgur_chart::ir::ChartSpec,
|
|
250
|
+
format: &str,
|
|
251
|
+
font: Option<&[u8]>,
|
|
252
|
+
scale: f32,
|
|
253
|
+
) -> Result<Rendered, RenderFail> {
|
|
254
|
+
match format {
|
|
255
|
+
"svg" => {
|
|
256
|
+
// Font present → render_chart_with_font (Err → ParseError on the SVG path);
|
|
257
|
+
// else the bundled-font render.
|
|
258
|
+
let svg = match font {
|
|
259
|
+
Some(bytes) => fulgur_chart::render::render_chart_with_font(ir, bytes)
|
|
260
|
+
.map_err(RenderFail::Parse)?,
|
|
261
|
+
None => fulgur_chart::render::render_chart(ir),
|
|
262
|
+
};
|
|
263
|
+
Ok(Rendered::Svg(svg))
|
|
264
|
+
}
|
|
265
|
+
"png" => {
|
|
266
|
+
let fb = font.unwrap_or(fulgur_chart::font::DEFAULT_FONT);
|
|
267
|
+
// Invalid font on the image path → RenderError (the SVG path maps this to ParseError).
|
|
268
|
+
let png = fulgur_chart::raster_direct::render_chart_to_png(ir, scale, fb)
|
|
269
|
+
.map_err(RenderFail::Render)?;
|
|
270
|
+
Ok(Rendered::Png(png))
|
|
271
|
+
}
|
|
272
|
+
other => Err(RenderFail::UnsupportedFormat(other.to_string())),
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
170
276
|
// --- public API: render (low-level primitive; the FulgurChart::Builder is the intended API) ---
|
|
171
277
|
|
|
172
278
|
/// `FulgurChart.render(spec_json, format, **opts)` → String.
|
|
@@ -182,28 +288,25 @@ fn render(ruby: &Ruby, args: &[Value]) -> Result<RString, Error> {
|
|
|
182
288
|
let opts = parse_opts(ruby, scanned.keywords)?;
|
|
183
289
|
let ir = build_ir(ruby, &spec_json, &opts)?;
|
|
184
290
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
Ok(ruby.str_from_slice(&png)) // ASCII-8BIT (BINARY) String
|
|
205
|
-
}
|
|
206
|
-
other => Err(parse_err(
|
|
291
|
+
// The heavy rendering touches no Ruby objects (ir/font are owned plain data; font was
|
|
292
|
+
// already copied out of the VM in parse_opts), so run it with the GVL released. Other
|
|
293
|
+
// Ruby threads — including other renders — then run truly in parallel. Ruby strings and
|
|
294
|
+
// exceptions are built BELOW, after the GVL is re-acquired; the closure must not touch
|
|
295
|
+
// the Ruby C API.
|
|
296
|
+
let font = opts.font; // Option<Vec<u8>>, owned
|
|
297
|
+
let scale = opts.scale;
|
|
298
|
+
// Release the GVL for the heavy render. If an interrupt is already pending, nogvl returns
|
|
299
|
+
// None without running the closure; fall back to rendering under the GVL and let Ruby
|
|
300
|
+
// raise the still-pending interrupt when control returns from this method.
|
|
301
|
+
let result = nogvl(|| render_pure(&ir, format.as_str(), font.as_deref(), scale))
|
|
302
|
+
.unwrap_or_else(|| render_pure(&ir, format.as_str(), font.as_deref(), scale));
|
|
303
|
+
|
|
304
|
+
match result {
|
|
305
|
+
Ok(Rendered::Svg(svg)) => Ok(ruby.str_new(&svg)), // UTF-8 String
|
|
306
|
+
Ok(Rendered::Png(png)) => Ok(ruby.str_from_slice(&png)), // ASCII-8BIT (BINARY) String
|
|
307
|
+
Err(RenderFail::Parse(m)) => Err(parse_err(ruby, m)),
|
|
308
|
+
Err(RenderFail::Render(m)) => Err(render_err(ruby, m)),
|
|
309
|
+
Err(RenderFail::UnsupportedFormat(other)) => Err(parse_err(
|
|
207
310
|
ruby,
|
|
208
311
|
format!("unsupported format '{other}' (supported: svg, png)"),
|
|
209
312
|
)),
|