typst 0.15.1.8 → 0.15.1.9.pre

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e504e7d6117a089aea226128ffb96a606d0569184d274a31b786b40323ea2459
4
- data.tar.gz: c54e606e7ec710230a13fd418b8faeb1f427342f6533ebe0cfa1402fbb0ee327
3
+ metadata.gz: 3c7cc49e13eced386fc44cfb914ebf07dc5892490730f3fd4fbed7448e974f74
4
+ data.tar.gz: 1f7e43e5d752bf8817f22ad3a3da673f1370f697d93effe05d69fe15128960e9
5
5
  SHA512:
6
- metadata.gz: db255e768cc4b75a8f233494478140672721c19de71b5c7a8e9f3dc48cbe89fb4a8a372f004fd5136388ba0a6c673a9e133a003ea73dfc285bab6f371108cd50
7
- data.tar.gz: ae2a297b332214c1b83ed24a0862b5288aae8ded7a7d921b198f7777ce5ef091492ff85d4484729f171416a2ab8cd9765b4234f338e2bbd7ca3bf9c6434c315d
6
+ metadata.gz: c44932e11673f6881589eb84e1e7a736e1116adf8c4f9200d4e14da7624eeb24df01320dac16faea7f5ac3e0d8567ea44c400422c94c57e250e7c705d289643c
7
+ data.tar.gz: 8b5689a033596181456b73a321f7df85b3ecd66b67865a33b6030b9560d0474b596f330564b7c335bf0f15bb9e469129fea424d01f1ac97ef1f13f595a7511c8
data/Cargo.lock CHANGED
@@ -2060,18 +2060,18 @@ dependencies = [
2060
2060
 
2061
2061
  [[package]]
2062
2062
  name = "rb-sys"
2063
- version = "0.9.128"
2063
+ version = "0.9.130"
2064
2064
  source = "registry+https://github.com/rust-lang/crates.io-index"
2065
- checksum = "45ca28513560e56cfb79a62b1fce363c73af170a182024ce880c77ee9429920a"
2065
+ checksum = "02faf625bb10ba893e3ae620f19c9fb1b5f8fcae0fe4eb86bb3f2230fad75edb"
2066
2066
  dependencies = [
2067
2067
  "rb-sys-build",
2068
2068
  ]
2069
2069
 
2070
2070
  [[package]]
2071
2071
  name = "rb-sys-build"
2072
- version = "0.9.128"
2072
+ version = "0.9.130"
2073
2073
  source = "registry+https://github.com/rust-lang/crates.io-index"
2074
- checksum = "ce04b2c55eff3a21aaa623fcc655d94373238e72cac6b3e1a3641ff31649f99a"
2074
+ checksum = "05be6f9c86fe5482808162826f6c047d093b3670582296c770de1d94f8066694"
2075
2075
  dependencies = [
2076
2076
  "bindgen",
2077
2077
  "lazy_static",
data/README.md CHANGED
@@ -263,6 +263,58 @@ max_age = 10
263
263
  Typst::clear_cache(max_age)
264
264
  ```
265
265
 
266
+ ### Experimental
267
+
268
+ #### Threads and concurrency
269
+
270
+ A new experimental `concurrent` flag is added to work around Ruby's global VM lock and speed things up for batch workloads.
271
+
272
+ Set the option globally
273
+ ```ruby
274
+ Typst.concurrent = true
275
+ ```
276
+
277
+ Or set the option per call
278
+ ```ruby
279
+ Typst(body: invoice, concurrent: true)
280
+ ```
281
+
282
+ Compiling releases Ruby's global VM lock, so several threads can compile at the
283
+ same time and genuinely use several cores.
284
+
285
+ ```ruby
286
+ docs = invoices.map { |invoice| Thread.new { Typst(body: invoice, concurrent: true).compile(:pdf) } }.map(&:value)
287
+ ```
288
+
289
+ Eight threads compiling the same document, measured on a 24-core machine:
290
+
291
+ | threads | documents/second | cores busy |
292
+ |--------:|-----------------:|-----------:|
293
+ | 1 | 9.9 | 1.00 |
294
+ | 8 | 36.0 | 7.65 |
295
+
296
+ That is throughput across documents, not speed within one. typst parallelizes
297
+ page runs, so a document with a single page layout is one run and stays on one
298
+ core no matter how many threads it is given.
299
+
300
+ Threads share the compilation cache and the font discovery. Compiling different
301
+ sources, with different `sys_inputs`, font paths and roots, at the same time is
302
+ safe.
303
+
304
+ Three things to know:
305
+
306
+ * A compile cannot be interrupted. `Thread#kill` and `Ctrl-C` take effect once
307
+ it returns.
308
+ * Package storage is not safe to populate concurrently. Threads that need the
309
+ same not-yet-cached package each download their own copy and untar it
310
+ straight into the same directory, over each other; on Windows that fails
311
+ outright rather than clobbering. Compile once to warm the cache before
312
+ fanning out. Reading a package already on disk from many threads is safe.
313
+ * `clear_cache` takes the cache's write lock, so calling it in a loop while other
314
+ threads compile will starve them. Measured with four threads compiling and one
315
+ evicting as fast as it could, the compiles took 400 times longer. Call it
316
+ between batches of documents, not between documents.
317
+
266
318
  ## Contributors & Acknowledgements
267
319
  typst-rb is based on [typst-py](https://github.com/messense/typst-py) by [messense](https://github.com/messense)\
268
320
  clear_cache was contributed by [NRicciVestmark](https://github.com/NRicciVestmark)\
@@ -270,7 +322,8 @@ CI improvements were contributed by [am1006](https://github.com/am1006)\
270
322
  Defect resolutions by [adam12](https://github.com/adam12) and [walterdavis](https://github.com/walterdavis)\
271
323
  Design suggestions by [alec-c4](https://github.com/alec-c4)\
272
324
  Compiler warnings were contributed by [TheSoloHacker47](https://github.com/TheSoloHacker47) \
273
- Font optimization patches were contributed by [dmke](https://github.com/dmke)
325
+ Font optimization patches were contributed by [dmke](https://github.com/dmke) \
326
+ Concurrency patches were contributed by [dmke](https://github.com/dmke)
274
327
 
275
328
  ## License
276
329
 
data/README.typ CHANGED
@@ -270,14 +270,69 @@ max_age = 10
270
270
  Typst::clear_cache(max_age)
271
271
  ```
272
272
 
273
+ === Experimental
274
+
275
+ ==== Threads and concurrency
276
+
277
+ A new experimental `concurrent` flag is added to work around Ruby's global VM lock and speed things up for batch workloads.
278
+
279
+ Set the option globally
280
+ ```ruby
281
+ Typst.concurrent = true
282
+ ```
283
+
284
+ Or set the option per call
285
+ ```ruby
286
+ Typst(body: invoice, concurrent: true)
287
+ ```
288
+
289
+ Compiling releases Ruby's global VM lock, so several threads can compile at the
290
+ same time and genuinely use several cores.
291
+
292
+ ```ruby
293
+ docs = invoices.map { |invoice| Thread.new { Typst(body: invoice, concurrent: true).compile(:pdf) } }.map(&:value)
294
+ ```
295
+
296
+ Eight threads compiling the same document, measured on a 24-core machine:
297
+
298
+ #table(
299
+ columns: 3,
300
+ align: (right, right, right),
301
+ table.header([threads], [documents/second], [cores busy]),
302
+ [1], [9.9], [1.00],
303
+ [8], [36.0], [7.65],
304
+ )
305
+
306
+ That is throughput across documents, not speed within one. typst parallelizes
307
+ page runs, so a document with a single page layout is one run and stays on one
308
+ core no matter how many threads it is given.
309
+
310
+ Threads share the compilation cache and the font discovery. Compiling different
311
+ sources, with different `sys_inputs`, font paths and roots, at the same time is
312
+ safe.
313
+
314
+ Three things to know:
315
+
316
+ - A compile cannot be interrupted. `Thread#kill` and `Ctrl-C` take effect once
317
+ it returns.
318
+ - Package storage is not safe to populate concurrently. Threads that need the
319
+ same not-yet-cached package each download their own copy and untar it
320
+ straight into the same directory, over each other; on Windows that fails
321
+ outright rather than clobbering. Compile once to warm the cache before
322
+ fanning out. Reading a package already on disk from many threads is safe.
323
+ - `clear_cache` takes the cache's write lock, so calling it in a loop while other
324
+ threads compile will starve them. Measured with four threads compiling and one
325
+ evicting as fast as it could, the compiles took 400 times longer. Call it
326
+ between batches of documents, not between documents.
327
+
273
328
  == Contributors & Acknowledgements
274
329
  typst-rb is based on #link("https://github.com/messense/typst-py")[typst-py] by #link("https://github.com/messense")[messense]\
275
330
  clear_cache was contributed by #link("https://github.com/NRicciVestmark")[NRicciVestmark]\
276
331
  CI improvements were contributed by #link("https://github.com/am1006")[am1006]\
277
332
  Defect resolutions by #link("https://github.com/adam12")[adam12] and #link("https://github.com/walterdavis")[walterdavis]\
278
333
  Design suggestions by #link("https://github.com/alec-c4")[alec-c4]\
279
- Compiler warnings were contributed by #link("https://github.com/TheSoloHacker47")[TheSoloHacker47] \
280
- Font optimization patches were contributed by #link("https://github.com/dmke")[dmke]
334
+ Compiler warnings were contributed by #link("https://github.com/TheSoloHacker47")[TheSoloHacker47]
335
+ Concurrency patches were contributed by #link("https://github.com/dmke")[dmke]
281
336
 
282
337
  == License
283
338
 
data/Rakefile CHANGED
@@ -30,12 +30,16 @@ end
30
30
 
31
31
  Rake::TestTask.new do |t|
32
32
  t.libs << "test"
33
- t.test_files = FileList['test/*_test.rb']
33
+ # Absolute, because test/typst_test.rb chdirs into its own directory as it
34
+ # loads and the loader resolves what follows it against the working directory.
35
+ t.test_files = FileList['test/*_test.rb'].map { |f| File.expand_path(f) }
34
36
  t.verbose = true
35
37
  end
36
38
 
37
39
  task 'benchmark' do |t|
38
- sh "bundle exec ruby benchmarks/compile_pdf.rb"
40
+ Dir.glob("benchmarks/*.rb").each do |b|
41
+ sh "bundle exec ruby #{b}"
42
+ end
39
43
  end
40
44
 
41
45
  task 'gem:native' do |t|
@@ -49,4 +53,4 @@ task 'gem:native:push' do |t|
49
53
  CROSS_PLATFORMS.each do |platform|
50
54
  sh "gem push pkg/typst-#{spec.version}-#{platform}.gem"
51
55
  end
52
- end
56
+ end
data/ext/typst/Cargo.toml CHANGED
@@ -52,4 +52,4 @@ walkdir = "2.4.0"
52
52
 
53
53
  # enable rb-sys feature to test against Ruby head. This is only needed if you
54
54
  # want to work with the unreleased, in-development, next version of Ruby
55
- rb-sys = { version = "0.9.128", default-features = false, features = ["stable-api-compiled-fallback"] }
55
+ rb-sys = { version = "0.9.130", default-features = false, features = ["stable-api-compiled-fallback"] }
data/ext/typst/extconf.rb CHANGED
@@ -1,4 +1,15 @@
1
1
  require "mkmf"
2
2
  require "rb_sys/mkmf"
3
3
 
4
+ module RbSys
5
+ class CargoBuilder < Gem::Ext::Builder
6
+ alias_method :old_platform_specific_rustc_args, :platform_specific_rustc_args
7
+ def new_platform_specific_rustc_args(dest_dir, flags = [])
8
+ flags += ["-l", "kernel32"] if mingw_target? # add kernel32 before libruby to avoid the sleep bug
9
+ old_platform_specific_rustc_args(dest_dir, flags)
10
+ end
11
+ alias_method :platform_specific_rustc_args, :new_platform_specific_rustc_args
12
+ end
13
+ end
14
+
4
15
  create_rust_makefile("typst/typst")
data/ext/typst/src/lib.rs CHANGED
@@ -7,15 +7,16 @@ use query::{query as typst_query, QueryCommand, SerializationFormat};
7
7
  use typst::foundations::{Dict, Value};
8
8
  use typst_library::Feature;
9
9
  use typst_pdf::PdfStandard;
10
+ use nogvl::without_gvl;
10
11
  use world::SystemWorld;
11
12
 
12
13
  mod compiler;
13
14
  mod download;
15
+ mod nogvl;
14
16
  mod query;
15
17
  mod world;
16
18
 
17
19
  fn to_html(
18
- ruby: &Ruby,
19
20
  input: PathBuf,
20
21
  root: Option<PathBuf>,
21
22
  font_paths: Vec<PathBuf>,
@@ -24,13 +25,13 @@ fn to_html(
24
25
  render_bleed: bool,
25
26
  pretty: bool,
26
27
  sys_inputs: HashMap<String, String>,
27
- ) -> Result<(Vec<Vec<u8>>, Vec<String>), Error> {
28
+ ) -> Result<(Vec<Vec<u8>>, Vec<String>), String> {
28
29
  let input = input.canonicalize()
29
- .map_err(|err| magnus::Error::new(ruby.exception_arg_error(), err.to_string()))?;
30
+ .map_err(|err| err.to_string())?;
30
31
 
31
32
  let root = if let Some(root) = root {
32
33
  root.canonicalize()
33
- .map_err(|err| magnus::Error::new(ruby.exception_arg_error(), err.to_string()))?
34
+ .map_err(|err| err.to_string())?
34
35
  } else if let Some(dir) = input.parent() {
35
36
  dir.into()
36
37
  } else {
@@ -60,32 +61,53 @@ fn to_html(
60
61
  .ignore_system_fonts(ignore_system_fonts)
61
62
  .ignore_embedded_fonts(ignore_embedded_fonts)
62
63
  .build()
63
- .map_err(|msg| magnus::Error::new(ruby.exception_arg_error(), msg.to_string()))?;
64
+ .map_err(|msg| msg.to_string())?;
64
65
 
65
66
  let compiled = world
66
67
  .compile(Some("html"), None, &Vec::new(), pretty, render_bleed)
67
- .map_err(|msg| magnus::Error::new(ruby.exception_arg_error(), msg.to_string()))?;
68
+ .map_err(|msg| msg.to_string())?;
68
69
 
69
70
  Ok(compiled)
70
71
  }
71
72
 
72
- fn to_svg(
73
+ fn route_to_html(
73
74
  ruby: &Ruby,
74
75
  input: PathBuf,
75
76
  root: Option<PathBuf>,
76
77
  font_paths: Vec<PathBuf>,
77
78
  ignore_system_fonts: bool,
78
79
  ignore_embedded_fonts: bool,
80
+ concurrent: bool,
79
81
  render_bleed: bool,
80
82
  pretty: bool,
81
83
  sys_inputs: HashMap<String, String>,
82
84
  ) -> Result<(Vec<Vec<u8>>, Vec<String>), Error> {
85
+ if concurrent {
86
+ without_gvl(move || -> Result<(Vec<Vec<u8>>, Vec<String>), String> {
87
+ to_html(input, root, font_paths, ignore_system_fonts, ignore_embedded_fonts, render_bleed, pretty, sys_inputs)
88
+ })
89
+ } else {
90
+ to_html(input, root, font_paths, ignore_system_fonts, ignore_embedded_fonts, render_bleed, pretty, sys_inputs)
91
+ }
92
+ .map_err(|msg| magnus::Error::new(ruby.exception_arg_error(), msg))
93
+ }
94
+
95
+ fn to_svg(
96
+ input: PathBuf,
97
+ root: Option<PathBuf>,
98
+ font_paths: Vec<PathBuf>,
99
+ ignore_system_fonts: bool,
100
+ ignore_embedded_fonts: bool,
101
+ render_bleed: bool,
102
+ pretty: bool,
103
+ sys_inputs: HashMap<String, String>,
104
+ ) -> Result<(Vec<Vec<u8>>, Vec<String>), String> {
83
105
  let input = input.canonicalize()
84
- .map_err(|err| magnus::Error::new(ruby.exception_arg_error(), err.to_string()))?;
106
+ .map_err(|msg| msg.to_string())?;
85
107
 
86
108
  let root = if let Some(root) = root {
87
109
  root.canonicalize()
88
- .map_err(|err| magnus::Error::new(ruby.exception_arg_error(), err.to_string()))?
110
+ .map_err(|msg| msg.to_string())?
89
111
  } else if let Some(dir) = input.parent() {
90
112
  dir.into()
91
113
  } else {
@@ -102,32 +124,53 @@ fn to_svg(
102
124
  .ignore_system_fonts(ignore_system_fonts)
103
125
  .ignore_embedded_fonts(ignore_embedded_fonts)
104
126
  .build()
105
- .map_err(|msg| magnus::Error::new(ruby.exception_arg_error(), msg.to_string()))?;
127
+ .map_err(|msg| msg.to_string())?;
106
128
 
107
129
  let compiled = world
108
130
  .compile(Some("svg"), None, &Vec::new(), pretty, render_bleed)
109
- .map_err(|msg| magnus::Error::new(ruby.exception_arg_error(), msg.to_string()))?;
131
+ .map_err(|msg| msg.to_string())?;
110
132
 
111
133
  Ok(compiled)
112
134
  }
113
135
 
114
- fn to_png(
136
+ fn route_to_svg(
115
137
  ruby: &Ruby,
116
138
  input: PathBuf,
117
139
  root: Option<PathBuf>,
118
140
  font_paths: Vec<PathBuf>,
119
141
  ignore_system_fonts: bool,
120
142
  ignore_embedded_fonts: bool,
143
+ concurrent: bool,
121
144
  render_bleed: bool,
145
+ pretty: bool,
122
146
  sys_inputs: HashMap<String, String>,
123
- ppi: Option<f32>,
124
147
  ) -> Result<(Vec<Vec<u8>>, Vec<String>), Error> {
148
+ if concurrent {
149
+ without_gvl(move || -> Result<(Vec<Vec<u8>>, Vec<String>), String> {
150
+ to_svg(input, root, font_paths, ignore_system_fonts, ignore_embedded_fonts, render_bleed, pretty, sys_inputs)
151
+ })
152
+ } else {
153
+ to_svg(input, root, font_paths, ignore_system_fonts, ignore_embedded_fonts, render_bleed, pretty, sys_inputs)
154
+ }
155
+ .map_err(|msg| magnus::Error::new(ruby.exception_arg_error(), msg))
156
+ }
157
+
158
+ fn to_png(
159
+ input: PathBuf,
160
+ root: Option<PathBuf>,
161
+ font_paths: Vec<PathBuf>,
162
+ ignore_system_fonts: bool,
163
+ ignore_embedded_fonts: bool,
164
+ render_bleed: bool,
165
+ sys_inputs: HashMap<String, String>,
166
+ ppi: Option<f32>,
167
+ ) -> Result<(Vec<Vec<u8>>, Vec<String>), String> {
125
168
  let input = input.canonicalize()
126
- .map_err(|err| magnus::Error::new(ruby.exception_arg_error(), err.to_string()))?;
169
+ .map_err(|msg| msg.to_string())?;
127
170
 
128
171
  let root = if let Some(root) = root {
129
172
  root.canonicalize()
130
- .map_err(|err| magnus::Error::new(ruby.exception_arg_error(), err.to_string()))?
173
+ .map_err(|msg| msg.to_string())?
131
174
  } else if let Some(dir) = input.parent() {
132
175
  dir.into()
133
176
  } else {
@@ -144,17 +187,38 @@ fn to_png(
144
187
  .ignore_system_fonts(ignore_system_fonts)
145
188
  .ignore_embedded_fonts(ignore_embedded_fonts)
146
189
  .build()
147
- .map_err(|msg| magnus::Error::new(ruby.exception_arg_error(), msg.to_string()))?;
190
+ .map_err(|msg| msg.to_string())?;
148
191
 
149
192
  let compiled = world
150
193
  .compile(Some("png"), ppi, &Vec::new(), false, render_bleed)
151
- .map_err(|msg| magnus::Error::new(ruby.exception_arg_error(), msg.to_string()))?;
194
+ .map_err(|msg| msg.to_string())?;
152
195
 
153
196
  Ok(compiled)
154
197
  }
155
198
 
156
- fn to_pdf(
199
+ fn route_to_png(
157
200
  ruby: &Ruby,
201
+ input: PathBuf,
202
+ root: Option<PathBuf>,
203
+ font_paths: Vec<PathBuf>,
204
+ ignore_system_fonts: bool,
205
+ ignore_embedded_fonts: bool,
206
+ concurrent: bool,
207
+ render_bleed: bool,
208
+ sys_inputs: HashMap<String, String>,
209
+ ppi: Option<f32>,
210
+ ) -> Result<(Vec<Vec<u8>>, Vec<String>), Error> {
211
+ if concurrent {
212
+ without_gvl(move || -> Result<(Vec<Vec<u8>>, Vec<String>), String> {
213
+ to_png(input, root, font_paths, ignore_system_fonts, ignore_embedded_fonts, render_bleed, sys_inputs, ppi)
214
+ })
215
+ } else {
216
+ to_png(input, root, font_paths, ignore_system_fonts, ignore_embedded_fonts, render_bleed, sys_inputs, ppi)
217
+ }
218
+ .map_err(|msg| magnus::Error::new(ruby.exception_arg_error(), msg))
219
+ }
220
+
221
+ fn to_pdf(
158
222
  input: PathBuf,
159
223
  root: Option<PathBuf>,
160
224
  font_paths: Vec<PathBuf>,
@@ -163,13 +227,13 @@ fn to_pdf(
163
227
  pretty: bool,
164
228
  sys_inputs: HashMap<String, String>,
165
229
  pdf_standards: Vec<String>,
166
- ) -> Result<(Vec<Vec<u8>>, Vec<String>), Error> {
230
+ ) -> Result<(Vec<Vec<u8>>, Vec<String>), String> {
167
231
  let input = input.canonicalize()
168
- .map_err(|err| magnus::Error::new(ruby.exception_arg_error(), err.to_string()))?;
232
+ .map_err(|msg| msg.to_string())?;
169
233
 
170
234
  let root = if let Some(root) = root {
171
235
  root.canonicalize()
172
- .map_err(|err| magnus::Error::new(ruby.exception_arg_error(), err.to_string()))?
236
+ .map_err(|msg| msg.to_string())?
173
237
  } else if let Some(dir) = input.parent() {
174
238
  dir.into()
175
239
  } else {
@@ -186,7 +250,7 @@ fn to_pdf(
186
250
  .ignore_system_fonts(ignore_system_fonts)
187
251
  .ignore_embedded_fonts(ignore_embedded_fonts)
188
252
  .build()
189
- .map_err(|msg| magnus::Error::new(ruby.exception_arg_error(), msg.to_string()))?;
253
+ .map_err(|msg| msg.to_string())?;
190
254
 
191
255
  let pdf_standards_lookup: HashMap::<&str, PdfStandard> = HashMap::from([
192
256
  ("1.4", PdfStandard::V_1_4),
@@ -213,19 +277,40 @@ fn to_pdf(
213
277
  let result = pdf_standards_lookup.get(pdf_standard.as_str());
214
278
  match result {
215
279
  Some(value) => pdf_standards_vec.push(*value),
216
- _ => return Err(magnus::Error::new(ruby.exception_arg_error(), "Unknown PdfStandard")),
280
+ _ => return Err("Unknown PdfStandard".to_string()),
217
281
  }
218
282
  }
219
283
 
220
284
  let compiled = world
221
285
  .compile(Some("pdf"), None, &pdf_standards_vec, pretty, true)
222
- .map_err(|msg| magnus::Error::new(ruby.exception_arg_error(), msg.to_string()))?;
286
+ .map_err(|msg| msg.to_string())?;
223
287
 
224
288
  Ok(compiled)
225
289
  }
226
290
 
227
- fn query(
291
+ fn route_to_pdf(
228
292
  ruby: &Ruby,
293
+ input: PathBuf,
294
+ root: Option<PathBuf>,
295
+ font_paths: Vec<PathBuf>,
296
+ ignore_system_fonts: bool,
297
+ ignore_embedded_fonts: bool,
298
+ concurrent: bool,
299
+ pretty: bool,
300
+ sys_inputs: HashMap<String, String>,
301
+ pdf_standards: Vec<String>,
302
+ ) -> Result<(Vec<Vec<u8>>, Vec<String>), Error> {
303
+ if concurrent {
304
+ without_gvl(move || -> Result<(Vec<Vec<u8>>, Vec<String>), String> {
305
+ to_pdf(input, root, font_paths, ignore_system_fonts, ignore_embedded_fonts, pretty, sys_inputs, pdf_standards)
306
+ })
307
+ } else {
308
+ to_pdf(input, root, font_paths, ignore_system_fonts, ignore_embedded_fonts, pretty, sys_inputs, pdf_standards)
309
+ }
310
+ .map_err(|msg| magnus::Error::new(ruby.exception_arg_error(), msg))
311
+ }
312
+
313
+ fn query(
229
314
  selector: String,
230
315
  field: Option<String>,
231
316
  one: bool,
@@ -236,19 +321,19 @@ fn query(
236
321
  ignore_system_fonts: bool,
237
322
  ignore_embedded_fonts: bool,
238
323
  sys_inputs: HashMap<String, String>,
239
- ) -> Result<String, Error> {
324
+ ) -> Result<String, String> {
240
325
  let format = match format.unwrap().to_ascii_lowercase().as_str() {
241
326
  "json" => SerializationFormat::Json,
242
327
  "yaml" => SerializationFormat::Yaml,
243
- _ => return Err(magnus::Error::new(ruby.exception_arg_error(), "unsupported serialization format"))?,
328
+ _ => return Err("unsupported serialization format".to_string()),
244
329
  };
245
330
 
246
331
  let input = input.canonicalize()
247
- .map_err(|err| magnus::Error::new(ruby.exception_arg_error(), err.to_string()))?;
332
+ .map_err(|msg| msg.to_string())?;
248
333
 
249
334
  let root = if let Some(root) = root {
250
335
  root.canonicalize()
251
- .map_err(|err| magnus::Error::new(ruby.exception_arg_error(), err.to_string()))?
336
+ .map_err(|msg| msg.to_string())?
252
337
  } else if let Some(dir) = input.parent() {
253
338
  dir.into()
254
339
  } else {
@@ -265,7 +350,7 @@ fn query(
265
350
  .ignore_system_fonts(ignore_system_fonts)
266
351
  .ignore_embedded_fonts(ignore_embedded_fonts)
267
352
  .build()
268
- .map_err(|msg| magnus::Error::new(ruby.exception_arg_error(), msg.to_string()))?;
353
+ .map_err(|msg| msg.to_string())?;
269
354
 
270
355
  let result = typst_query(
271
356
  &mut world,
@@ -279,8 +364,32 @@ fn query(
279
364
 
280
365
  match result {
281
366
  Ok(data) => Ok(data),
282
- Err(msg) => Err(magnus::Error::new(ruby.exception_arg_error(), msg.to_string())),
367
+ Err(msg) => return Err(msg.to_string()),
368
+ }
369
+ }
370
+
371
+ fn route_query(
372
+ ruby: &Ruby,
373
+ selector: String,
374
+ field: Option<String>,
375
+ one: bool,
376
+ format: Option<String>,
377
+ input: PathBuf,
378
+ root: Option<PathBuf>,
379
+ font_paths: Vec<PathBuf>,
380
+ ignore_system_fonts: bool,
381
+ ignore_embedded_fonts: bool,
382
+ concurrent: bool,
383
+ sys_inputs: HashMap<String, String>,
384
+ ) -> Result<String, Error> {
385
+ if concurrent {
386
+ without_gvl(move || -> Result<String, String> {
387
+ query(selector, field, one, format, input, root, font_paths, ignore_system_fonts, ignore_embedded_fonts, sys_inputs)
388
+ })
389
+ } else {
390
+ query(selector, field, one, format, input, root, font_paths, ignore_system_fonts, ignore_embedded_fonts, sys_inputs)
283
391
  }
392
+ .map_err(|msg| magnus::Error::new(ruby.exception_arg_error(), msg))
284
393
  }
285
394
 
286
395
  fn clear_cache(_ruby: &Ruby, max_age: usize) {
@@ -296,11 +405,11 @@ fn init(ruby: &Ruby) -> Result<(), Error> {
296
405
  env_logger::init();
297
406
 
298
407
  let module = ruby.define_module("Typst")?;
299
- module.define_singleton_method("_to_pdf", function!(to_pdf, 8))?;
300
- module.define_singleton_method("_to_svg", function!(to_svg, 8))?;
301
- module.define_singleton_method("_to_png", function!(to_png, 8))?;
302
- module.define_singleton_method("_to_html", function!(to_html, 8))?;
303
- module.define_singleton_method("_query", function!(query, 10))?;
408
+ module.define_singleton_method("_to_pdf", function!(route_to_pdf, 9))?;
409
+ module.define_singleton_method("_to_svg", function!(route_to_svg, 9))?;
410
+ module.define_singleton_method("_to_png", function!(route_to_png, 9))?;
411
+ module.define_singleton_method("_to_html", function!(route_to_html, 9))?;
412
+ module.define_singleton_method("_query", function!(route_query, 11))?;
304
413
  module.define_singleton_method("_clear_cache", function!(clear_cache, 1))?;
305
414
  module.define_singleton_method("_clear_font_cache", function!(clear_font_cache, 0))?;
306
415
  Ok(())
@@ -0,0 +1,49 @@
1
+ use std::ffi::c_void;
2
+ use std::panic::{catch_unwind, AssertUnwindSafe};
3
+
4
+ /// Runs `f` with the GVL released, so other Ruby threads keep running while
5
+ /// Typst compiles.
6
+ ///
7
+ /// `f` runs on the calling OS thread and must not touch the Ruby API — no
8
+ /// `Ruby` handle, no `Value`, no allocation of Ruby objects. Convert to and
9
+ /// from Ruby types outside of this call.
10
+ ///
11
+ /// No unblocking function is registered, so the call cannot be interrupted;
12
+ /// a compile always runs to completion. That matches the previous behaviour,
13
+ /// where holding the GVL made the compile uninterruptible anyway.
14
+ pub fn without_gvl<F, T>(f: F) -> T
15
+ where
16
+ F: FnOnce() -> T,
17
+ {
18
+ struct Payload<F, T> {
19
+ f: Option<F>,
20
+ result: Option<std::thread::Result<T>>,
21
+ }
22
+
23
+ // A Rust panic must not unwind across the C frame that Ruby pushes here,
24
+ // so it is caught and resumed once we hold the GVL again.
25
+ unsafe extern "C" fn trampoline<F, T>(data: *mut c_void) -> *mut c_void
26
+ where
27
+ F: FnOnce() -> T,
28
+ {
29
+ let payload = &mut *data.cast::<Payload<F, T>>();
30
+ let f = payload.f.take().expect("closure called twice");
31
+ payload.result = Some(catch_unwind(AssertUnwindSafe(f)));
32
+ std::ptr::null_mut()
33
+ }
34
+
35
+ let mut payload = Payload { f: Some(f), result: None };
36
+ unsafe {
37
+ rb_sys::rb_thread_call_without_gvl(
38
+ Some(trampoline::<F, T>),
39
+ (&raw mut payload).cast::<c_void>(),
40
+ None,
41
+ std::ptr::null_mut(),
42
+ );
43
+ }
44
+
45
+ match payload.result.take().expect("closure was not called") {
46
+ Ok(value) => value,
47
+ Err(panic) => std::panic::resume_unwind(panic),
48
+ }
49
+ }
data/lib/base.rb CHANGED
@@ -38,12 +38,13 @@ module Typst
38
38
  options[:ignore_embedded_fonts] ||= false
39
39
  options[:pretty] ||= false
40
40
  options[:render_bleed] ||= false
41
+ options[:concurrent] ||= Typst.concurrent
41
42
 
42
43
  self.options = options
43
44
  end
44
45
 
45
46
  def typst_options
46
- [:file, :root, :font_paths, :ignore_system_fonts, :ignore_embedded_fonts, :render_bleed]
47
+ [:file, :root, :font_paths, :ignore_system_fonts, :ignore_embedded_fonts, :concurrent, :render_bleed]
47
48
  end
48
49
 
49
50
  def typst_args(opts)
data/lib/query.rb CHANGED
@@ -2,9 +2,9 @@ module Typst
2
2
  class Query < Base
3
3
  attr_accessor :format
4
4
 
5
- def initialize(selector, input, field: nil, one: false, format: "json", root: ".", font_paths: [], ignore_system_fonts: false, ignore_embedded_fonts: false, sys_inputs: {})
5
+ def initialize(selector, input, field: nil, one: false, format: "json", root: ".", font_paths: [], ignore_system_fonts: false, ignore_embedded_fonts: false, concurrent: false, sys_inputs: {})
6
6
  self.format = format
7
- @result = Typst::_query(selector, field, one, format, input, root, font_paths, ignore_system_fonts, ignore_embedded_fonts, sys_inputs)
7
+ @result = Typst::_query(selector, field, one, format, input, root, font_paths, ignore_system_fonts, ignore_embedded_fonts, concurrent, sys_inputs)
8
8
  end
9
9
 
10
10
  def result(raw: false)
data/lib/typst.rb CHANGED
@@ -4,6 +4,7 @@ end
4
4
 
5
5
  module Typst
6
6
  @@formats = {}
7
+ @@concurrent = false
7
8
 
8
9
  def self.register_format(**format)
9
10
  @@formats.merge!(format)
@@ -13,6 +14,14 @@ module Typst
13
14
  @@formats
14
15
  end
15
16
 
17
+ def self.concurrent
18
+ @@concurrent
19
+ end
20
+
21
+ def self.concurrent=(value)
22
+ @@concurrent = value
23
+ end
24
+
16
25
  def self.clear_cache(max_age = 0)
17
26
  Typst::_clear_cache(max_age)
18
27
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: typst
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.15.1.8
4
+ version: 0.15.1.9.pre
5
5
  platform: ruby
6
6
  authors:
7
7
  - Flinn
@@ -18,7 +18,7 @@ dependencies:
18
18
  version: '0.9'
19
19
  - - ">="
20
20
  - !ruby/object:Gem::Version
21
- version: 0.9.128
21
+ version: 0.9.130
22
22
  type: :runtime
23
23
  prerelease: false
24
24
  version_requirements: !ruby/object:Gem::Requirement
@@ -28,7 +28,7 @@ dependencies:
28
28
  version: '0.9'
29
29
  - - ">="
30
30
  - !ruby/object:Gem::Version
31
- version: 0.9.128
31
+ version: 0.9.130
32
32
  - !ruby/object:Gem::Dependency
33
33
  name: rubyzip
34
34
  requirement: !ruby/object:Gem::Requirement
@@ -88,6 +88,7 @@ files:
88
88
  - ext/typst/src/compiler.rs
89
89
  - ext/typst/src/download.rs
90
90
  - ext/typst/src/lib.rs
91
+ - ext/typst/src/nogvl.rs
91
92
  - ext/typst/src/package.rs
92
93
  - ext/typst/src/query.rs
93
94
  - ext/typst/src/world.rs