fulgur 0.0.1 → 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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +27 -0
- data/Cargo.toml +30 -0
- data/README.md +210 -9
- data/ext/fulgur/Cargo.toml +30 -0
- data/ext/fulgur/extconf.rb +8 -0
- data/lib/fulgur/asset_bundle.rb +11 -0
- data/lib/fulgur/margin.rb +36 -0
- data/lib/fulgur/version.rb +5 -0
- data/lib/fulgur.rb +15 -11
- data/src/asset_bundle.rs +78 -0
- data/src/engine.rs +297 -0
- data/src/error.rs +57 -0
- data/src/gvl.rs +79 -0
- data/src/lib.rs +40 -0
- data/src/margin.rs +76 -0
- data/src/page_size.rs +93 -0
- data/src/pdf.rs +114 -0
- metadata +41 -14
data/src/engine.rs
ADDED
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
//! `Fulgur::Engine` と `Fulgur::EngineBuilder` の Ruby バインディング。
|
|
2
|
+
//!
|
|
3
|
+
//! - `Engine.new(**kwargs)`: kwargs-only constructor。
|
|
4
|
+
//! - `Engine.builder`: `EngineBuilder` を返す。
|
|
5
|
+
//! - `EngineBuilder#page_size`, `#margin`, `#assets`, `#landscape`, `#title`,
|
|
6
|
+
//! `#author`, `#lang`, `#bookmarks`: setter は `self` を返してチェーンを成立させる。
|
|
7
|
+
//! - `EngineBuilder#build`: `Engine` を返す。2 回目の呼び出しは RuntimeError。
|
|
8
|
+
//!
|
|
9
|
+
//! `Engine#render_html` は `Fulgur::Pdf` を返し、`#render_html_to_file` は
|
|
10
|
+
//! PDF をそのままファイルに書き出す。どちらもレンダリング中は GVL を解放する。
|
|
11
|
+
|
|
12
|
+
use crate::asset_bundle::RbAssetBundle;
|
|
13
|
+
use crate::error::map_fulgur_error;
|
|
14
|
+
use crate::margin::RbMargin;
|
|
15
|
+
use crate::page_size::extract as extract_page_size;
|
|
16
|
+
use crate::pdf::RbPdf;
|
|
17
|
+
use fulgur::{Engine, EngineBuilder};
|
|
18
|
+
use magnus::{
|
|
19
|
+
Error, Module, RModule, Ruby, Value, function, method,
|
|
20
|
+
prelude::*,
|
|
21
|
+
scan_args::{get_kwargs, scan_args},
|
|
22
|
+
};
|
|
23
|
+
use std::cell::RefCell;
|
|
24
|
+
|
|
25
|
+
#[magnus::wrap(class = "Fulgur::EngineBuilder", free_immediately, size)]
|
|
26
|
+
pub struct RbEngineBuilder {
|
|
27
|
+
inner: RefCell<Option<EngineBuilder>>,
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
impl RbEngineBuilder {
|
|
31
|
+
fn new() -> Self {
|
|
32
|
+
Self {
|
|
33
|
+
inner: RefCell::new(Some(Engine::builder())),
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
fn take(&self) -> Result<EngineBuilder, Error> {
|
|
38
|
+
self.inner.borrow_mut().take().ok_or_else(|| {
|
|
39
|
+
Error::new(
|
|
40
|
+
magnus::exception::runtime_error(),
|
|
41
|
+
"EngineBuilder has already been built",
|
|
42
|
+
)
|
|
43
|
+
})
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
fn map(&self, f: impl FnOnce(EngineBuilder) -> EngineBuilder) -> Result<(), Error> {
|
|
47
|
+
let b = self.take()?;
|
|
48
|
+
*self.inner.borrow_mut() = Some(f(b));
|
|
49
|
+
Ok(())
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// -- setter (chain API) --
|
|
54
|
+
//
|
|
55
|
+
// `magnus::typed_data::Obj<T>` は TypedData wrapper を保持する Ruby オブジェクト参照で、
|
|
56
|
+
// `Deref<Target = T>` を実装している。setter は同じ self (Obj) を返して Ruby 側で
|
|
57
|
+
// `.page_size(:a4).margin(...)` の chain を成立させる。
|
|
58
|
+
|
|
59
|
+
fn builder_page_size(
|
|
60
|
+
b: magnus::typed_data::Obj<RbEngineBuilder>,
|
|
61
|
+
value: Value,
|
|
62
|
+
) -> Result<magnus::typed_data::Obj<RbEngineBuilder>, Error> {
|
|
63
|
+
let ps = extract_page_size(value)?;
|
|
64
|
+
b.map(|inner| inner.page_size(ps))?;
|
|
65
|
+
Ok(b)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
fn builder_margin(
|
|
69
|
+
b: magnus::typed_data::Obj<RbEngineBuilder>,
|
|
70
|
+
m: &RbMargin,
|
|
71
|
+
) -> Result<magnus::typed_data::Obj<RbEngineBuilder>, Error> {
|
|
72
|
+
// `Margin` は `Copy`。クロージャに move するためローカルへコピーする。
|
|
73
|
+
let margin = m.inner;
|
|
74
|
+
b.map(|inner| inner.margin(margin))?;
|
|
75
|
+
Ok(b)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
fn builder_assets(
|
|
79
|
+
b: magnus::typed_data::Obj<RbEngineBuilder>,
|
|
80
|
+
bundle: &RbAssetBundle,
|
|
81
|
+
) -> Result<magnus::typed_data::Obj<RbEngineBuilder>, Error> {
|
|
82
|
+
let taken = bundle.take_inner();
|
|
83
|
+
b.map(|inner| inner.assets(taken))?;
|
|
84
|
+
Ok(b)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
fn builder_landscape(
|
|
88
|
+
b: magnus::typed_data::Obj<RbEngineBuilder>,
|
|
89
|
+
v: bool,
|
|
90
|
+
) -> Result<magnus::typed_data::Obj<RbEngineBuilder>, Error> {
|
|
91
|
+
b.map(|inner| inner.landscape(v))?;
|
|
92
|
+
Ok(b)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
fn builder_title(
|
|
96
|
+
b: magnus::typed_data::Obj<RbEngineBuilder>,
|
|
97
|
+
s: String,
|
|
98
|
+
) -> Result<magnus::typed_data::Obj<RbEngineBuilder>, Error> {
|
|
99
|
+
b.map(|inner| inner.title(s))?;
|
|
100
|
+
Ok(b)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
fn builder_author(
|
|
104
|
+
b: magnus::typed_data::Obj<RbEngineBuilder>,
|
|
105
|
+
s: String,
|
|
106
|
+
) -> Result<magnus::typed_data::Obj<RbEngineBuilder>, Error> {
|
|
107
|
+
b.map(|inner| inner.author(s))?;
|
|
108
|
+
Ok(b)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
fn builder_lang(
|
|
112
|
+
b: magnus::typed_data::Obj<RbEngineBuilder>,
|
|
113
|
+
s: String,
|
|
114
|
+
) -> Result<magnus::typed_data::Obj<RbEngineBuilder>, Error> {
|
|
115
|
+
b.map(|inner| inner.lang(s))?;
|
|
116
|
+
Ok(b)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
fn builder_bookmarks(
|
|
120
|
+
b: magnus::typed_data::Obj<RbEngineBuilder>,
|
|
121
|
+
v: bool,
|
|
122
|
+
) -> Result<magnus::typed_data::Obj<RbEngineBuilder>, Error> {
|
|
123
|
+
b.map(|inner| inner.bookmarks(v))?;
|
|
124
|
+
Ok(b)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
fn builder_build(b: magnus::typed_data::Obj<RbEngineBuilder>) -> Result<RbEngine, Error> {
|
|
128
|
+
let built = b.take()?;
|
|
129
|
+
Ok(RbEngine {
|
|
130
|
+
inner: built.build(),
|
|
131
|
+
})
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
#[magnus::wrap(class = "Fulgur::Engine", free_immediately, size)]
|
|
135
|
+
pub struct RbEngine {
|
|
136
|
+
pub(crate) inner: Engine,
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
impl RbEngine {
|
|
140
|
+
/// HTML 文字列を PDF バイト列に変換し、`Fulgur::Pdf` でラップして返す。
|
|
141
|
+
///
|
|
142
|
+
/// レンダリング本体は GVL を解放して実行するため、他の Ruby スレッド
|
|
143
|
+
/// (Thread.new 等) が並行して進める。GVL 解放中のクロージャ内では
|
|
144
|
+
/// Ruby VM に触れてはならないので、エラー変換 (`map_fulgur_error`) は
|
|
145
|
+
/// GVL 再取得後に行う。
|
|
146
|
+
fn render_html(&self, html: String) -> Result<RbPdf, Error> {
|
|
147
|
+
// `Engine: Send + Sync` は `src/lib.rs` の `assert_impl_all!` で
|
|
148
|
+
// コンパイル時に検証済み。raw pointer に一段落としてクロージャへ
|
|
149
|
+
// 渡し、GVL 解放スレッド側で `&Engine` に戻す。
|
|
150
|
+
struct Args {
|
|
151
|
+
engine: *const Engine,
|
|
152
|
+
html: String,
|
|
153
|
+
}
|
|
154
|
+
// SAFETY: `Engine: Sync` なので複数スレッドから &Engine 経由で
|
|
155
|
+
// read-only 参照するのは安全。raw pointer 自体は !Send のため
|
|
156
|
+
// 明示的に unsafe impl で Send を付与する。self (RbEngine) は
|
|
157
|
+
// `without_gvl` の間 block される呼び出し側スタックに生きている
|
|
158
|
+
// ので、dangling にはならない。
|
|
159
|
+
unsafe impl Send for Args {}
|
|
160
|
+
|
|
161
|
+
let args = Args {
|
|
162
|
+
engine: &self.inner as *const Engine,
|
|
163
|
+
html,
|
|
164
|
+
};
|
|
165
|
+
let result: Result<Vec<u8>, fulgur::Error> = crate::gvl::without_gvl(args, |a| {
|
|
166
|
+
// SAFETY: `a.engine` は呼び出し元の `&self.inner` から作った
|
|
167
|
+
// pointer。`without_gvl` は呼び出し元を block しているため、
|
|
168
|
+
// この closure が走っている間 `self` は生存している。
|
|
169
|
+
let engine: &Engine = unsafe { &*a.engine };
|
|
170
|
+
engine.render_html(&a.html)
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
let ruby = Ruby::get().expect("ruby vm");
|
|
174
|
+
match result {
|
|
175
|
+
Ok(bytes) => Ok(RbPdf::new(bytes)),
|
|
176
|
+
Err(e) => Err(map_fulgur_error(&ruby, e)),
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/// HTML を PDF に変換してファイルに書き出す。`render_html` と同じく GVL を
|
|
181
|
+
/// 解放して実行する。`path` は `String` または `to_path` 応答オブジェクト (`Pathname` 等)。
|
|
182
|
+
fn render_html_to_file(&self, html: String, path: Value) -> Result<(), Error> {
|
|
183
|
+
struct Args {
|
|
184
|
+
engine: *const Engine,
|
|
185
|
+
html: String,
|
|
186
|
+
path: std::path::PathBuf,
|
|
187
|
+
}
|
|
188
|
+
// SAFETY: `render_html` と同じ不変条件。`Engine: Send + Sync` は
|
|
189
|
+
// compile-time に assert されている。`without_gvl` は呼び出し元を
|
|
190
|
+
// block するため、closure 実行中に `self` (= *engine) は生きている。
|
|
191
|
+
unsafe impl Send for Args {}
|
|
192
|
+
|
|
193
|
+
let path_str = crate::pdf::coerce_to_path(path)?;
|
|
194
|
+
let args = Args {
|
|
195
|
+
engine: &self.inner as *const Engine,
|
|
196
|
+
html,
|
|
197
|
+
path: std::path::PathBuf::from(path_str),
|
|
198
|
+
};
|
|
199
|
+
let result: Result<(), fulgur::Error> = crate::gvl::without_gvl(args, |a| {
|
|
200
|
+
// SAFETY: `a.engine` は呼び出し元の `&self.inner` から作った
|
|
201
|
+
// pointer。`without_gvl` の block 中は self が生存している。
|
|
202
|
+
let engine: &Engine = unsafe { &*a.engine };
|
|
203
|
+
engine.render_html_to_file(&a.html, &a.path)
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
let ruby = Ruby::get().expect("ruby vm");
|
|
207
|
+
result.map_err(|e| map_fulgur_error(&ruby, e))
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/// kwargs-only constructor。positional args は受け付けない。
|
|
212
|
+
fn engine_new(args: &[Value]) -> Result<RbEngine, Error> {
|
|
213
|
+
let scanned = scan_args::<(), (), (), (), _, ()>(args)?;
|
|
214
|
+
let kw = get_kwargs::<
|
|
215
|
+
_,
|
|
216
|
+
(),
|
|
217
|
+
(
|
|
218
|
+
Option<Value>,
|
|
219
|
+
Option<&RbMargin>,
|
|
220
|
+
Option<bool>,
|
|
221
|
+
Option<String>,
|
|
222
|
+
Option<String>,
|
|
223
|
+
Option<String>,
|
|
224
|
+
Option<bool>,
|
|
225
|
+
Option<&RbAssetBundle>,
|
|
226
|
+
),
|
|
227
|
+
(),
|
|
228
|
+
>(
|
|
229
|
+
scanned.keywords,
|
|
230
|
+
&[],
|
|
231
|
+
&[
|
|
232
|
+
"page_size",
|
|
233
|
+
"margin",
|
|
234
|
+
"landscape",
|
|
235
|
+
"title",
|
|
236
|
+
"author",
|
|
237
|
+
"lang",
|
|
238
|
+
"bookmarks",
|
|
239
|
+
"assets",
|
|
240
|
+
],
|
|
241
|
+
)?;
|
|
242
|
+
let (page_size, margin, landscape, title, author, lang, bookmarks, assets) = kw.optional;
|
|
243
|
+
|
|
244
|
+
let mut b = Engine::builder();
|
|
245
|
+
if let Some(v) = page_size {
|
|
246
|
+
b = b.page_size(extract_page_size(v)?);
|
|
247
|
+
}
|
|
248
|
+
if let Some(m) = margin {
|
|
249
|
+
b = b.margin(m.inner);
|
|
250
|
+
}
|
|
251
|
+
if let Some(v) = landscape {
|
|
252
|
+
b = b.landscape(v);
|
|
253
|
+
}
|
|
254
|
+
if let Some(s) = title {
|
|
255
|
+
b = b.title(s);
|
|
256
|
+
}
|
|
257
|
+
if let Some(s) = author {
|
|
258
|
+
b = b.author(s);
|
|
259
|
+
}
|
|
260
|
+
if let Some(s) = lang {
|
|
261
|
+
b = b.lang(s);
|
|
262
|
+
}
|
|
263
|
+
if let Some(v) = bookmarks {
|
|
264
|
+
b = b.bookmarks(v);
|
|
265
|
+
}
|
|
266
|
+
if let Some(bundle) = assets {
|
|
267
|
+
b = b.assets(bundle.take_inner());
|
|
268
|
+
}
|
|
269
|
+
Ok(RbEngine { inner: b.build() })
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
fn engine_builder() -> RbEngineBuilder {
|
|
273
|
+
RbEngineBuilder::new()
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
pub fn define(_ruby: &Ruby, fulgur: &RModule) -> Result<(), Error> {
|
|
277
|
+
let engine = fulgur.define_class("Engine", magnus::class::object())?;
|
|
278
|
+
engine.define_singleton_method("new", function!(engine_new, -1))?;
|
|
279
|
+
engine.define_singleton_method("builder", function!(engine_builder, 0))?;
|
|
280
|
+
engine.define_method("render_html", method!(RbEngine::render_html, 1))?;
|
|
281
|
+
engine.define_method(
|
|
282
|
+
"render_html_to_file",
|
|
283
|
+
method!(RbEngine::render_html_to_file, 2),
|
|
284
|
+
)?;
|
|
285
|
+
|
|
286
|
+
let builder = fulgur.define_class("EngineBuilder", magnus::class::object())?;
|
|
287
|
+
builder.define_method("page_size", method!(builder_page_size, 1))?;
|
|
288
|
+
builder.define_method("margin", method!(builder_margin, 1))?;
|
|
289
|
+
builder.define_method("assets", method!(builder_assets, 1))?;
|
|
290
|
+
builder.define_method("landscape", method!(builder_landscape, 1))?;
|
|
291
|
+
builder.define_method("title", method!(builder_title, 1))?;
|
|
292
|
+
builder.define_method("author", method!(builder_author, 1))?;
|
|
293
|
+
builder.define_method("lang", method!(builder_lang, 1))?;
|
|
294
|
+
builder.define_method("bookmarks", method!(builder_bookmarks, 1))?;
|
|
295
|
+
builder.define_method("build", method!(builder_build, 0))?;
|
|
296
|
+
Ok(())
|
|
297
|
+
}
|
data/src/error.rs
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
//! Fulgur::Error → Ruby 例外 への写像。
|
|
2
|
+
//!
|
|
3
|
+
//! Ruby 側の `Fulgur::Error` / `Fulgur::RenderError` / `Fulgur::AssetError` クラスは
|
|
4
|
+
//! `lib/fulgur.rb` で既に定義済み。このモジュールでは lookup のみ行い、
|
|
5
|
+
//! `fulgur::Error` の variant に応じて適切な Ruby 例外に変換する。
|
|
6
|
+
|
|
7
|
+
use fulgur::Error as FulgurError;
|
|
8
|
+
use magnus::{Error, ExceptionClass, Module, RModule, Ruby, exception};
|
|
9
|
+
|
|
10
|
+
/// Ruby 側の `Fulgur::<name>` 例外クラスを lookup する。
|
|
11
|
+
fn class(ruby: &Ruby, name: &str) -> Result<ExceptionClass, Error> {
|
|
12
|
+
let fulgur = ruby.class_object().const_get::<_, RModule>("Fulgur")?;
|
|
13
|
+
fulgur.const_get::<_, ExceptionClass>(name)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/// `fulgur::Error` を Ruby の `magnus::Error` に変換する。
|
|
17
|
+
///
|
|
18
|
+
/// - `Io(NotFound)` → `Errno::ENOENT`
|
|
19
|
+
/// - `Io(_)` / `WoffDecode` / `HtmlParse` / `Layout` / `PdfGeneration` / `Template` → `Fulgur::RenderError`
|
|
20
|
+
/// - `Asset` / `UnsupportedFontFormat` → `Fulgur::AssetError`
|
|
21
|
+
///
|
|
22
|
+
/// lookup に失敗した場合は `RuntimeError` にフォールバックする。
|
|
23
|
+
pub fn map_fulgur_error(ruby: &Ruby, err: FulgurError) -> Error {
|
|
24
|
+
match err {
|
|
25
|
+
FulgurError::Io(io_err) => match io_err.kind() {
|
|
26
|
+
std::io::ErrorKind::NotFound => {
|
|
27
|
+
let errno = ruby
|
|
28
|
+
.class_object()
|
|
29
|
+
.const_get::<_, RModule>("Errno")
|
|
30
|
+
.and_then(|m| m.const_get::<_, ExceptionClass>("ENOENT"))
|
|
31
|
+
.unwrap_or_else(|_| exception::runtime_error());
|
|
32
|
+
Error::new(errno, io_err.to_string())
|
|
33
|
+
}
|
|
34
|
+
_ => render_error(ruby, io_err.to_string()),
|
|
35
|
+
},
|
|
36
|
+
FulgurError::Asset(msg) | FulgurError::UnsupportedFontFormat(msg) => asset_error(ruby, msg),
|
|
37
|
+
FulgurError::WoffDecode(msg)
|
|
38
|
+
| FulgurError::HtmlParse(msg)
|
|
39
|
+
| FulgurError::Layout(msg)
|
|
40
|
+
| FulgurError::PdfGeneration(msg)
|
|
41
|
+
| FulgurError::Template(msg) => render_error(ruby, msg),
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
fn render_error(ruby: &Ruby, msg: String) -> Error {
|
|
46
|
+
match class(ruby, "RenderError") {
|
|
47
|
+
Ok(c) => Error::new(c, msg),
|
|
48
|
+
Err(_) => Error::new(exception::runtime_error(), msg),
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
fn asset_error(ruby: &Ruby, msg: String) -> Error {
|
|
53
|
+
match class(ruby, "AssetError") {
|
|
54
|
+
Ok(c) => Error::new(c, msg),
|
|
55
|
+
Err(_) => Error::new(exception::runtime_error(), msg),
|
|
56
|
+
}
|
|
57
|
+
}
|
data/src/gvl.rs
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
//! GVL (Giant VM Lock) 解放ヘルパ。
|
|
2
|
+
//!
|
|
3
|
+
//! magnus 0.7 は GVL 解放 API を持たないため、rb-sys の
|
|
4
|
+
//! `rb_thread_call_without_gvl` を直接呼ぶ。解放中のスレッドで
|
|
5
|
+
//! Ruby VM に触れると UB になるため、closure は Rust-only データで動く
|
|
6
|
+
//! ようにする (`&Engine`, `String` など)。
|
|
7
|
+
//!
|
|
8
|
+
//! # 設計メモ
|
|
9
|
+
//!
|
|
10
|
+
//! - `body` は `fn` pointer (キャプチャ無し)。キャプチャが必要なら
|
|
11
|
+
//! `Data` に必要な値を詰めて渡す。現在の `render_html` 呼び出しでは
|
|
12
|
+
//! `Args { engine: *const Engine, html: String }` 相当を渡す。
|
|
13
|
+
//! - ubf (unblock function) は `None`。Ruby プロセスが signal を受けても
|
|
14
|
+
//! GVL 再取得まで保留される。v0.5.0 では interruptable rendering を
|
|
15
|
+
//! 実装しないので OK。
|
|
16
|
+
//! - `body` 内で panic が発生した場合、`extern "C"` 境界を unwind すると
|
|
17
|
+
//! UB になる (rustc docs 参照)。shim 内で `catch_unwind` で捕捉し、
|
|
18
|
+
//! GVL 再取得後に `resume_unwind` で伝播させる。
|
|
19
|
+
|
|
20
|
+
use std::ffi::c_void;
|
|
21
|
+
use std::panic::{AssertUnwindSafe, catch_unwind, resume_unwind};
|
|
22
|
+
|
|
23
|
+
/// `body` を GVL 解放状態で実行する。
|
|
24
|
+
///
|
|
25
|
+
/// # Safety (呼び出し側契約)
|
|
26
|
+
///
|
|
27
|
+
/// `body` 内で Ruby VM API を呼んではならない。`Value`, `RString`, `Ruby::get()`
|
|
28
|
+
/// のような Ruby 依存の型・関数はすべて禁止。純粋な Rust データ (`String`,
|
|
29
|
+
/// `&Engine` など) のみ操作すること。
|
|
30
|
+
///
|
|
31
|
+
/// `body` 内で panic が発生した場合は `catch_unwind` で捕捉され、GVL 再取得後に
|
|
32
|
+
/// `resume_unwind` で呼び出し側スレッドに伝播する。unwind が `extern "C"` 境界を
|
|
33
|
+
/// 越えないため UB にはならない。
|
|
34
|
+
pub fn without_gvl<Data, Ret>(data: Data, body: fn(Data) -> Ret) -> Ret {
|
|
35
|
+
struct Payload<D, R> {
|
|
36
|
+
data: Option<D>,
|
|
37
|
+
body: fn(D) -> R,
|
|
38
|
+
result: Option<std::thread::Result<R>>,
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
unsafe extern "C" fn shim<D, R>(arg: *mut c_void) -> *mut c_void {
|
|
42
|
+
// SAFETY: arg は `Payload<D, R>` への mutable pointer (caller が作る)。
|
|
43
|
+
// GVL 解放中、他の Ruby スレッドはこの領域を触れない (payload は
|
|
44
|
+
// caller のスタックにあり、caller は block する)。
|
|
45
|
+
let p = unsafe { &mut *(arg as *mut Payload<D, R>) };
|
|
46
|
+
let data = p.data.take().expect("payload data taken twice");
|
|
47
|
+
let body = p.body;
|
|
48
|
+
// panic を catch_unwind で捕捉し、extern "C" 境界を越える unwind (UB) を防ぐ。
|
|
49
|
+
// AssertUnwindSafe は `body: fn(Data) -> Ret` が UnwindSafe でないケース
|
|
50
|
+
// (e.g., `&Engine` を含む Data) でも、panic 後は payload を触らないため安全。
|
|
51
|
+
p.result = Some(catch_unwind(AssertUnwindSafe(move || body(data))));
|
|
52
|
+
std::ptr::null_mut()
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
let mut payload: Payload<Data, Ret> = Payload {
|
|
56
|
+
data: Some(data),
|
|
57
|
+
body,
|
|
58
|
+
result: None,
|
|
59
|
+
};
|
|
60
|
+
// SAFETY: shim は payload を mutable 参照するが、rb_thread_call_without_gvl
|
|
61
|
+
// は caller を block し、shim が終わるまで payload は生存する。ubf=None
|
|
62
|
+
// なので shim は必ず完走し、payload.result が Some になる。
|
|
63
|
+
unsafe {
|
|
64
|
+
rb_sys::rb_thread_call_without_gvl(
|
|
65
|
+
Some(shim::<Data, Ret>),
|
|
66
|
+
&mut payload as *mut _ as *mut c_void,
|
|
67
|
+
None,
|
|
68
|
+
std::ptr::null_mut(),
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
match payload
|
|
72
|
+
.result
|
|
73
|
+
.expect("rb_thread_call_without_gvl did not invoke shim")
|
|
74
|
+
{
|
|
75
|
+
Ok(v) => v,
|
|
76
|
+
// GVL 再取得済みの状態で panic を呼び出し側に伝播する。
|
|
77
|
+
Err(payload) => resume_unwind(payload),
|
|
78
|
+
}
|
|
79
|
+
}
|
data/src/lib.rs
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
//! Ruby bindings for fulgur (HTML/CSS → PDF).
|
|
2
|
+
//!
|
|
3
|
+
//! すべての magnus 依存コードは `ruby-api` feature で gate している。
|
|
4
|
+
//! feature off の場合このクレートは空になり、`cargo build --workspace` が通る。
|
|
5
|
+
//! 実バイナリは `rake compile` が `features = ["ruby-api"]` を注入してビルドする。
|
|
6
|
+
|
|
7
|
+
#![cfg(feature = "ruby-api")]
|
|
8
|
+
|
|
9
|
+
use magnus::{Error, define_module};
|
|
10
|
+
|
|
11
|
+
mod asset_bundle;
|
|
12
|
+
mod engine;
|
|
13
|
+
mod error;
|
|
14
|
+
mod gvl;
|
|
15
|
+
mod margin;
|
|
16
|
+
mod page_size;
|
|
17
|
+
mod pdf;
|
|
18
|
+
|
|
19
|
+
/// `engine.rs` の GVL 解放 (`unsafe impl Send for Args`) は `fulgur::Engine: Send + Sync` の
|
|
20
|
+
/// コンパイル時保証に依存している。`[cfg(test)]` ゲートを付けずに通常ビルドで走らせることで、
|
|
21
|
+
/// `fulgur` crate 側の変更で `Engine` が `!Send` になった瞬間に `rake compile` が落ちる。
|
|
22
|
+
mod assertions {
|
|
23
|
+
use static_assertions::assert_impl_all;
|
|
24
|
+
assert_impl_all!(fulgur::Engine: Send, Sync);
|
|
25
|
+
// `fulgur::AssetBundle` (root re-export) は 0.4.5 には存在せず HEAD のみ。
|
|
26
|
+
// release-ruby は crates.io の ext 依存 (0.4.5 時点では 0.4.5) を使うため、
|
|
27
|
+
// full path `fulgur::asset::AssetBundle` を参照して前方互換を保つ。
|
|
28
|
+
assert_impl_all!(fulgur::asset::AssetBundle: Send, Sync);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
#[magnus::init]
|
|
32
|
+
fn init(ruby: &magnus::Ruby) -> Result<(), Error> {
|
|
33
|
+
let fulgur = define_module("Fulgur")?;
|
|
34
|
+
page_size::define(ruby, &fulgur)?;
|
|
35
|
+
margin::define(ruby, &fulgur)?;
|
|
36
|
+
asset_bundle::define(ruby, &fulgur)?;
|
|
37
|
+
pdf::define(ruby, &fulgur)?;
|
|
38
|
+
engine::define(ruby, &fulgur)?;
|
|
39
|
+
Ok(())
|
|
40
|
+
}
|
data/src/margin.rs
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
//! `Fulgur::Margin` Ruby class wrapping `fulgur::Margin`.
|
|
2
|
+
//!
|
|
3
|
+
//! primitive constructor `__build__(top, right, bottom, left)` を Rust 側で定義し、
|
|
4
|
+
//! Ruby 側 (`lib/fulgur/margin.rb`) が positional / kwargs を解釈して `__build__` を呼び出す。
|
|
5
|
+
//! factory method として `.uniform(pt)` と `.symmetric(vertical, horizontal)` も公開する。
|
|
6
|
+
|
|
7
|
+
use fulgur::Margin;
|
|
8
|
+
use magnus::{Error, Module, RModule, Ruby, function, method, prelude::*};
|
|
9
|
+
|
|
10
|
+
#[magnus::wrap(class = "Fulgur::Margin", free_immediately, size)]
|
|
11
|
+
#[derive(Clone, Copy)]
|
|
12
|
+
pub struct RbMargin {
|
|
13
|
+
pub(crate) inner: Margin,
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
impl RbMargin {
|
|
17
|
+
pub(crate) fn new(inner: Margin) -> Self {
|
|
18
|
+
Self { inner }
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
fn top(&self) -> f32 {
|
|
22
|
+
self.inner.top
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
fn right(&self) -> f32 {
|
|
26
|
+
self.inner.right
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
fn bottom(&self) -> f32 {
|
|
30
|
+
self.inner.bottom
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
fn left(&self) -> f32 {
|
|
34
|
+
self.inner.left
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
fn inspect(&self) -> String {
|
|
38
|
+
format!(
|
|
39
|
+
"#<Fulgur::Margin top={:.2} right={:.2} bottom={:.2} left={:.2}>",
|
|
40
|
+
self.inner.top, self.inner.right, self.inner.bottom, self.inner.left
|
|
41
|
+
)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
fn uniform(pt: f32) -> Self {
|
|
45
|
+
Self::new(Margin::uniform(pt))
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
fn symmetric(vertical: f32, horizontal: f32) -> Self {
|
|
49
|
+
Self::new(Margin::symmetric(vertical, horizontal))
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/// Ruby 側 `Fulgur::Margin.new` から呼ばれる primitive constructor。
|
|
53
|
+
/// Ruby 側が positional / kwargs を解釈して `__build__(t, r, b, l)` を呼び出す。
|
|
54
|
+
fn from_trbl(top: f32, right: f32, bottom: f32, left: f32) -> Self {
|
|
55
|
+
Self::new(Margin {
|
|
56
|
+
top,
|
|
57
|
+
right,
|
|
58
|
+
bottom,
|
|
59
|
+
left,
|
|
60
|
+
})
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
pub fn define(_ruby: &Ruby, fulgur: &RModule) -> Result<(), Error> {
|
|
65
|
+
let class = fulgur.define_class("Margin", magnus::class::object())?;
|
|
66
|
+
class.define_singleton_method("__build__", function!(RbMargin::from_trbl, 4))?;
|
|
67
|
+
class.define_singleton_method("uniform", function!(RbMargin::uniform, 1))?;
|
|
68
|
+
class.define_singleton_method("symmetric", function!(RbMargin::symmetric, 2))?;
|
|
69
|
+
class.define_method("top", method!(RbMargin::top, 0))?;
|
|
70
|
+
class.define_method("right", method!(RbMargin::right, 0))?;
|
|
71
|
+
class.define_method("bottom", method!(RbMargin::bottom, 0))?;
|
|
72
|
+
class.define_method("left", method!(RbMargin::left, 0))?;
|
|
73
|
+
class.define_method("inspect", method!(RbMargin::inspect, 0))?;
|
|
74
|
+
class.define_method("to_s", method!(RbMargin::inspect, 0))?;
|
|
75
|
+
Ok(())
|
|
76
|
+
}
|
data/src/page_size.rs
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
//! `Fulgur::PageSize` Ruby class wrapping `fulgur::PageSize`.
|
|
2
|
+
//!
|
|
3
|
+
//! 定数 `A4` / `LETTER` / `A3`、`.custom(width_mm, height_mm)` クラスメソッド、
|
|
4
|
+
//! インスタンスメソッド `width` / `height` / `landscape` / `inspect` / `to_s` を公開する。
|
|
5
|
+
//!
|
|
6
|
+
//! 併せて、Task 6 以降の engine バインディングから使われる `extract()` ヘルパーを提供し、
|
|
7
|
+
//! `Symbol` / `String` / `Fulgur::PageSize` のいずれでも `fulgur::PageSize` に変換できるようにする。
|
|
8
|
+
|
|
9
|
+
use fulgur::PageSize;
|
|
10
|
+
use magnus::{
|
|
11
|
+
Error, Module, RModule, Ruby, Symbol, TryConvert, Value, function, method, prelude::*,
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
#[magnus::wrap(class = "Fulgur::PageSize", free_immediately, size)]
|
|
15
|
+
#[derive(Clone, Copy)]
|
|
16
|
+
pub struct RbPageSize {
|
|
17
|
+
pub(crate) inner: PageSize,
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
impl RbPageSize {
|
|
21
|
+
pub(crate) fn new(inner: PageSize) -> Self {
|
|
22
|
+
Self { inner }
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
fn width(&self) -> f32 {
|
|
26
|
+
self.inner.width
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
fn height(&self) -> f32 {
|
|
30
|
+
self.inner.height
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
fn landscape(&self) -> Self {
|
|
34
|
+
Self::new(self.inner.landscape())
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
fn inspect(&self) -> String {
|
|
38
|
+
format!(
|
|
39
|
+
"#<Fulgur::PageSize width={:.2} height={:.2}>",
|
|
40
|
+
self.inner.width, self.inner.height
|
|
41
|
+
)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
fn custom(width_mm: f32, height_mm: f32) -> Self {
|
|
45
|
+
Self::new(PageSize::custom(width_mm, height_mm))
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/// `Symbol` / `String` / `Fulgur::PageSize` を `fulgur::PageSize` に変換する。
|
|
50
|
+
/// Task 6 以降の engine バインディングから呼び出される。
|
|
51
|
+
#[allow(dead_code)]
|
|
52
|
+
pub fn extract(value: Value) -> Result<PageSize, Error> {
|
|
53
|
+
if let Ok(ps) = <&RbPageSize>::try_convert(value) {
|
|
54
|
+
return Ok(ps.inner);
|
|
55
|
+
}
|
|
56
|
+
if let Ok(sym) = <Symbol>::try_convert(value) {
|
|
57
|
+
return parse_name(&sym.name()?);
|
|
58
|
+
}
|
|
59
|
+
if let Ok(s) = <String>::try_convert(value) {
|
|
60
|
+
return parse_name(&s);
|
|
61
|
+
}
|
|
62
|
+
Err(Error::new(
|
|
63
|
+
magnus::exception::arg_error(),
|
|
64
|
+
"page_size must be Symbol, String, or Fulgur::PageSize",
|
|
65
|
+
))
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
fn parse_name(name: &str) -> Result<PageSize, Error> {
|
|
69
|
+
match name.to_ascii_uppercase().as_str() {
|
|
70
|
+
"A4" => Ok(PageSize::A4),
|
|
71
|
+
"LETTER" => Ok(PageSize::LETTER),
|
|
72
|
+
"A3" => Ok(PageSize::A3),
|
|
73
|
+
other => Err(Error::new(
|
|
74
|
+
magnus::exception::arg_error(),
|
|
75
|
+
format!("unknown page size: {other}"),
|
|
76
|
+
)),
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
pub fn define(_ruby: &Ruby, fulgur: &RModule) -> Result<(), Error> {
|
|
81
|
+
let class = fulgur.define_class("PageSize", magnus::class::object())?;
|
|
82
|
+
class.define_singleton_method("custom", function!(RbPageSize::custom, 2))?;
|
|
83
|
+
class.define_method("width", method!(RbPageSize::width, 0))?;
|
|
84
|
+
class.define_method("height", method!(RbPageSize::height, 0))?;
|
|
85
|
+
class.define_method("landscape", method!(RbPageSize::landscape, 0))?;
|
|
86
|
+
class.define_method("inspect", method!(RbPageSize::inspect, 0))?;
|
|
87
|
+
class.define_method("to_s", method!(RbPageSize::inspect, 0))?;
|
|
88
|
+
|
|
89
|
+
class.const_set("A4", RbPageSize::new(PageSize::A4))?;
|
|
90
|
+
class.const_set("LETTER", RbPageSize::new(PageSize::LETTER))?;
|
|
91
|
+
class.const_set("A3", RbPageSize::new(PageSize::A3))?;
|
|
92
|
+
Ok(())
|
|
93
|
+
}
|