外部からの信頼できない入力がある場合ctRegexを使うのはやめよう
俺も気がついてなかった。。
D言語はコンパイル時に正規表現オブジェクトを構築する ctRegex という関数があるのだが、これはバックトラック法の実装しか選択できない。そのため、マッチングの際に入力される文字数に対して線形時間である計算量保証が成り立たない。
普段実行時の regex を使う場合は後方参照(\1みたいな表現)が入力文字列の中にあるかどうかでThompshon NFAを使うかバックトラック法を使うかを自動判別して適用されるが、コンパイル時生成の場合は上で述べたようにルールが変わっている。
バックトラック法だとReDoSのような攻撃手法が成り立ってしまうので、タイトルにあるように外部からの信頼できない入力がある場合は使うのを避けるべきである。
GPUで描画するモダンなフォントレンダラを自作したはなし
まえおき
ちょっと前の話なんですが、fadisさんの低レイヤーからはじめるGUIという発表をみてからぼんやりGPUでフォント描画するやつやりてえなーと思っていました。かっこいいので。
でまあちょいちょい調べてたりしてたんですが、けっこういろんな手法があるんですよね。
- SDF: グリフアウトラインからの距離をテクスチャにのせてfragment shaderで計算する。
- MSDF: SDFの改良的な。RGB3チャンネル(Mutli-channel)を使ってSDFを符号化。
- ジオメトリメッシュ: 上の発表にあった、事前に三角形分割しておいて頂点データとしてGPUに送る。
- Loop-Blinn: fragment shaderでベジエ曲線の内外判定を行う。
- Slug: ベジエ曲線をバンド構造(グリフを水平・垂直の両方向にバンド分割し、各バンドにそのバンドを横断するベジエ曲線の参照リストを持たせる)で整理し、GPUフラグメントシェーダが各ピクセルで曲線とのワインディングを直接計算してサブピクセル精度のカバレッジを求める。
- Vello: 手法の名前ではなく実装。Compute Shaderでベジェ曲線のラスタライズをやる。
まあ他にもあるんだと思います。
余談ですが、こういうアルゴリズムがいろいろある中で(SDFとかは2007年の発表)、もちろんゲーム業界なんかでは使われているんでしょうが、あまり一般のソフトウェアで使われていなかったりする印象があります。 GhosttyがGPU使っててすごいみたいな発表もみましたが、ラスタライズそのものはCPUベース(FreeTypeかな)で、複数のグリフをShelf packingしてひとつのテクスチャにしたものをGPUに転送してそれをdrawしているだけ、みたいな感じでした。 使っているコンポーネントの違いはあれどwezTermとか、もっというとSkia(Chrome)やWebRender(Firefox)なんかも同じ仕組みですね。
Slugアルゴリズム
今回採用したのはSlugアルゴリズムです。
というか実装の直接のきっかけになったのはHarfBuzzの作者が自作レンダラをSlugに置き換えたという話を知ったからなんですね。 それで芋づる式にSlugがほんのつい最近Public Domainになったなどを知って、タイミングいいやん!となったかんじです。 SlugはHLSLの参照実装もあるので機械的なGLSLポートも容易だった、というのもあります。まあ置き換えはAIにやらせましたが。。
Slugいいなと思っているのは(ほかのGPUベースのフォント描画アルゴリズムにもいえることかもしれませんが)ピクセルとベジェ曲線のワインディング・内外判定を行う際に実質的にアンチエイリアスのような処理が入っていることですね。
リファレンス実装とちょっと変えている点としては、テクスチャにのっけるのではなくVulkanのStorage Bufferを利用している点です。このほうが実装シンプルになるのでは?と思いつき、実際にちゃんと動いているっぽいのでまあたぶん問題ないでしょう。
HarfBuzz
当初はhairetsuという純D言語製のフォントパース・テキストシェーピング用ライブラリを使うつもりだったんですが、まだまだ成熟しておらずたとえばCFFアウトラインを取得できないことやそもそも最新のコンパイラでビルドが通らない(これについてはおそらくコンパイラのバグ)といった諸問題があり、最終的には定番のHarfBuzzを利用することにしました。
HarfBuzzはかなり使いやすいのでいいですね。テキストシェーピングというとグリフの配置だったりカーニングだったりまあいろいろなことをやる必要があるのですが、単一のAPI呼び出しの裏にすべて隠蔽されていて細かいところを意識する必要がなかったです。 また、これはあくまでミニマムなtoy実装という面もあるんですが、FreeTypeに依存する必要がなかったのもよかったです。
おわりに
あくまでtoy実装ですが学ぶことは多かったですね。 めんどうなコーディングはClaudeクンにやってもらう部分も多かったですが、こういう実装になるには細かい指示を出し続ける必要があったなというのも学びではありました。
あとぜんぜん関係ないですが、今フォント関連はRust化の波がアツいんですよね。 GoogleがフォントのコンポネントのRust化を推しまくっていたり(てかChromeはフォントパースはもうRustにしてますね)、HarfBuzzのRustポートが段階的に進んでいたり(C++を置き換えるところまでいくのは相当かかりそうですが)。
気が向いたらこのへんもまとめてみたいと思います。それでは。
RLP encode 実装メモ
d-eth/dethのrlpEncodeの実装がだいぶ厳しい。
理由としては主に
- 引数にbytes(ubyteのtype alias)しかとれない
- 主要な型をrlpのbytes表現にシリアライズしてからrlpEncodeしないといけない
- パフォーマンスに気を使っていない実装になっている
- .dupなどコピーしまくり
- 数値のコンパクト表現を実装する際も毎回メモリを確保
なのでこのあたりを置き換えるようなものを作ろうとしている。まだencodeしか実装していないがいちおうdecodeも作る予定。
- 型ごとにencodeとencodeLengthを実装
- わざわざシリアライズしてからencode処理を呼び出す必要がない
- 内部的な実装ではバッファを外から渡す方式にしてそこにencodeしたものを追加していく
- 数値のコンパクト表現の実装の細かい最適化
- llvm_ctlz intrinsicの利用/ヒープ領域の割り当てを避けるなど
といったあたりを意識しながら作っている。
go-ethereumのrlp実装などもみたが、主に参考にしているのはalloy-rs/rlpでAPIなども似せている。
バイト列の扱い
ubyteを渡す際にRLP表現においてこれがたんなるバイト列なのかubyte型数値のリストなのかを区別する必要がある。 どういう方式でencodeしたいかはユーザの意思しだいであるが、ubyte型のみでは区別ができないので多少工夫がいる。
kubo39/rlpではubyteをencodeする場合のみテンプレート引数としてasListを渡す方式にした。
void encode(bool asList = false, T)(T[] value, ref ubyte[] buffer) nothrow pure @safe if (is(Unqual!T == ubyte)) { static if (asList) { rlpListHeader(value).encodeHeader(buffer); foreach (elem; value) elem.encode(buffer); } else { if (value.length != 1 || value[0] >= rlp.EMPTY_STRING_CODE) { Header h = { isList: false, payloadLength: value.length }; h.encodeHeader(buffer); } buffer ~= value; } }
Alpine Linuxで小さなD言語の静的バイナリを作りたい
こちらはD言語 Advent Calendar 2025 シリーズ2の16日目の記事として書いてます。
ちょうど https://github.com/mattn/small-build-test というプロジェクトをみかけて。
docker imageで生成物をデプロイ・配布することは今ではもう当たり前のことになってきており、とりわけ本番環境にimageを頻繁にデプロイするような使い方をしているとストレージ・通信のコストの面からimageのサイズは極力小さくしておきたいという要求がある。
D言語はバイナリを生成するコンパイラを提供している言語でありPythonのようにランタイムを同梱して配布する必要がないので、こういったコスト観点では有利かもしれない。せっかくなのでどのくらいまでいけるかみてみよう。
レギュレーション用?のコードはなんの変哲もないHello, World!。 ただし標準ライブラリstd.stdioを使っていることに意味がある。
import std.stdio; void main() { writeln("Hello, World!"); }
最終的にはこのようなDockerfileになった。
# syntax=docker/dockerfile:1 FROM alpine:latest AS builder RUN apk add --no-cache ldc gcc musl-dev WORKDIR /app COPY main.d . RUN ldc2 -Oz --release --boundscheck=off --platformlib= --flto=full --static -L-Wl,--strip-all -of=/app/hello main.d FROM scratch COPY --from=builder /app/hello /hello CMD [ "/hello" ]
まずはちゃんと動くことを確認する。
$ docker build -t hello-d . -q sha256:b563982aa847a4888c56220722093228b1bb706de92218a9ef111f548a50d46c $ docker run --rm hello-d:latest Hello, World!
imageの大きさをみてみると、ただのHello, World!にしてはそこそこ大きい(もちろんPythonなんかに比べると格段に小さくできるのだが)
$ docker image ls --format=table hello-d REPOSITORY TAG IMAGE ID CREATED SIZE hello-d latest b563982aa847 2 days ago 4.75MB
内訳をみたい。
docker create --name tmp hello-d docker cp tmp:/hello . docker rm tmp
静的なバイナリにはなっている。
$ file hello
hello: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), static-pie linked, BuildID[sha1]=eb5b088c42d206c904c7729645709e99d03b2d15, stripped
$ ldd hello
statically linked
size -Aでみた結果。
$ size -A hello hello : section size addr .note.gnu.build-id 36 680 .dynsym 328104 720 .gnu.hash 101132 328824 .dynstr 1181543 429956 .rela.dyn 224736 1611504 .gcc_except_table 8352 1836240 .rodata 460604 1844608 .eh_frame_hdr 89188 2305212 .eh_frame 405044 2394400 .text 1778941 2803584 .init 3 4582525 .fini 3 4582528 .tdata 2800 4586640 .tbss 888 4589440 .fini_array 24 4589440 .init_array 64 4589464 .data.rel.ro 58728 4589536 .dynamic 304 4648264 .got 144 4648568 .got.plt 24 4648712 .relro_padding 224 4648736 .tm_clone_table 0 4652832 .data 108824 4652832 __minfo 1224 4761656 .bss 7824 4762880 .comment 95 0 Total 4758853
.dynstrとかめっちゃでかい。libdruntimeとかlibphobos2をpieでビルドするとシンボル名の情報がここにのっかってくるためだ。stripだとここは消せない。
以下の出力をみると雰囲気が掴めるだろうか。
$ readelf -p .dynstr hello | grep _D3std | head -10 | ddemangle [ 41] @safe void std.stdio.writeln!(immutable(char)[]).writeln(immutable(char)[]) [ 27c] @safe noreturn std.exception.bailOut!(std.exception.ErrnoException).bailOut(immutable(char)[], ulong, scope const(char)[]) [ 2c3] @safe void std.stdio.File.LockingTextWriter.put!(immutable(char)[]).put(scope immutable(char)[]) [ 2ff] @safe void std.stdio.File.LockingTextWriter.put!(char).put(scope char) [ 338] nothrow @nogc @trusted ulong std.stdio.trustedFwrite!(char).trustedFwrite(shared(core.stdc.stdio._IO_FILE)*, const(char[])) [ 381] @safe int std.exception.enforce!(std.exception.ErrnoException).enforce!(int).enforce(int, lazy const(char)[], immutable(char)[], ulong) [ 3d1] @safe void std.stdio.File.LockingTextWriter.put!(immutable(char)).put(scope immutable(char)) [ 40c] pure @safe uint std.utf.stride!(char[]).stride(char[]) [ 430] pure @safe dchar std.utf.decodeFront!(0, char[]).decodeFront(ref char[]) [ 4a6] pure @safe ulong std.utf.encode!(0).encode(out dchar[1], dchar)
.dynsym も然り。
$ readelf --demangle=dlang --dyn-sym -W hello | head -10 Symbol table '.dynsym' contains 13671 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 00000000002ebc90 97 FUNC WEAK DEFAULT 10 std.encoding.EncoderInstance!(dchar).__mixin13.encode(dchar, ref dchar[])
2: 0000000000365320 24 FUNC WEAK DEFAULT 10 std.range.SortedRange!(immutable(char)[][], "a < b", 0).SortedRange.__xtoHash(ref const(std.range.SortedRange!(immutable(char)[][], "a < b", 0).SortedRange))
3: 000000000043b150 18 FUNC WEAK DEFAULT 10 rt.util.typeinfo.TypeInfoArrayGeneric!(long, ulong).TypeInfoArrayGeneric.toString() const
4: 0000000000472b30 24 OBJECT WEAK DEFAULT 23 initializer for TypeInfo_APyS3std8datetime8timezone13PosixTimeZone6TTInfo
5: 000000000037f760 39 FUNC WEAK DEFAULT 10 std.typecons.SafeRefCounted!(std.file.DirIteratorImpl, 0).SafeRefCounted.RefCountedStore.deallocateStore()
6: 000000000038da10 159 FUNC WEAK DEFAULT 10 std.uni.PackedArrayViewImpl!(std.uni.BitPacked!(uint, 13uL).BitPacked, 16uL).PackedArrayViewImpl.__xopEquals(ref const(std.uni.PackedArrayViewImpl!(std.uni.BitPacked!(uint, 13uL).BitPacked, 16uL).PackedArrayViewImpl)) const
そのほかで言うと
- eh_frame: druntime/phobosが例外を使ってる部分で切り離せない
- .text,.rodata: テンプレート展開でコードサイズが大きくなっている/TypeInfoがけっこう大きい/モジュールコンストラクタ(
static this()/static shared this())があるモジュールは消せないのでコード上使ってなくてもbundleされている
といったあたりで、apkで提供されているldcのstatic libraryを使っているとこれ以上小さくするのはなかなか難しいということがわかった。
前日のD言語Advent Calenderの記事にしたldc-build-runtimeを使って自前で標準ライブラリをビルドするアプローチをここでもあわせて紹介しておくが、まあ実用上ここまでやる必要はほとんどないだろう。
(追記: 2025-12-18)
relocationとかもうちょっとなんとなからんかなーと調べてみたところ、relative relocationを圧縮するRELRフォーマットというものがあり、実際にこれを試してみると小さくなった。
diff --git a/d/Dockerfile b/d/Dockerfile index a01e6e7..4a8c551 100644 --- a/d/Dockerfile +++ b/d/Dockerfile @@ -3,7 +3,7 @@ FROM alpine:latest AS builder RUN apk add --no-cache ldc gcc musl-dev WORKDIR /app COPY main.d . -RUN ldc2 -Oz --release --boundscheck=off --platformlib= --flto=full --static -L-Wl,--strip-all -of=/app/hello main.d +RUN ldc2 -Oz --release --boundscheck=off --platformlib= --flto=full --static -L-Wl,-z,pack-relative-relocs -L-Wl,--strip-all -of=/app/hello main.d FROM scratch COPY --from=builder /app/hello /hello
上の結果と比較すると rela.dyn セクションが relr.dyn セクションに名前が変わっており、(バイナリ全体からみると微々たるものだが)セクションのサイズも大幅に削減されている。
$ size -A hello section size addr .note.gnu.build-id 36 680 .dynsym 328104 720 .gnu.hash 101132 328824 .dynstr 1181543 429956 .relr.dyn 2672 1611504 .gcc_except_table 8352 1614176 .rodata 460604 1622528 .eh_frame_hdr 89188 2083132 .eh_frame 405044 2172320 .text 1778941 2581504 .init 3 4360445 .fini 3 4360448 .tdata 2800 4364560 .tbss 888 4367360 .fini_array 24 4367360 .init_array 64 4367384 .data.rel.ro 58728 4367456 .dynamic 288 4426184 .got 144 4426472 .got.plt 24 4426616 .relro_padding 1136 4426640 .tm_clone_table 0 4430736 .data 108824 4430752 __minfo 1224 4539576 .bss 7824 4540800 .comment 95 0 Total 4537685
D言語製VulkanバインディングのEruptedを利用していてハマった話
この記事はグラフィックス全般 Advent Calendar 2025の3日目の記事です。
Vulkan向けで以下のようなVK_KHR_dynamic_rendering拡張を使ってdynamic renderingを行うコードを書いていたところ
auto imageView = swapchain.getCurrentView(); auto extent = swapchain.getExtent(); // 補足: EruptedはsTypeをコード生成時によしなに設定してくれる VkRenderingAttachmentInfoKHR colorAttachment = { imageView: imageView, imageLayout: VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, loadOp: VK_ATTACHMENT_LOAD_OP_CLEAR, storeOp: VK_ATTACHMENT_STORE_OP_STORE, clearValue: VkClearValue( VkClearColorValue([0.6f, 0.2f, 0.3f, 1.0f]) ), }; VkRenderingInfoKHR renderingInfo = { renderArea: VkRect2D(VkOffset2D(0, 0), extent), layerCount: 1, colorAttachmentCount: 1, pColorAttachments: &colorAttachment }; vkCmdBeginRenderingKHR(commandBuffer.get(), &renderingInfo); vkCmdEndRenderingKHR(commandBuffer.get());
とある環境で実行してみるとcode -1073741819(Windows: Access Violation)のエラーになった。
$ dub run -q Error Program exited with code -1073741819
vkCmdBeginRenderingKHRは0x0になっているために当然このような落ち方をするのだが、動的ロードを行っているとはいえなぜこのようになってしまうのかなかなか思い当たらない。
vulkaninfoでも拡張をサポートしているという情報は出ているし、loadDeviceLevelFunctionsの呼び忘れやVK_KHR_DYNAMIC_RENDERING_EXTENSION_NAMEの指定忘れをしたわけでもない。
調べてみたところ、原因は利用しているVulkanバインディングの実装の問題であることがわかった。
D言語製のVulkanバインディングであるEruptedはVulkan1.3でコアに追加された拡張に対してコアの関数へのaliasにしてしまっている。
// VK_KHR_dynamic_rendering alias CmdBeginRenderingKHR = CmdBeginRendering; alias CmdEndRenderingKHR = CmdEndRendering;
型定義などはaliasにしても(ほぼほぼ)問題ないのだが、Vulkan1.3機能に対応していないドライバではVulkan1.3の機能を使わないようにして拡張を利用しようとしても暗黙にVulkan1.3の関数を参照しようとしてAccess Violationが起きてしまっていたわけだ。
原因がわかれば解決策はそれほど難しい話ではない。 自前で関数アドレスをロードしてしまえばよいだけである。
pfnCmdBeginRenderingKHR = cast(PFN_vkCmdBeginRenderingKHR) vkGetDeviceProcAddr(m_vkDevice, "vkCmdBeginRenderingKHR");
ひとまずの応急処置としてはこれでよいのだが、これはupstream側で対処すべき問題のようにみえる。
Erupted側で対応する場合を考えると、Eruptedがvk.ymlを読み込んで自動生成する処理の関数生成のエイリアスの分岐内での処理あたりを変更する必要がありそうだ。 ちなみにErputedが依存・利用しているPythonパッケージはVulkan-Headersで提供されているもので、コード生成の元となるvk.ymlもこのレポジトリに含まれる(そもそもVulkan-Headersはこれを元にCのヘッダファイルを生成したものを提供するためのもの)。 ここで提供されているgenerator.py(vk.ymlからソースを生成するためのgeneratorコード)を継承・利用する形でD言語のVulkanバインディングが生成されている。
Erupted自体がそれほどメンテナンスされていないこともあり結局ここまで踏み込んで対応すべきかどうかは今の時点でも決め切れていないが、ひとまずはこういった問題があることは認識できた。
swap/swapRangesのはなし
ふと、swapみたいな単純なコードでも標準ライブラリの実装を呼び出した方がいいよ、という例として
import core.int128 : Cent; import std.algorithm.mutation : swap; struct Huge { Cent[128] data; alias this = data; } void f(ref Huge x, ref Huge y) { swap(x, y); }
みたいなコードを使おうと思ったんだけど、ぜんぜんそんなことはなかった。
$ ldc2 -O -c swap.d
$ objdump -dr --demangle=dlang -Mintel swap.o
(...)
0000000000000000 <swap.f(ref swap.Huge, ref swap.Huge)>:
0: 41 57 push r15
2: 41 56 push r14
4: 53 push rbx
5: 48 81 ec 00 08 00 00 sub rsp,0x800
c: 48 89 f3 mov rbx,rsi
f: 49 89 fe mov r14,rdi
12: 49 89 e7 mov r15,rsp
15: ba 00 08 00 00 mov edx,0x800
1a: 4c 89 ff mov rdi,r15
1d: 4c 89 f6 mov rsi,r14
20: e8 00 00 00 00 call 25 <swap.f(ref swap.Huge, ref swap.Huge)+0x25>
21: R_X86_64_PLT32 memcpy-0x4
25: ba 00 08 00 00 mov edx,0x800
2a: 4c 89 f7 mov rdi,r14
2d: 48 89 de mov rsi,rbx
30: e8 00 00 00 00 call 35 <swap.f(ref swap.Huge, ref swap.Huge)+0x35>
31: R_X86_64_PLT32 memcpy-0x4
35: ba 00 08 00 00 mov edx,0x800
3a: 48 89 df mov rdi,rbx
3d: 4c 89 fe mov rsi,r15
40: e8 00 00 00 00 call 45 <swap.f(ref swap.Huge, ref swap.Huge)+0x45>
41: R_X86_64_PLT32 memcpy-0x4
45: 48 81 c4 00 08 00 00 add rsp,0x800
4c: 5b pop rbx
4d: 41 5e pop r14
4f: 41 5f pop r15
51: c3 ret
(...)
0000000000000000 <std.algorithm.mutation.swap!(swap.Huge).swap(ref swap.Huge, ref swap.Huge)>:
0: 41 57 push r15
2: 41 56 push r14
4: 53 push rbx
5: 48 81 ec 00 08 00 00 sub rsp,0x800
c: 48 89 f3 mov rbx,rsi
f: 49 89 fe mov r14,rdi
12: 49 89 e7 mov r15,rsp
15: ba 00 08 00 00 mov edx,0x800
1a: 4c 89 ff mov rdi,r15
1d: 4c 89 f6 mov rsi,r14
20: e8 00 00 00 00 call 25 <std.algorithm.mutation.swap!(swap.Huge).swap(ref swap.Huge, ref swap.Huge)+0x25>
21: R_X86_64_PLT32 memcpy-0x4
25: ba 00 08 00 00 mov edx,0x800
2a: 4c 89 f7 mov rdi,r14
2d: 48 89 de mov rsi,rbx
30: e8 00 00 00 00 call 35 <std.algorithm.mutation.swap!(swap.Huge).swap(ref swap.Huge, ref swap.Huge)+0x35>
31: R_X86_64_PLT32 memcpy-0x4
35: ba 00 08 00 00 mov edx,0x800
3a: 48 89 df mov rdi,rbx
3d: 4c 89 fe mov rsi,r15
40: e8 00 00 00 00 call 45 <std.algorithm.mutation.swap!(swap.Huge).swap(ref swap.Huge, ref swap.Huge)+0x45>
41: R_X86_64_PLT32 memcpy-0x4
45: 48 81 c4 00 08 00 00 add rsp,0x800
4c: 5b pop rbx
4d: 41 5e pop r14
4f: 41 5f pop r15
51: c3 ret
実装みてもそんな感じしないけど
void swap(T)(ref T lhs, ref T rhs) if (is(typeof(lhs.proxySwap(rhs)))) { lhs.proxySwap(rhs); } /// ditto void swap(T)(ref T lhs, ref T rhs) @trusted pure nothrow @nogc if (isBlitAssignable!T && !is(typeof(lhs.proxySwap(rhs)))) { import std.traits : hasAliasing, hasElaborateAssign, isAssignable, isStaticArray; static if (hasAliasing!T) if (!__ctfe) { import std.exception : doesPointTo; assert(!doesPointTo(lhs, lhs), "Swap: lhs internal pointer."); assert(!doesPointTo(rhs, rhs), "Swap: rhs internal pointer."); assert(!doesPointTo(lhs, rhs), "Swap: lhs points to rhs."); assert(!doesPointTo(rhs, lhs), "Swap: rhs points to lhs."); } static if (hasElaborateAssign!T || !isAssignable!T) { if (&lhs != &rhs) { static if (__traits(compiles, lhs.tupleof = rhs.tupleof)) { if (__ctfe) { // can't reinterpret cast foreach (i, ref e; lhs.tupleof) swap(e, rhs.tupleof[i]); return; } } // For structs with non-trivial assignment, move memory directly ubyte[T.sizeof] t = void; auto a = (cast(ubyte*) &lhs)[0 .. T.sizeof]; auto b = (cast(ubyte*) &rhs)[0 .. T.sizeof]; t[] = a[]; a[] = b[]; b[] = t[]; } } else { //Avoid assigning overlapping arrays. Dynamic arrays are fine, because //it's their ptr and length properties which get assigned rather //than their elements when assigning them, but static arrays are value //types and therefore all of their elements get copied as part of //assigning them, which would be assigning overlapping arrays if lhs //and rhs were the same array. static if (isStaticArray!T) { if (lhs.ptr == rhs.ptr) return; } // For non-elaborate-assign types, suffice to do the classic swap static if (__traits(hasCopyConstructor, T)) { // don't invoke any elaborate constructors either T tmp = void; tmp = lhs; } else auto tmp = lhs; lhs = rhs; rhs = tmp; } }
なんかうまく最適化してくれそうだからここにきてほしいんだよな。
// For structs with non-trivial assignment, move memory directly ubyte[T.sizeof] t = void; auto a = (cast(ubyte*) &lhs)[0 .. T.sizeof]; auto b = (cast(ubyte*) &rhs)[0 .. T.sizeof]; t[] = a[]; a[] = b[]; b[] = t[];
hasElaborateAssign!T || !isAssignable!Tを満たしてないのか。
import core.int128 : Cent; import std.algorithm.mutation : swap; import std.traits : isAssignable, hasElaborateAssign; struct Huge { Cent[128] data; alias this = data; } void f(ref Huge x, ref Huge y) { pragma(msg, hasElaborateAssign!Huge); pragma(msg, !isAssignable!Huge); swap(x, y); }
だめだった。
$ ldc2 -O -c swap.d false false
disable this(this)を足そう。
import core.int128 : Cent; import std.algorithm.mutation : swap; import std.traits : isAssignable, hasElaborateAssign; struct Huge { @disable this(this); Cent[128] data; alias this = data; } void f(ref Huge x, ref Huge y) { pragma(msg, hasElaborateAssign!Huge); pragma(msg, !isAssignable!Huge); swap(x, y); }
$ ldc2 -O -c swap.d true true
これでいけるかな。
$ objdump -dr --demangle=dlang -Mintel swap.o
(...)
0000000000000000 <swap.f(ref swap.Huge, ref swap.Huge)>:
0: 48 39 f7 cmp rdi,rsi
3: 74 5c je 61 <swap.f(ref swap.Huge, ref swap.Huge)+0x61>
5: 41 57 push r15
7: 41 56 push r14
9: 53 push rbx
a: 48 81 ec 00 08 00 00 sub rsp,0x800
11: 48 89 f3 mov rbx,rsi
14: 49 89 fe mov r14,rdi
17: 49 89 e7 mov r15,rsp
1a: ba 00 08 00 00 mov edx,0x800
1f: 4c 89 ff mov rdi,r15
22: 4c 89 f6 mov rsi,r14
25: e8 00 00 00 00 call 2a <swap.f(ref swap.Huge, ref swap.Huge)+0x2a>
26: R_X86_64_PLT32 memcpy-0x4
2a: be 00 08 00 00 mov esi,0x800
2f: b9 00 08 00 00 mov ecx,0x800
34: 41 b8 01 00 00 00 mov r8d,0x1
3a: 4c 89 f7 mov rdi,r14
3d: 48 89 da mov rdx,rbx
40: e8 00 00 00 00 call 45 <swap.f(ref swap.Huge, ref swap.Huge)+0x45>
41: R_X86_64_PLT32 _d_array_slice_copy-0x4
45: ba 00 08 00 00 mov edx,0x800
4a: 48 89 df mov rdi,rbx
4d: 4c 89 fe mov rsi,r15
50: e8 00 00 00 00 call 55 <swap.f(ref swap.Huge, ref swap.Huge)+0x55>
51: R_X86_64_PLT32 memcpy-0x4
55: 48 81 c4 00 08 00 00 add rsp,0x800
5c: 5b pop rbx
5d: 41 5e pop r14
5f: 41 5f pop r15
61: c3 ret
(...)
0000000000000000 <std.algorithm.mutation.swap!(swap.Huge).swap(ref swap.Huge, ref swap.Huge)>:
0: 48 39 f7 cmp rdi,rsi
3: 74 5c je 61 <std.algorithm.mutation.swap!(swap.Huge).swap(ref swap.Huge, ref swap.Huge)+0x61>
5: 41 57 push r15
7: 41 56 push r14
9: 53 push rbx
a: 48 81 ec 00 08 00 00 sub rsp,0x800
11: 48 89 f3 mov rbx,rsi
14: 49 89 fe mov r14,rdi
17: 49 89 e7 mov r15,rsp
1a: ba 00 08 00 00 mov edx,0x800
1f: 4c 89 ff mov rdi,r15
22: 4c 89 f6 mov rsi,r14
25: e8 00 00 00 00 call 2a <std.algorithm.mutation.swap!(swap.Huge).swap(ref swap.Huge, ref swap.Huge)+0x2a>
26: R_X86_64_PLT32 memcpy-0x4
2a: be 00 08 00 00 mov esi,0x800
2f: b9 00 08 00 00 mov ecx,0x800
34: 41 b8 01 00 00 00 mov r8d,0x1
3a: 4c 89 f7 mov rdi,r14
3d: 48 89 da mov rdx,rbx
40: e8 00 00 00 00 call 45 <std.algorithm.mutation.swap!(swap.Huge).swap(ref swap.Huge, ref swap.Huge)+0x45>
41: R_X86_64_PLT32 _d_array_slice_copy-0x4
45: ba 00 08 00 00 mov edx,0x800
4a: 48 89 df mov rdi,rbx
4d: 4c 89 fe mov rsi,r15
50: e8 00 00 00 00 call 55 <std.algorithm.mutation.swap!(swap.Huge).swap(ref swap.Huge, ref swap.Huge)+0x55>
51: R_X86_64_PLT32 memcpy-0x4
55: 48 81 c4 00 08 00 00 add rsp,0x800
5c: 5b pop rbx
5d: 41 5e pop r14
5f: 41 5f pop r15
61: c3 ret
(...)
0000000000000000 <std.algorithm.mutation.swap!(core.int128.Cent[128]).swap(ref core.int128.Cent[128], ref core.int128.Cent[128])>:
0: 48 39 f7 cmp rdi,rsi
3: 74 51 je 56 <std.algorithm.mutation.swap!(core.int128.Cent[128]).swap(ref core.int128.Cent[128], ref core.int128.Cent[128])+0x56>
5: 41 57 push r15
7: 41 56 push r14
9: 53 push rbx
a: 48 81 ec 00 08 00 00 sub rsp,0x800
11: 48 89 f3 mov rbx,rsi
14: 49 89 fe mov r14,rdi
17: 49 89 e7 mov r15,rsp
1a: ba 00 08 00 00 mov edx,0x800
1f: 4c 89 ff mov rdi,r15
22: 4c 89 f6 mov rsi,r14
25: e8 00 00 00 00 call 2a <std.algorithm.mutation.swap!(core.int128.Cent[128]).swap(ref core.int128.Cent[128], ref core.int128.Cent[128])+0x2a>
26: R_X86_64_PLT32 memcpy-0x4
2a: ba 00 08 00 00 mov edx,0x800
2f: 4c 89 f7 mov rdi,r14
32: 48 89 de mov rsi,rbx
35: e8 00 00 00 00 call 3a <std.algorithm.mutation.swap!(core.int128.Cent[128]).swap(ref core.int128.Cent[128], ref core.int128.Cent[128])+0x3a>
36: R_X86_64_PLT32 memcpy-0x4
3a: ba 00 08 00 00 mov edx,0x800
3f: 48 89 df mov rdi,rbx
42: 4c 89 fe mov rsi,r15
45: e8 00 00 00 00 call 4a <std.algorithm.mutation.swap!(core.int128.Cent[128]).swap(ref core.int128.Cent[128], ref core.int128.Cent[128])+0x4a>
46: R_X86_64_PLT32 memcpy-0x4
4a: 48 81 c4 00 08 00 00 add rsp,0x800
51: 5b pop rbx
52: 41 5e pop r14
54: 41 5f pop r15
56: c3 ret
ぜんぜんだめ。
(というか_d_array_slice_copyはmemcpyにloweringされてほしいのだがエイリアス情報がないのでだめか。@restrictをつけるとできそうだが。)
sliceのコピーがそもそもだめなのか。
export void myswap(ref Huge lhs, ref Huge rhs) { ubyte[Huge.sizeof] t = void; auto a = (cast(ubyte*) &lhs)[0 .. Huge.sizeof]; auto b = (cast(ubyte*) &rhs)[0 .. Huge.sizeof]; t[] = a[]; a[] = b[]; b[] = t[]; }
そうらしい。
$ ldc2 -O -c swap.d
$ objdump -dr --demangle=dlang -Mintel swap.o
(...)
0000000000000000 <swap.myswap(ref swap.Huge, ref swap.Huge)>:
0: 41 57 push r15
2: 41 56 push r14
4: 53 push rbx
5: 48 81 ec 00 08 00 00 sub rsp,0x800
c: 48 89 f3 mov rbx,rsi
f: 49 89 fe mov r14,rdi
12: 49 89 e7 mov r15,rsp
15: ba 00 08 00 00 mov edx,0x800
1a: 4c 89 ff mov rdi,r15
1d: 4c 89 f6 mov rsi,r14
20: e8 00 00 00 00 call 25 <swap.myswap(ref swap.Huge, ref swap.Huge)+0x25>
21: R_X86_64_PLT32 memcpy-0x4
25: be 00 08 00 00 mov esi,0x800
2a: b9 00 08 00 00 mov ecx,0x800
2f: 41 b8 01 00 00 00 mov r8d,0x1
35: 4c 89 f7 mov rdi,r14
38: 48 89 da mov rdx,rbx
3b: e8 00 00 00 00 call 40 <swap.myswap(ref swap.Huge, ref swap.Huge)+0x40>
3c: R_X86_64_PLT32 _d_array_slice_copy-0x4
40: ba 00 08 00 00 mov edx,0x800
45: 48 89 df mov rdi,rbx
48: 4c 89 fe mov rsi,r15
4b: e8 00 00 00 00 call 50 <swap.myswap(ref swap.Huge, ref swap.Huge)+0x50>
4c: R_X86_64_PLT32 memcpy-0x4
50: 48 81 c4 00 08 00 00 add rsp,0x800
57: 5b pop rbx
58: 41 5e pop r14
5a: 41 5f pop r15
5c: c3 ret
と思ってみてみたらswapRangesってのがあるのか。
import core.int128 : Cent; import std.algorithm.mutation : swapRanges; struct Huge { Cent[128] data; alias this = data; } void f(ref Huge x, ref Huge y) { swapRanges(x[], y[]); }
あ、これでいいですね。
0000000000000000 <swap.f(ref swap.Huge, ref swap.Huge)>: swap.f(ref swap.Huge, ref swap.Huge): 0: 31 c0 xor eax,eax 2: 66 66 66 66 66 2e 0f data16 data16 data16 data16 cs nop WORD PTR [rax+rax*1+0x0] 9: 1f 84 00 00 00 00 00 10: 0f 10 04 07 movups xmm0,XMMWORD PTR [rdi+rax*1] 14: 0f 29 44 24 e8 movaps XMMWORD PTR [rsp-0x18],xmm0 19: 0f 10 04 06 movups xmm0,XMMWORD PTR [rsi+rax*1] 1d: 0f 11 04 07 movups XMMWORD PTR [rdi+rax*1],xmm0 21: 0f 28 44 24 e8 movaps xmm0,XMMWORD PTR [rsp-0x18] 26: 0f 11 04 06 movups XMMWORD PTR [rsi+rax*1],xmm0 2a: 0f 10 44 07 10 movups xmm0,XMMWORD PTR [rdi+rax*1+0x10] 2f: 0f 29 44 24 e8 movaps XMMWORD PTR [rsp-0x18],xmm0 34: 0f 10 44 06 10 movups xmm0,XMMWORD PTR [rsi+rax*1+0x10] 39: 0f 11 44 07 10 movups XMMWORD PTR [rdi+rax*1+0x10],xmm0 3e: 0f 28 44 24 e8 movaps xmm0,XMMWORD PTR [rsp-0x18] 43: 0f 11 44 06 10 movups XMMWORD PTR [rsi+rax*1+0x10],xmm0 48: 48 83 c0 20 add rax,0x20 4c: 48 3d 00 08 00 00 cmp rax,0x800 52: 75 bc jne 10 <swap.f(ref swap.Huge, ref swap.Huge)+0x10> 54: c3 ret Disassembly of section .text._D3std9algorithm8mutation__T10swapRangesTAS4core6int1284CentTQuZQBkFNaNbNiNfQBjQBmZSQDe8typecons__T5TupleTQCnTQCrZQp: 0000000000000000 <std.algorithm.mutation.swapRanges!(core.int128.Cent[], core.int128.Cent[]).swapRanges(core.int128.Cent[], core.int128.Cent[])>: std.algorithm.mutation.swapRanges!(core.int128.Cent[], core.int128.Cent[]).swapRanges(core.int128.Cent[], core.int128.Cent[]): 0: 48 89 f8 mov rax,rdi 3: 48 85 f6 test rsi,rsi 6: 40 0f 94 c7 sete dil a: 48 85 c9 test rcx,rcx d: 41 0f 94 c1 sete r9b 11: 41 08 f9 or r9b,dil 14: 75 4d jne 63 <std.algorithm.mutation.swapRanges!(core.int128.Cent[], core.int128.Cent[]).swapRanges(core.int128.Cent[], core.int128.Cent[])+0x63> 16: 31 ff xor edi,edi 18: 0f 1f 84 00 00 00 00 nop DWORD PTR [rax+rax*1+0x0] 1f: 00 20: 0f 10 02 movups xmm0,XMMWORD PTR [rdx] 23: 0f 29 44 24 e8 movaps XMMWORD PTR [rsp-0x18],xmm0 28: 41 0f 10 00 movups xmm0,XMMWORD PTR [r8] 2c: 0f 11 02 movups XMMWORD PTR [rdx],xmm0 2f: 0f 28 44 24 e8 movaps xmm0,XMMWORD PTR [rsp-0x18] 34: 41 0f 11 00 movups XMMWORD PTR [r8],xmm0 38: 48 83 c2 10 add rdx,0x10 3c: 49 83 c0 10 add r8,0x10 40: 4c 8d 14 3e lea r10,[rsi+rdi*1] 44: 4c 8d 4f ff lea r9,[rdi-0x1] 48: 49 83 fa 01 cmp r10,0x1 4c: 74 0f je 5d <std.algorithm.mutation.swapRanges!(core.int128.Cent[], core.int128.Cent[]).swapRanges(core.int128.Cent[], core.int128.Cent[])+0x5d> 4e: 4c 8d 14 0f lea r10,[rdi+rcx*1] 52: 49 ff ca dec r10 55: 4c 89 cf mov rdi,r9 58: 4d 85 d2 test r10,r10 5b: 75 c3 jne 20 <std.algorithm.mutation.swapRanges!(core.int128.Cent[], core.int128.Cent[]).swapRanges(core.int128.Cent[], core.int128.Cent[])+0x20> 5d: 4c 01 c9 add rcx,r9 60: 4c 01 ce add rsi,r9 63: 48 89 30 mov QWORD PTR [rax],rsi 66: 48 89 50 08 mov QWORD PTR [rax+0x8],rdx 6a: 48 89 48 10 mov QWORD PTR [rax+0x10],rcx 6e: 4c 89 40 18 mov QWORD PTR [rax+0x18],r8 72: c3 ret
どうもLLVMは1バイトずつswapする処理をうまく最適化できるらしい。 swapRangesもこのような実装になっている。
Tuple!(InputRange1, InputRange2) swapRanges(InputRange1, InputRange2)(InputRange1 r1, InputRange2 r2) if (hasSwappableElements!InputRange1 && hasSwappableElements!InputRange2 && is(ElementType!InputRange1 == ElementType!InputRange2)) { for (; !r1.empty && !r2.empty; r1.popFront(), r2.popFront()) { swap(r1.front, r2.front); } return tuple(r1, r2); }
いままでのswapのはなしはなんだったの?なんだったんですかね。。
Conclusion
- 複数のelementがあるデータのswapがしたいときはswapRangesを使おう
Linuxサンドボックス環境の比較 2025年版
あらすじ
近年、supply-chain attackとかこわい。 特にCLIでnpmやらcodexやらclaude codeやらgeminiやら動かすのに裸んぼでは心もとないのではないか。
というわけでナウなLinuxサンドボックスを調べて比較してみる。
比較のやつ
firejail
老舗?のイメージ。 利用方法などはarch wikiがまとまっている。 わりと使いやすい、かつsuid+chroot+namespace+seccomp-bpfという必要なのがそろっている構成で、experimentalだがlandlockサポートも入っている。 じゃあfirejailでいいじゃん、という気もするのだが、そもそもfirejail使うでいいのか?というところが出発点でもある。
余談だがfirejailのコードはけっこう学びが多かった、このhackとかfirejailで知ったはず。
nsjail
officialなやつではないけどgoogleのやつ。 これもけっこう老舗感ある。 あまりfirejailと違わないような?出てきたときはnamespace対応だったり(ns-prefixはそこからきたんだったはず)があったかもしれないけど。 ebpfとかなんかがんばってる雰囲気。けどやっぱりあんまり違いはわからない。
systemd-nspawn
systemd-nspawnはデフォルトで存在しているのでそこは便利なのだが、決して使いやすいものではない。Nix(内部的にsystemd-nspawn使ってる)にするのがどうせならいいけど、これはこれで重い作業なんだよな。
そういえばfirejailは不十分なことがあってsystemd-nspawn(+systemd-run)にしてるってはなしあるけどよくわからんな。
まあでもプロセス隔離ならchroot+Linux namespace+seccomp-bpfでよくて、image buildやらpull/pushとかしたいとかでなければ要らん気もするな。
bubblewrap
flatpackで使われている(sandbox部分を切り出した?)setuidサンドボックス環境。 unshareのラッパーみたいなプリミティブなやつで、下で紹介するbubblejailのほうもあるんだけど、bubblewrap自体もbwrapコマンドでお手軽にサンドボックス環境を作ることはできる。 まあやっぱりfirejailと変わらない雰囲気。
これは直接本論とは関係ないのだが、OCamlのパッケージマネージャであるopamをLinuxで利用する場合にbubblewrapが必要となる(opamはsandbox環境でコマンドを実行することを強制しているのだ、賢い!)
bubblejail
bubblejailは上のbubblewrap(bwrap)の土台にしたもの。 READMEにfirejailとの違いがあって、
firejailの最大の問題点の1つは、誤ってサンドボックス化されていないアプリケーションを実行しても気付かない可能性があることです。 bubblejailは既存のホームディレクトリを透過的に上書きしようとする代わりに独立したホームディレクトリを作成します。 各インスタンスは独立したホームディレクトリを表します。通常、サンドボックス化されたアプリケーションはそれぞれ独自のホームディレクトリを持ちます。
とのこと。 あとまあbwrapは全部引数で渡す必要があるので設定ファイルで分離するとか。
crabjail
これもbubblewrap相当のcrablockというプログラムのラッパーになっている。 crablock自体bubblewrapのようにコマンドとして利用でき、crabjailはcrablockを呼び出す構造になっている。 特徴としてはcrablockも含めてRustで書かれている点とかか。
cage
内部実装にlandlockを使っている。 landlockはこんな話もあって、うーん不十分!wという気もする。
クロスプラットフォームなのが特徴といえばそうなのだが、とくに求めていないしな。 macOSの人はこれ使ったらいいと思いました。seatbeltよく知らないけど。
landrun
だいたいcageと同じと思われる。
結論
人類車輪の再発明好きすぎでは? bubblejailがよさそうな雰囲気あるけどbubblewrapのラッパーを自作するのがいい気がしてきたのでやる(はいまた車輪の再発明を繰り返す)(高速な伏線回収)