sghtmltopdf 0.4.0 → 0.5.1

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: 0f317db9c1a48c59df592601e2246186da7539007f19859be3ebbf6c057283bb
4
- data.tar.gz: 10d371ed48aa9b6772c840a75d4cc9e456468c783cae83ee95a0b73588b345a1
3
+ metadata.gz: c1679b41d8745afc3af14b9ea754429a5c9f895b770af172cbbdfba5af87658d
4
+ data.tar.gz: 3a176472e76010d1aa22a365f71b02c0c8ffe1461a1e05518f1a7f7bc8180d7c
5
5
  SHA512:
6
- metadata.gz: ba289e596824c8cbc3a259bd1adfd9397e47c333cdbcefeff1d8e33ae512c64ec197a67d98f5f90a35f46cefc833b837b0348e6625dd69e03ff732a717844d3b
7
- data.tar.gz: 9f6f870dee5e361e175d19d778d3129d416814cb7c7ae0092afe95e59f6469a1f43cbe24a5fa0e9a719c39ee406cb157cbf50285a64a67760f761705558dd9e1
6
+ metadata.gz: 401481476fb5d2dd13aadbac6b5d1e4fe2280826bfb38b6d20be486e825a78a938536821b1a8338273ccf9f5d51317e1cdc1eb557fb779b97a9a32400f9190e3
7
+ data.tar.gz: 4d9002823ff97d3b625be63065c5547269eb7ffde84e65b1eabfef57d92ad0f033539be96a66dd1b1e761dc3bfbfc1ae02f71c7549708dc87f1c77c72f62300a
data/Cargo.lock CHANGED
@@ -927,9 +927,9 @@ dependencies = [
927
927
 
928
928
  [[package]]
929
929
  name = "rustls"
930
- version = "0.23.42"
930
+ version = "0.23.45"
931
931
  source = "registry+https://github.com/rust-lang/crates.io-index"
932
- checksum = "3c54fcab019b409d04215d3a17cb438fd7fbf192ee61461f20f4fe18704bc138"
932
+ checksum = "0d41d731c7d2f962d1ccc364cec258de3c0e93b38c2fb3ba97ac74513048d634"
933
933
  dependencies = [
934
934
  "log",
935
935
  "once_cell",
@@ -951,9 +951,9 @@ dependencies = [
951
951
 
952
952
  [[package]]
953
953
  name = "rustls-webpki"
954
- version = "0.103.13"
954
+ version = "0.103.15"
955
955
  source = "registry+https://github.com/rust-lang/crates.io-index"
956
- checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e"
956
+ checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2"
957
957
  dependencies = [
958
958
  "ring",
959
959
  "rustls-pki-types",
@@ -1050,16 +1050,7 @@ dependencies = [
1050
1050
 
1051
1051
  [[package]]
1052
1052
  name = "sghtmltopdf"
1053
- version = "0.4.0"
1054
- dependencies = [
1055
- "magnus",
1056
- "rb-sys",
1057
- "sghtmltopdf-core",
1058
- ]
1059
-
1060
- [[package]]
1061
- name = "sghtmltopdf-core"
1062
- version = "0.4.0"
1053
+ version = "0.5.1"
1063
1054
  dependencies = [
1064
1055
  "base64",
1065
1056
  "clap",
@@ -1084,6 +1075,15 @@ dependencies = [
1084
1075
  "ureq",
1085
1076
  ]
1086
1077
 
1078
+ [[package]]
1079
+ name = "sghtmltopdf-ruby"
1080
+ version = "0.5.1"
1081
+ dependencies = [
1082
+ "magnus",
1083
+ "rb-sys",
1084
+ "sghtmltopdf",
1085
+ ]
1086
+
1087
1087
  [[package]]
1088
1088
  name = "shell-words"
1089
1089
  version = "1.1.1"
data/README.md CHANGED
@@ -13,10 +13,10 @@ The engine runs inside your process through a native extension (magnus + rb-sys)
13
13
  gem "sghtmltopdf"
14
14
  ```
15
15
 
16
- Precompiled native gems are published for `x86_64-linux`, `aarch64-linux`, `x86_64-linux-musl`, `aarch64-linux-musl`, and `arm64-darwin`.
16
+ Precompiled native gems are published for `x86_64-linux`, `aarch64-linux`, `x86_64-linux-musl`, `aarch64-linux-musl`, `arm64-darwin`, and `x86_64-darwin`.
17
17
  There is no build step on those platforms.
18
18
 
19
- Elsewhere (Intel Mac, Windows) the gem cannot run in-process — the source gem does not carry the Rust core and will refuse to build with an explanatory message.
19
+ Elsewhere (Windows) the gem cannot run in-process — the source gem does not carry the Rust core and will refuse to build with an explanatory message.
20
20
  Point those environments at a separate `sghtmltopdf server` process instead; see [Delegating to a server](#delegating-to-a-server).
21
21
 
22
22
  Requires Ruby >= 3.2.
@@ -38,6 +38,31 @@ Sghtmltopdf.render_to_file(html, "invoice.pdf", page_size: "A4")
38
38
  Sghtmltopdf.render(html) { |bytes| io.write(bytes) }
39
39
  ```
40
40
 
41
+ ## Header and footer HTML
42
+
43
+ Pass markup directly without creating temporary files:
44
+
45
+ ```ruby
46
+ Sghtmltopdf.render(html,
47
+ header_html_content: '<div>Invoice [title]</div>',
48
+ footer_html_content: '<div>Page [page] of [topage]</div>')
49
+ ```
50
+
51
+ These options work with `render`, block output, `render_to_file`, global
52
+ configuration, and server delegation. The shared CLI options are
53
+ `--header-html-content` and `--footer-html-content`.
54
+
55
+ `header_html` and `footer_html` still accept file paths. Supplying both a path
56
+ and content for the same side raises `Sghtmltopdf::UsageError`, including when
57
+ one comes from global configuration. `nil` or `false` disables a configured
58
+ option; an empty string is an explicitly empty header or footer. Either HTML
59
+ form takes precedence over simple text options for that side.
60
+
61
+ Content uses the same placeholder expansion and margin clipping as file input.
62
+ Embedded `data:` images are supported; external resources remain blocked.
63
+ With server delegation, markup travels in URL query parameters, so URL length
64
+ limits may apply and request logs may contain the markup.
65
+
41
66
  ## Rails
42
67
 
43
68
  Adding the gem is enough; the Railtie wires everything up, and nothing is loaded when Rails is absent.
@@ -1,22 +1,28 @@
1
1
  [package]
2
- name = "sghtmltopdf"
3
- version = "0.4.0"
2
+ # The package name differs from the library name on purpose: the core crate is published as
3
+ # `sghtmltopdf`, and two packages with the same name cannot be written to one lockfile.
4
+ name = "sghtmltopdf-ruby"
5
+ version = "0.5.1"
4
6
  edition = "2021"
5
7
  publish = false
6
8
  description = "Ruby binding for sghtmltopdf (see bindings/ruby)"
7
9
 
8
10
  [lib]
9
- # Rubyの拡張ライブラリ(.so/.bundle)として読み込まれる。
11
+ # Loaded as a Ruby extension library (.so/.bundle). The name has to stay `sghtmltopdf`:
12
+ # rb_sys looks for `libsghtmltopdf.so` (it derives that from the gem name) and
13
+ # `Init_sghtmltopdf` is the entry point Ruby calls.
14
+ name = "sghtmltopdf"
10
15
  crate-type = ["cdylib"]
11
16
 
12
17
  [dependencies]
13
- # `rb-sys` featureで`AsRawValue`/`FromRawValue`が使えるようになる
18
+ # The `rb-sys` feature is what makes `AsRawValue`/`FromRawValue` available.
14
19
  magnus = { version = "0.8", features = ["rb-sys"] }
15
- # GVL解放(`rb_thread_call_without_gvl`)はmagnusに無いので直接呼ぶ。
20
+ # magnus has no way to release the GVL (`rb_thread_call_without_gvl`), so we call it directly.
16
21
  rb-sys = "0.9"
17
- # HTTPサーバ(tiny_http)は要らないので`server` featureは外す。
18
- # `svg`はCLIと同じ既定に揃える(gemからもSVGを描画できるようにする)。
19
- sghtmltopdf-core = { path = "../../../../core", default-features = false, features = [
22
+ # The HTTP server (tiny_http) is not needed here, so the `server` feature is left out.
23
+ # `svg` matches the CLI default, so the gem can render SVG too.
24
+ # Renamed on import so that the `sghtmltopdf_core::` paths in the source stay as they are.
25
+ sghtmltopdf_core = { package = "sghtmltopdf", path = "../../../../core", default-features = false, features = [
20
26
  "cli",
21
27
  "svg",
22
28
  ] }
@@ -6,16 +6,16 @@ require "rb_sys/mkmf"
6
6
  core = File.expand_path("../../../../core", __dir__)
7
7
  unless File.exist?(File.join(core, "Cargo.toml"))
8
8
  abort <<~MESSAGE
9
- sghtmltopdf: Rustコア(#{core})が見つかりません。
9
+ sghtmltopdf: the Rust core (#{core}) was not found.
10
10
 
11
- このgemは対応プラットフォーム向けのprecompiled gemとして配布しています。
12
- お使いの環境(#{RUBY_PLATFORM} / ruby #{RUBY_VERSION})向けのビルド済みgemが
13
- 無いためソースからのビルドが試みられましたが、ソースgemにはRustコアが
14
- 含まれていないためビルドできません。
11
+ This gem is distributed as a precompiled gem for the supported platforms.
12
+ No prebuilt gem exists for your environment (#{RUBY_PLATFORM} / ruby #{RUBY_VERSION}),
13
+ so a build from source was attempted, but the source gem does not include the
14
+ Rust core and cannot be built.
15
15
 
16
- 対応プラットフォーム: x86_64-linux / aarch64-linux / x86_64-linux-musl / aarch64-linux-musl / arm64-darwin
16
+ Supported platforms: x86_64-linux / aarch64-linux / x86_64-linux-musl / aarch64-linux-musl / arm64-darwin / x86_64-darwin
17
17
  MESSAGE
18
18
  end
19
19
 
20
- # `lib/sghtmltopdf/sghtmltopdf.so`として作る。
20
+ # Built as `lib/sghtmltopdf/sghtmltopdf.so`.
21
21
  create_rust_makefile("sghtmltopdf/sghtmltopdf")
@@ -1,28 +1,28 @@
1
- //! 確定したPDFのバイト列を、Rubyのブロックへチャンクごとに渡す仕組み。
1
+ //! Handing the settled PDF bytes to a Ruby block, chunk by chunk.
2
2
  //!
3
- //! # スレッドの分け方
3
+ //! # How the threads are split
4
4
  //!
5
- //! レンダリングはDOMの深さぶん再帰する(スタイル計算・レイアウト・描画)
6
- //! Rubyのスレッドのマシンスタックは既定1MiB(`RubyVM::DEFAULT_PARAMS`の
7
- //! `thread_machine_stack_size`)しかなく、Pumaのワーカースレッド上でそのまま
8
- //! 走らせると深さ200弱でスタックを溢れさせる。しかもGVLを解放した状態で
9
- //! ガードページに触れるため、プロセスが落ちるのではなくスレッドが固まる。
5
+ //! Rendering recurses as deep as the DOM (style computation, layout, drawing).
6
+ //! A Ruby thread's machine stack is 1MiB by default (`thread_machine_stack_size` in
7
+ //! `RubyVM::DEFAULT_PARAMS`), so running it directly on a Puma worker thread overflows the
8
+ //! stack at a depth just under 200. Worse, it touches the guard page with the GVL released,
9
+ //! so rather than the process dying the thread hangs.
10
10
  //!
11
- //! そこでレンダリングは[`sghtmltopdf_core::render_stack::STACK_SIZE`]のスタックを
12
- //! 明示的に確保した専用スレッドで走らせ、確定したチャンクはチャネル越しに
13
- //! 元のスレッドへ渡す。Rubyへ触れるのは元のスレッドだけに限る。
11
+ //! So rendering runs on a dedicated thread with a
12
+ //! [`sghtmltopdf_core::STACK_SIZE`] stack allocated explicitly, and the settled
13
+ //! chunks are passed back to the original thread over a channel. Only the original thread ever touches Ruby.
14
14
  //!
15
15
  //! ```text
16
- //! 元のスレッド(Rubyが作った / GVL解放中) レンダリングスレッド(16MiB)
16
+ //! original thread (Ruby's, GVL released) rendering thread (16MiB)
17
17
  //! recv(chunk) <---------- chunk ---------- Sink::write
18
18
  //! with_gvl { block.call(chunk) }
19
- //! send(ack) ------------ ack ----------> (次のチャンクへ)
19
+ //! send(ack) ------------ ack ----------> (on to the next chunk)
20
20
  //! ```
21
21
  //!
22
- //! この向きでないと成立しない: [`crate::gvl::with_gvl`]
23
- //! `rb_thread_call_with_gvl`は「そのスレッドが`rb_thread_call_without_gvl`で
24
- //! GVLを手放している」ことが前提で、Rubyの知らないスレッドから呼ぶことは
25
- //! できない。だからレンダリングスレッドはRubyに一切触れない。
22
+ //! It only works this way round: [`crate::gvl::with_gvl`]'s `rb_thread_call_with_gvl`
23
+ //! assumes "this thread released the GVL through `rb_thread_call_without_gvl`" and cannot
24
+ //! be called from a thread Ruby does not know about. So the rendering thread never touches
25
+ //! Ruby at all.
26
26
 
27
27
  use std::io;
28
28
  use std::sync::mpsc::{Receiver, SyncSender};
@@ -30,29 +30,28 @@ use std::sync::mpsc::{Receiver, SyncSender};
30
30
  use magnus::rb_sys::{AsRawValue, FromRawValue};
31
31
  use magnus::{block::Proc, Error, ExceptionClass, RString, Ruby, Value};
32
32
  use rb_sys::VALUE;
33
- use sghtmltopdf_core::sink::Sink;
33
+ use sghtmltopdf_core::Sink;
34
34
 
35
35
  use crate::gvl;
36
36
 
37
- /// ブロックが中断したときに`Sink::write`が返すエラー。
37
+ /// The error `Sink::write` returns when the block was interrupted.
38
38
  ///
39
- /// `convert::render`が`Sink<Output = (), Error = io::Error>`を要求するため、
40
- /// Ruby由来の情報をエラーの型に載せられない。本当の理由は
41
- /// [`PendingUnwind`]へ置き、こちらは「巻き戻すための合図」として使う。
39
+ /// `convert::render` requires `Sink<Output = (), Error = io::Error>`, so no Ruby-derived
40
+ /// information can ride on the error type. The real reason is put in [`PendingUnwind`] and
41
+ /// this is used purely as "the signal to unwind".
42
42
  fn interrupted() -> io::Error {
43
- io::Error::other("Rubyのブロックが中断しました")
43
+ io::Error::other("the Ruby block was interrupted")
44
44
  }
45
45
 
46
- /// `rb_gc_register_address`でGCから守った`VALUE`の置き場。
46
+ /// Somewhere to keep a `VALUE` protected from the GC with `rb_gc_register_address`.
47
47
  ///
48
- /// Rubyの保守的GCはマシンスタックを走査してVALUEを見つけるが、
49
- /// GVLを解放した時点のスタック位置までしか走査しない
50
- /// (解放時にマシンコンテキストが保存されるため)。`without_gvl`の内側で
51
- /// スタックに積んだ値はその先にあるので走査されない。解放区間をまたいで
52
- /// 生かしたいVALUEは、必ずここへ登録する。
48
+ /// Ruby's conservative GC scans the machine stack to find VALUEs, but only as far as the
49
+ /// stack position at the moment the GVL was released (the machine context being saved then).
50
+ /// A value pushed onto the stack inside `without_gvl` lies beyond that and is not scanned.
51
+ /// Any VALUE that must survive across the released region has to be registered here.
53
52
  ///
54
- /// 登録アドレスは`Box`で固定する。GCのコンパクションでオブジェクトが移動
55
- /// しても、登録したアドレスの中身は更新されるため、古い参照を掴まない。
53
+ /// The registered address is pinned with a `Box`. Even when GC compaction moves the object,
54
+ /// the contents at the registered address are updated, so no stale reference is held.
56
55
  pub struct ValueSlot {
57
56
  slot: Box<VALUE>,
58
57
  }
@@ -64,12 +63,12 @@ impl ValueSlot {
64
63
  Self { slot }
65
64
  }
66
65
 
67
- /// GC登録済みスロットのアドレス。
66
+ /// The address of the GC-registered slot.
68
67
  pub fn addr(&self) -> *mut VALUE {
69
68
  &*self.slot as *const VALUE as *mut VALUE
70
69
  }
71
70
 
72
- /// 現在の`VALUE`。GVLを保持している間だけ呼ぶこと。
71
+ /// The current `VALUE`. Call only while holding the GVL.
73
72
  pub fn get(&self) -> VALUE {
74
73
  *self.slot
75
74
  }
@@ -81,13 +80,13 @@ impl Drop for ValueSlot {
81
80
  }
82
81
  }
83
82
 
84
- /// GVL解放区間へ運ぶための、[`ValueSlot`]のアドレス。
83
+ /// The address of a [`ValueSlot`], for carrying into a GVL-released region.
85
84
  #[derive(Clone, Copy)]
86
85
  pub struct BlockSlot(*mut VALUE);
87
86
 
88
- // SAFETY: 解放区間ではアドレスを数値として持ち回るだけで、`VALUE`として
89
- // 読むのは`with_gvl`の内側(GVLを保持している間)に限る。指す先は
90
- // `ValueSlot`がGCに登録済みで、`without_gvl`が返るまで生きている。
87
+ // SAFETY: in the released region the address is only carried around as a number; it is read
88
+ // as a `VALUE` only inside `with_gvl` (that is, while holding the GVL). What it points at is
89
+ // a `ValueSlot` registered with the GC and alive until `without_gvl` returns.
91
90
  unsafe impl Send for BlockSlot {}
92
91
 
93
92
  impl BlockSlot {
@@ -95,36 +94,36 @@ impl BlockSlot {
95
94
  Self(slot.addr())
96
95
  }
97
96
 
98
- /// GVLを保持している前提で`Proc`へ戻す。
97
+ /// Convert back to a `Proc`, assuming the GVL is held.
99
98
  fn proc(self) -> Option<Proc> {
100
99
  let value = unsafe { Value::from_raw(*self.0) };
101
100
  Proc::from_value(value)
102
101
  }
103
102
  }
104
103
 
105
- /// ブロックが投げた例外・脱出を、GVL解放区間の外へ運ぶための受け皿。
104
+ /// The receptacle for carrying an exception or non-local exit thrown by the block out of the GVL-released region.
106
105
  #[derive(Default)]
107
106
  pub struct PendingUnwind {
108
107
  unwind: Option<Unwind>,
109
108
  }
110
109
 
111
110
  enum Unwind {
112
- /// Rubyの例外オブジェクト。
111
+ /// A Ruby exception object.
113
112
  Exception(ValueSlot),
114
- /// Rust(magnus)が組み立てたエラー。クラスとメッセージを別々に運ぶ。
113
+ /// An error the Rust side (magnus) built. The class and the message are carried separately.
115
114
  Raise { class: ValueSlot, message: String },
116
- /// `break`・`return`・`throw`など。値は`rb_jump_tag`へ渡すタグ。
115
+ /// A `break`, `return`, `throw` and so on. The value is the tag passed to `rb_jump_tag`.
117
116
  Jump(i32),
118
117
  }
119
118
 
120
119
  impl PendingUnwind {
121
- /// ブロックが中断していれば`true`。
120
+ /// Whether the block was interrupted.
122
121
  pub fn is_pending(&self) -> bool {
123
122
  self.unwind.is_some()
124
123
  }
125
124
 
126
- /// magnusの`Error`を、解放区間をまたげる形に変換して保存する。
127
- /// GVLを保持している間に呼ぶこと(GCへの登録を行うため)
125
+ /// Convert a magnus `Error` into a form that can cross the released region and store it.
126
+ /// Call it while holding the GVL (it registers with the GC).
128
127
  fn store(&mut self, error: Error) {
129
128
  use magnus::error::ErrorType;
130
129
 
@@ -141,26 +140,26 @@ impl PendingUnwind {
141
140
  self.unwind = Some(unwind);
142
141
  }
143
142
 
144
- /// 保存した中断をRubyへ返す。GVLを保持している間に呼ぶこと。
143
+ /// Return the stored interruption to Ruby. Call it while holding the GVL.
145
144
  ///
146
- /// `break`などの脱出は`rb_jump_tag`で忠実に伝播させる。この関数は
147
- /// そこから戻らないため、Rust側の後始末が済んでから呼ぶこと
148
- /// (`ValueSlot`のGC登録解除もこの関数の中で済ませてある)
145
+ /// A non-local exit such as `break` is propagated faithfully with `rb_jump_tag`. This
146
+ /// function does not return, so call it once the Rust-side cleanup is done
147
+ /// (deregistering the `ValueSlot` from the GC is already handled inside it).
149
148
  pub fn into_error(self) -> Option<Error> {
150
149
  match self.unwind? {
151
150
  Unwind::Exception(slot) => {
152
151
  let value = unsafe { Value::from_raw(slot.get()) };
153
- // 登録を外すのは`Error`を組み立てたあと。ここから先は
154
- // 呼び出し元がGVLを保持したままRubyへ戻るので、
155
- // 保守的GCの走査範囲に入る。
152
+ // Deregistration happens after the `Error` is built. From here on the caller
153
+ // returns to Ruby still holding the GVL, so it is within the conservative
154
+ // GC's scanning range.
156
155
  let error = magnus::Exception::from_value(value).map(Error::from);
157
156
  drop(slot);
158
157
  Some(error.unwrap_or_else(|| {
159
158
  Error::new(
160
159
  Ruby::get()
161
- .expect("GVLを保持したまま呼ばれるはず")
160
+ .expect("it should be called while holding the GVL")
162
161
  .exception_runtime_error(),
163
- "ブロックが投げた例外を復元できませんでした",
162
+ "could not restore the exception the block threw",
164
163
  )
165
164
  }))
166
165
  }
@@ -171,24 +170,24 @@ impl PendingUnwind {
171
170
  Some(error.unwrap_or_else(|| {
172
171
  Error::new(
173
172
  Ruby::get()
174
- .expect("GVLを保持したまま呼ばれるはず")
173
+ .expect("it should be called while holding the GVL")
175
174
  .exception_runtime_error(),
176
- "ブロックの中断を復元できませんでした",
175
+ "could not restore the block's interruption",
177
176
  )
178
177
  }))
179
178
  }
180
- // `rb_jump_tag`は戻らない(`-> !`)。`self`の他のフィールドは
181
- // ここまでで全部落ちている。
179
+ // `rb_jump_tag` does not return (`-> !`). Every other field of `self` has already
180
+ // been dropped by this point.
182
181
  Unwind::Jump(tag) => unsafe { rb_sys::rb_jump_tag(tag) },
183
182
  }
184
183
  }
185
184
  }
186
185
 
187
- /// 確定したバイト列を`chunk_size`ごとにチャネルへ流すSink。
186
+ /// A Sink streaming the settled bytes to a channel in `chunk_size` pieces.
188
187
  ///
189
- /// レンダリングスレッド側で使う。Rubyには一切触れないので、`Send`であり
190
- /// GVLの制約とも無縁。1チャンク送るごとに受け取り側の応答を待つ
191
- /// (rendezvous)ことで、ブロックの処理より先に走ってメモリを溜め込まない。
188
+ /// Used on the rendering thread. It never touches Ruby, so it is `Send` and free of the
189
+ /// GVL's constraints. It waits for the receiving side's acknowledgement after every chunk
190
+ /// (a rendezvous), so it cannot run ahead of the block's processing and pile up memory.
192
191
  pub struct ChannelSink {
193
192
  chunks: SyncSender<Vec<u8>>,
194
193
  ack: Receiver<bool>,
@@ -202,16 +201,16 @@ impl ChannelSink {
202
201
  chunks,
203
202
  ack,
204
203
  buf: Vec::new(),
205
- // 0だと1バイトごとにGVLを取り直すことになるため下限を設ける。
204
+ // At 0 the GVL would be reacquired for every byte, hence the lower bound.
206
205
  chunk_size: chunk_size.max(1),
207
206
  }
208
207
  }
209
208
 
210
- /// 1チャンク渡して、ブロックが受け取り終えるまで待つ。
209
+ /// Hand over one chunk and wait until the block has finished receiving it.
211
210
  ///
212
- /// 送れない(受け取り側が降りた)場合と、ブロックが中断を返した場合は
213
- /// どちらも[`interrupted`]で巻き戻す。中断の本当の理由は受け取り側の
214
- /// [`PendingUnwind`]に入っている。
211
+ /// Both being unable to send (the receiver went away) and the block returning an
212
+ /// interruption unwind through [`interrupted`]. The real reason for the interruption is
213
+ /// in the receiver's [`PendingUnwind`].
215
214
  fn hand_off(&mut self, chunk: Vec<u8>) -> Result<(), io::Error> {
216
215
  if self.chunks.send(chunk).is_err() {
217
216
  return Err(interrupted());
@@ -245,18 +244,18 @@ impl Sink for ChannelSink {
245
244
  }
246
245
  }
247
246
 
248
- /// 1チャンクをRubyのブロックへ渡す。中断したら`false`を返す。
247
+ /// Hand one chunk to the Ruby block. Returns `false` on an interruption.
249
248
  ///
250
- /// GVLを取り戻すのはこの中だけ。ブロックの呼び出しはmagnusの`Proc::call`が
251
- /// 内部で`rb_protect`しているので、例外が出てもlongjmpがRustのフレームを
252
- /// 飛び越えない。
249
+ /// This is the only place the GVL is reacquired. The block call goes through magnus's
250
+ /// `Proc::call`, which wraps it in `rb_protect` internally, so an exception's longjmp cannot
251
+ /// jump over a Rust frame.
253
252
  ///
254
- /// エラーの保存もこの区間の中で済ませる。`with_gvl`からRubyのオブジェクト
255
- /// (例外)を持ち出すと、GVLを手放した瞬間にGCのスコープから外れてしまう
256
- /// ため(`gvl::with_gvl`のドキュメント)。持ち出すのは真偽値だけ。
253
+ /// Storing the error also happens inside this region. Carrying a Ruby object (an exception)
254
+ /// out of `with_gvl` would put it outside the GC's scope the moment the GVL is released
255
+ /// (see the documentation of `gvl::with_gvl`). Only a boolean is carried out.
257
256
  fn call_block(block: BlockSlot, pending: &mut PendingUnwind, bytes: Vec<u8>) -> bool {
258
257
  gvl::with_gvl(move || {
259
- let ruby = Ruby::get().expect("with_gvlの内側なのでGVLを持っている");
258
+ let ruby = Ruby::get().expect("inside with_gvl, so the GVL is held");
260
259
  let result = match block.proc() {
261
260
  Some(proc) => {
262
261
  let chunk: RString = ruby.str_from_slice(&bytes);
@@ -264,13 +263,13 @@ fn call_block(block: BlockSlot, pending: &mut PendingUnwind, bytes: Vec<u8>) ->
264
263
  }
265
264
  None => Err(Error::new(
266
265
  ruby.exception_runtime_error(),
267
- "ブロックが失われました",
266
+ "the block was lost",
268
267
  )),
269
268
  };
270
269
  match result {
271
270
  Ok(()) => true,
272
271
  Err(error) => {
273
- // GCへの登録もGVLを持っているこの場で行う。
272
+ // Registering with the GC also happens here, while the GVL is held.
274
273
  pending.store(error);
275
274
  false
276
275
  }
@@ -278,25 +277,25 @@ fn call_block(block: BlockSlot, pending: &mut PendingUnwind, bytes: Vec<u8>) ->
278
277
  })
279
278
  }
280
279
 
281
- /// `render`をレンダリング専用スレッドで走らせ、出てきたチャンクをこのスレッド
282
- /// からRubyのブロックへ渡し続ける。
280
+ /// Run `render` on a dedicated rendering thread and keep handing the chunks it produces to
281
+ /// the Ruby block from this thread.
283
282
  ///
284
- /// GVLを解放している区間(`without_gvl`の内側)から、その解放したスレッド上で
285
- /// 呼ぶこと。モジュールdocの図のうち左側がこの関数にあたる。
283
+ /// Call it from inside a GVL-released region (inside `without_gvl`), on the thread that
284
+ /// released it. It is the left-hand side of the diagram in the module docs.
286
285
  pub fn pump_to_block<F>(
287
286
  block: BlockSlot,
288
287
  pending: &mut PendingUnwind,
289
288
  chunk_size: usize,
290
289
  render: F,
291
- ) -> Result<(), sghtmltopdf_core::cli::CliError>
290
+ ) -> Result<(), sghtmltopdf_core::ConvertError>
292
291
  where
293
- F: FnOnce(ChannelSink) -> Result<(), sghtmltopdf_core::cli::CliError> + Send + 'static,
292
+ F: FnOnce(ChannelSink) -> Result<(), sghtmltopdf_core::ConvertError> + Send + 'static,
294
293
  {
295
- use sghtmltopdf_core::cli::CliError;
296
- use sghtmltopdf_core::render_stack::STACK_SIZE;
294
+ use sghtmltopdf_core::ConvertError;
295
+ use sghtmltopdf_core::STACK_SIZE;
297
296
 
298
- // どちらも容量0のrendezvous。レンダリング側は1チャンクごとに
299
- // ブロックの完了を待つ。
297
+ // Both are zero-capacity rendezvous channels. The rendering side waits for the block to
298
+ // finish after every chunk.
300
299
  let (chunk_tx, chunk_rx) = std::sync::mpsc::sync_channel::<Vec<u8>>(0);
301
300
  let (ack_tx, ack_rx) = std::sync::mpsc::sync_channel::<bool>(0);
302
301
 
@@ -304,17 +303,17 @@ where
304
303
  .name("sghtmltopdf-render".to_string())
305
304
  .stack_size(STACK_SIZE)
306
305
  .spawn(move || render(ChannelSink::new(chunk_tx, ack_rx, chunk_size)))
307
- .map_err(|e| CliError::Input(format!("レンダリングスレッドを作れません: {e}")))?;
306
+ .map_err(|e| ConvertError::Input(format!("cannot create the rendering thread: {e}")))?;
308
307
 
309
308
  while let Ok(chunk) = chunk_rx.recv() {
310
309
  let ok = call_block(block, pending, chunk);
311
- // 応答を返せない(レンダリング側が既に降りた)場合も抜ける。
310
+ // Also break out when the acknowledgement cannot be sent (the rendering side already went away).
312
311
  if ack_tx.send(ok).is_err() || !ok {
313
312
  break;
314
313
  }
315
314
  }
316
- // 中断で抜けた場合、レンダリング側が次のsendでエラーになって巻き戻れる
317
- // よう、受け口を先に落とす。
315
+ // On an interruption, drop the receiving end first so the rendering side errors on its
316
+ // next send and can unwind.
318
317
  drop(chunk_rx);
319
318
  drop(ack_tx);
320
319
 
@@ -1,9 +1,9 @@
1
- //! Rubyの例外クラスと、コアの[`CliError`]からの対応付け。
1
+ //! The Ruby exception classes, and the mapping from the core's [`ConvertError`].
2
2
 
3
3
  use std::panic::AssertUnwindSafe;
4
4
 
5
5
  use magnus::{prelude::*, ExceptionClass, RModule, Ruby};
6
- use sghtmltopdf_core::cli::CliError;
6
+ use sghtmltopdf_core::ConvertError;
7
7
 
8
8
  pub fn define(ruby: &Ruby, module: RModule) -> Result<(), magnus::Error> {
9
9
  let base = module.define_error("Error", ruby.exception_standard_error())?;
@@ -15,60 +15,62 @@ pub fn define(ruby: &Ruby, module: RModule) -> Result<(), magnus::Error> {
15
15
  Ok(())
16
16
  }
17
17
 
18
- /// Rustのパニックを`Sghtmltopdf::InternalError`へ変換して`f`を実行する。
18
+ /// Run `f`, converting a Rust panic into `Sghtmltopdf::InternalError`.
19
19
  ///
20
- /// # なぜ自前で捕まえるか
20
+ /// # Why we catch it ourselves
21
21
  ///
22
- /// magnusもメソッド呼び出しをパニックから守っており、プロセスがabortする
23
- /// ことはない。ただしmagnusが変換する先はRubyの`fatal`で、これは
24
- /// `rescue Exception`でも捕まえられずプロセスが終了する。Webアプリの中で
25
- /// 1リクエストぶんのバグのためにワーカーごと落ちるのは困るので、
26
- /// magnusへ渡る前にここで`StandardError`の子孫へ変換する。
22
+ /// magnus also guards method calls against panics, so the process does not abort. But what
23
+ /// magnus converts to is Ruby's `fatal`, which not even `rescue Exception` catches and which
24
+ /// terminates the process. Losing a whole worker inside a web application over a bug in one
25
+ /// request is unacceptable, so it is converted to a descendant of `StandardError` here,
26
+ /// before it reaches magnus.
27
27
  ///
28
- /// パニックはコアの不具合を意味するので、握りつぶさずメッセージを残す。
28
+ /// A panic means a bug in the core, so the message is kept rather than swallowed.
29
29
  pub fn catch_panic<F, R>(ruby: &Ruby, f: F) -> Result<R, magnus::Error>
30
30
  where
31
31
  F: FnOnce() -> Result<R, magnus::Error>,
32
32
  {
33
- // AssertUnwindSafe: パニックで巻き戻った後に触るのはRuby側の例外生成だけで、
34
- // Rust側の壊れかけた状態を読み直すことはない。
33
+ // AssertUnwindSafe: after unwinding from a panic, all that is touched is building the
34
+ // Ruby-side exception; no half-broken Rust state is read back.
35
35
  match std::panic::catch_unwind(AssertUnwindSafe(f)) {
36
36
  Ok(result) => result,
37
37
  Err(payload) => Err(magnus::Error::new(
38
38
  class(ruby, "InternalError"),
39
- format!("内部エラー(パニック): {}", panic_message(&payload)),
39
+ format!("internal error (panic): {}", panic_message(&payload)),
40
40
  )),
41
41
  }
42
42
  }
43
43
 
44
- /// パニックのペイロードから人が読めるメッセージを取り出す。
44
+ /// Extract a human-readable message from a panic payload.
45
45
  fn panic_message(payload: &Box<dyn std::any::Any + Send>) -> String {
46
46
  if let Some(message) = payload.downcast_ref::<&'static str>() {
47
47
  (*message).to_string()
48
48
  } else if let Some(message) = payload.downcast_ref::<String>() {
49
49
  message.clone()
50
50
  } else {
51
- "詳細不明".to_string()
51
+ "no details".to_string()
52
52
  }
53
53
  }
54
54
 
55
- /// コアのエラーを、対応するRubyの例外へ変換する。
55
+ /// Convert a core error into the corresponding Ruby exception.
56
56
  ///
57
- /// メッセージはコアが返す文言をそのまま使う(CLIと同じ文言になる)。
58
- pub fn to_ruby(ruby: &Ruby, error: CliError) -> magnus::Error {
57
+ /// The message is the wording the core returns, verbatim (the same wording as the CLI).
58
+ pub fn to_ruby(ruby: &Ruby, error: ConvertError) -> magnus::Error {
59
59
  let (class_name, message) = match error {
60
- CliError::Usage(message) => ("UsageError", message),
61
- CliError::Input(message) => ("InputError", message),
62
- CliError::Render(message) => ("RenderError", message),
63
- CliError::Timeout(message) => ("TimeoutError", message),
60
+ ConvertError::Usage(message) => ("UsageError", message),
61
+ ConvertError::Input(message) => ("InputError", message),
62
+ ConvertError::Render(message) => ("RenderError", message),
63
+ ConvertError::Timeout(message) => ("TimeoutError", message),
64
+ // A classification added in a later core release. Surface it rather than hide it.
65
+ other => ("Error", other.to_string()),
64
66
  };
65
67
  magnus::Error::new(class(ruby, class_name), message)
66
68
  }
67
69
 
68
- /// `Sghtmltopdf::<name>`の例外クラスを引く。
70
+ /// Look up the `Sghtmltopdf::<name>` exception class.
69
71
  ///
70
- /// 定義は`.so`のロード時に済んでいる([`define`])。万一引けなかった場合は
71
- /// エラーを握りつぶさずに`RuntimeError`として上げる。
72
+ /// It is defined when the `.so` is loaded ([`define`]). On the off chance the lookup fails,
73
+ /// the error is raised as a `RuntimeError` rather than swallowed.
72
74
  pub fn class(ruby: &Ruby, name: &str) -> ExceptionClass {
73
75
  ruby.class_object()
74
76
  .const_get::<_, RModule>("Sghtmltopdf")