# デプロイ: バイナリ・埋め込み・ラッピング 素の`culebra`実行以外でCulebraコードを動かす3つの方法: standalone AOTバイナリ(`culebra build`)、C++ ホスト内へのVM/JIT埋め込み、自分の C++ クラスをビルトインとして公開する拡張`culebra`バイナリ (`culebra wrap`)。3つは1つのruntime archiveレイアウトを共有し、 [§4](#4-共有-runtime-archive-レイアウト) にまとめて記述し各章から参照する。 目次 ---- 1. [Standalone バイナリビルド(`culebra build`)](#1-standalone-バイナリビルドculebra-build) 2. [C++ ホストへの Culebra 埋め込み](#2-c-ホストへの-culebra-埋め込み) 3. [C++ ライブラリのラッピング(`culebra wrap`)](#3-c-ライブラリのラッピングculebra-wrap) 4. [共有 runtime archive レイアウト](#4-共有-runtime-archive-レイアウト) 開発用サブコマンド — `test`・`lint`・`fmt`・`dap` — は [`tooling.ja.md`](tooling.ja.md) が扱います。 ## 1. Standalone バイナリビルド(`culebra build`) `culebra build`は`.cul`ソースを **LLVM AOT codegen + システム `cc`** によって単体実行ファイルにコンパイルします。生成バイナリに LLVMランタイムは含まれず、依存表面は`libc++` / `libSystem` (macOS) または`libstdc++` / `libc` (Linux)、それと`Tensor`を 参照するときに限り`Accelerate` / BLASが加わるだけです。 ```sh culebra build path/to/program.cul -o ./program ./program [args...] ``` デフォルトはホストプラットフォーム向け。 ### ホスト側に必要なもの スクリプトの実行に`culebra`バイナリ以外は要りません。`--jit`も同じで、 コンパイルに使うLLVMはバイナリの中にあります。外へ出るのは `culebra build`だけです — codegenはプロセス内で完結しますが、 リンクにはプラットフォーム側のオブジェクトファイル周りが要り、 一度も何もビルドしたことのないマシンにはそれがありません。`build`は コンパイルを始める前にこれを確認し、端末上ならば足りないものを 「名前を挙げるだけ」でなくインストールするか尋ねます。 | ホスト | リンク段が必要とするもの | 入手方法 | |---|---|---| | macOS | Xcode Command Line Tools(`/usr/bin`の`cc`はその実体ではなくシムで、Mach-Oが`libSystem`をリンクする先のSDKスタブもこれが持ち込む) | `culebra toolchain install`(Appleのインストーラを起動)または`xcode-select --install` | | Linux | `cc`・libstdc++・Cランタイムのスタートアップファイル | `sudo apt install g++`(Debian / Ubuntu)、`sudo dnf install gcc-c++`(Fedora) | | Windows | mingwのCRTオブジェクトと静的ライブラリ — **コンパイラもリンカも不要** | `culebra toolchain install`(約8MB) | 他には何も要りません。AOTのリンクが必要とするアーカイブは、 サードパーティの静的ライブラリも含めて`culebra`バイナリ自身から 出てきます([§4](#4-共有-runtime-archive-レイアウト))。 システムからリンクされるライブラリとして残るのはzlibだけで、 `Http`・`Compress`・`to_png`を使うプログラムが`-lz`で引きます — macOSとWindowsのkitには既にありますが、Linuxではリンカが見る `libz.so`のシンボリックリンクがdevパッケージ側にあります (`sudo apt install zlib1g-dev`、`sudo dnf install zlib-devel`)。 culebraが自分でインストールしない唯一のホストがLinuxです。そこでの C++ツールチェーンはディストリビューションのパッケージマネージャの 持ち物でrootが要り、ユーザーの代わりに`sudo`を叩くのはコンパイラの 仕事ではありません。`culebra toolchain install`はそのディストリビュー ション向けのコマンドを1つ表示して終わります。 ### `culebra toolchain` ```sh culebra toolchain status # 今あるものと、リンクできるかどうか culebra toolchain install # 足りないものを入れる culebra toolchain install --from # リリース版でなく手元でビルドしたkitから culebra toolchain uninstall # culebraが入れたものを削除する ``` Windowsでは`install`が、このバイナリが名乗るバージョンのリリースに 添付されたkitを取得し、隣に公開されたダイジェストと突き合わせてから 展開します。展開先は ``` %LOCALAPPDATA%\culebra\toolchain\\ ``` 権限昇格も`PATH`の書き換えもレジストリへの書き込みもありません。 `uninstall`はこのディレクトリの削除で、手で消しても同じことです。 kitがバージョンごとに分かれているのは、kitのlibstdc++がこのバイナリに 埋め込まれたランタイムアーカイブをコンパイルしたものと同一でなければ ならないからです — `install`はマニフェストが別バージョンを名乗るkitを 拒否しますし、culebraを上げたら付属のkitを入れ直すことになります。 他のプラットフォームでの`install`は薄いものです。macOSではAppleの Command Line Toolsインストーラを起動し(このプロセスが待てないGUIの 流れなので、起動したことを伝えてbuildの再実行を促します)、Linuxでは パッケージコマンドを表示します。`uninstall`はどちらでも「culebraが 入れたものは無い」と報告します。 ### Windowsにコンパイラが要らない理由 Windowsはツールチェーンを一切同梱していませんが、そこでAOTのリンクに 足りないのはリンカであってコンパイラではありません — `culebra build`は オブジェクトをプロセス内で自分で吐きます。そこでリンカをバイナリの中に 置きました。JITのために既に載せているLLVMの上に、culebraがlldを リンクしています。kitが供給するのはリンクのmingw側の半分だけ — CRTオブジェクト、libstdc++ / libgcc、そしてWin32のインポートライブラリです。 そのLLVMを共有できることが安く済む理由で、代替案は推測でなく実測して 比べました。MSYS2の`ld.lld.exe`は5.7MBのプログラムですが147MBの `libLLVM` DLLにリンクされているため、それを同梱するkitは圧縮後55MB。 同じ静的アーカイブから自前でビルドしたlldはDLLを必要としませんが、 隣のバイナリと何も共有できず圧縮後41MBでした。lldを`culebra`に リンクする形はダウンロードを23MB増やし(lldのアーカイブ自体は4MBですが、 lldのCOFFドライバが全LLVMバックエンドを参照するため芋づる式に入る)、 kitは8MBに収まります。結果として`culebra build`を使う人の合計は 84MBでなく約60MB、その代わり一度も使わない人が23MBを負担します。 kitはリリース時に、バイナリの中のランタイムアーカイブをコンパイルしたのと 同じMSYS2 UCRT64ツリーから梱包されます(`misc/windows_toolchain/pack_windows_toolchain.sh`)。 両者のlibstdc++が一致することが、誰かが正しく突き合わせた結果ではなく 構成上の帰結になるのはこのためです。kitはそのツールチェーンのC++ドライバが 組み立てるリンカコマンドライン(`link-recipe.txt`)を記録しており、 culebraは自分のオブジェクトとfeature側のライブラリをその中間に差し込みます — つまりculebraが行うリンクは、ドライバ抜きで、ドライバが行ったはずの リンクそのものです。 他のMSYS2環境でなくUCRT64なのは、同じlibstdc++ ABI(UCRT64のclangはGCCの libstdc++の上にあり、CLANG64のlibc++は別ABI)と同じCランタイムだからです。 GNU ldでなくlldなのは、PEをELFやMach-Oのリンカと同じようにdead-stripできるのが lldだけだからです。GNU ldは全関数のunwind情報を残し、その情報が関数を残すので、 `--gc-sections`を付けてもランタイムアーカイブ全体が全プログラムに残ります。 WindowsでCulebra自体をソースからビルドするのは別の話で、そちらは完全な MSYS2ツールチェーンを必要とします。[`CONTRIBUTING.md`](../CONTRIBUTING.md)が 扱います。 ### オプション | フラグ | 説明 | |---|---| | `-o ` | 出力実行ファイルのパス(必須) | | `-O` | 最適化レベル0–3(デフォルト2) | | `--emit-llvm` | プログラムのLLVM IRも書き出す(デバッグ用) | | `--keep-symbols` | デバッグ用にシンボルテーブルを出力に残す(詳細は後述の[シンボルの除去](#シンボルの除去)を参照) | | `--no-tls` | `Http`をOpenSSL抜きでリンクする。約3.9 MB小さくなるが、`https://`と`wss://`は`HttpError`になる(後述の[TLS抜きでHttpをビルドする](#tls抜きでhttpをビルドする)を参照) | | `--target=` | 指定LLVM triple向けにクロスコンパイル | | `--sysroot=` | `cc`の`--sysroot=`にそのまま渡す | | `--rt-lib=` | ランタイムアーカイブのパスを上書き(cross-compileでは必須) | ### 環境変数オーバーライド | 変数 | 効果 | |---|---| | `CULEBRA_VERBOSE=1` | 中間オブジェクトのパスと完全なリンクコマンドを表示 | | `TMPDIR` | 中間オブジェクトファイル置き場(デフォルト`/tmp`) | ### Tensor-free / Http-free バイナリ 各機能軸は独立にforce-loadされる(仕組みは [§4](#4-共有-runtime-archive-レイアウト) 参照)ので、`Tensor`も `Http`も使わないプログラムはbaseのみをlinkする。OpenSSLを 落とすだけで約4.7 MB効く(非Httpバイナリ ~0.3 MBに対しHttp版は ~5.1 MB、OpenSSLは静的リンクのため)。同じゲーティングは、外部 ライブラリを引かないが自前のコードを抱える2つのサブシステムにも 効く。1つめは正規表現エンジン(`Regex`、`re'...'`リテラルを含む)。 この軸のコストは約0.9 MBで、cpp-regexlibとこのnamespace自身のgroupの 合計。実測の内訳はコード約0.55 MBとテーブル約0.24 MB。 外部依存が無くても弱/強分岐が要るのは、 エンジン内部の`__builtin_cpu_supports`によるランタイム分岐チェックが コンパイラに、それをコンパイルする翻訳単位ごとの起動時CPUID コンストラクタを生成させるためで、これを無条件にすると自前のコード 以外にもこのコンストラクタが全バイナリに入ってしまう。 2つめはTensorのelementwiseカーネルで、これがこの軸に乗る理由は バックエンドのchokeではカバーできない。`culebra::tensor_eval_node`が ゲートするのはBLASとMetalだが、cpp-tensorlibの`map_binary`実体化は 純粋なC++なので外部依存では締め出せない。しかも汎用の算術ヘルパは どちらかの被演算子が`Tensor`でありうる限りそこへ到達する — つまり 算術を含む全バイナリが該当する。そこで`culebra::tensor_binop`と `culebra::tensor_inplace_binop`(遅延の`+`とin-placeの`+=`の経路)に 同じ弱/強分岐を掛けている。効果は約115 KBで、helloの4分の1に当たる。 3つめは`culebra::tensor_backward`で、その理由はサイズでは ない。cpp-tensorlibは確保とグラフ評価をtensor archiveが仕込むhook経由に しているので、グラフを組むだけの翻訳単位はバックエンドを名指ししない。 しかしeagerに走るカーネルは直接デバイスを掴みにいき、VJPの1つがそれを 呼ぶ(`array::xent_bwd`、融合損失のpullback)。Tensor不使用の バイナリにそこを呼ぶ経路は無い——渡せる`Tensor`値が存在しない。だが リンカはそこを問わない。`-framework Metal`がリンク行に載るのはAST スキャンが`Tensor`を報告したときだけで、参照を置いているのはその スキャンではない——loweringはメソッド"名"`clone`・`backward`・`detach`を これらのruntime helperへ対応付けるので、自分で定義したクラスに同名の メソッドがあるだけでそこにTensorの腕が生成され、そのプログラムの `culebra build`がmacOSで`_MTLCreateSystemDefaultDevice`未定義で落ちて いた。(`array::clone()`のデバイスコピーは1つ下の層の同じ穴で、choke ではなくruntime hookにした。tlは`reshape`や`logsumexp`の内部から `clone`を呼ぶので、culebra側の境界では塞ぎようがない。) `tools/checks/check_rt_archive_backend_free.sh`がこの線を守る——base archive単体のdead-stripリンクで全runtime helperを強制的に生かし、 未定義のまま残るものを読む。 `Proc`のfork/exec層、`Canvas.Sprite.from_png` / `Canvas.Font`の背後の PNG/TTFデコーダ、そして`PEG`(cpp-peglib)も外部ライブラリを引かない が、それぞれ専用のchokeは不要——自身のnamespaceのdispatch tableを 通じてのみ到達するプレーンなコードとしてコンパイルされるので、 名指ししないプログラムはリンクされない(仕組みは [§4](#4-共有-runtime-archive-レイアウト) 参照)。ただし`PEG`は このdead-strippingが全バイトには届かない唯一のnamespaceで、`culebra:: pegparser::load()`自体はPEGを名指ししないバイナリから消えている ことを`--keep-symbols`ビルドの`nm -C --defined-only`で確認済みだが、 peglibの`Ope`クラス階層はvtableとtypeinfoを残し、それを `--gc-sections`が使用の有無に関わらず保持する。加えて、`std::function` に包んだローカルラムダのcomdatテンプレート実体化が、それを呼ぶはずの 関数が消えたあとも孤立して生き残る(リンカの既知の制限で、到達可能な 呼び出し経路ではない)。どちらも不活性で、PEG未使用バイナリで実行され ることは無く、実測で固定約53 KB——2本目のarchiveをforce-loadして 避けるのではなく、これは受け入れる。 `otool -L`(macOS)や`ldd`(Linux)で確認できます: ```sh $ culebra build my-program.cul -o /tmp/my-program # Tensor 未使用 $ otool -L /tmp/my-program /tmp/my-program: /usr/lib/libc++.1.dylib /System/Library/Frameworks/CoreServices.framework/.../CoreServices /usr/lib/libSystem.B.dylib /System/Library/Frameworks/CoreFoundation.framework/.../CoreFoundation ``` Tensorを使うプログラムでは全部入りアーカイブとフレームワーク 両方がリンクされます: ```sh $ otool -L /tmp/microgpt_tensor /tmp/microgpt_tensor: /usr/lib/libc++.1.dylib /System/Library/Frameworks/Accelerate.framework/.../Accelerate /usr/lib/libSystem.B.dylib ``` ### TLS抜きでHttpをビルドする `Http`がバイナリに足すもののうち82%はOpenSSLで、`http://`しか喋らない プログラムもそれを丸ごと払っています。`--no-tls`はHttp軸のもう半分 (TLS無しでビルドしたcpp-httplib)をリンクし、OpenSSLのアーカイブを 一切追加しません: ```sh $ culebra build fetch.cul -o fetch # 5.07 MB $ culebra build fetch.cul -o fetch --no-tls # 1.22 MB ``` macOS arm64で1行の`Http.get`を実測した値です。差の3.9 MBはコード 2.1 MB・テーブル1.6 MB・文字列0.2 MB。テーブルが効かない理由でもあり、 楕円曲線の事前計算値は圧縮できません(deflateしてもこのセクションは 87%にしかなりません)。 これを走査で自動判定することはできません。どのアーカイブが要るかは プログラムが名指しする名前から決まりますが、`Http.get(url)`を見ても `url`が`https://`かどうかは分かりません。それは実行時の値で、設定 ファイルからURLを組み立てるプログラムはコンパイルが通って現場で失敗 します。だからこれはビルド時に人が決めるものにしてあり、この形で ビルドしたバイナリはTLSのURLを平文にフォールバックさせず拒否します: ``` HttpError: Http: this binary was built with --no-tls, so https URLs are not supported: https://example.com/ at 1:9. ``` `wss://`も同じです。それ以外は変わりません。`http://`・`ws://`・ サーバ・gzipの透過展開はそのまま動き、zlibもリンクされたままです。 AOTバイナリが他の2レーンと食い違うことを許している唯一の箇所です。 VMと`--jit`は常にTLSを持ちます(どちらもOpenSSLを積んだ`culebra`の中で 走るため)。食い違いは自分で渡したフラグの範囲に限られ、黙ってではなく 明示的に失敗し、既定は変わりません。`--no-tls`を付けずにビルドすれば 3レーンは従来どおり一致します。 ### シンボルの除去 出力バイナリは自分のシンボルテーブルを読まない(ランタイムは 名前でシンボルを解決しない)ので、ビルドは2段階でこれを取り除く。 まずリンクがローカルシンボルを破棄し(`-Wl,-x` — ld64・GNU ld・ lldのいずれも解釈する。埋め込みランタイムアーカイブには `GCC_except_table*`、テンプレートや文字列の実体化が数千個ある)、 次にプラットフォームの`strip`ツールがリンカの残したグローバル シンボルテーブルを除去する(リリースパッケージが`culebra`本体に 掛けているのと同じ処理)。この2つで`print("hello")`のバイナリは 0.52 MB → 0.42 MBになる。ローダが必要とする動的シンボルは どちらの段階でも残る。デバッグ用に両方を飛ばすには`--keep-symbols` を渡す。クロスコンパイル(`--target`)の出力はリンク段階で止まる (ホストの`strip`は他形式のオブジェクトを読めない)。`PATH`に `strip`が無い場合も同様にエラーにはならない。 ### クロスコンパイル `--target=`でLLVMターゲットを指定。よく使うtriple: - `x86_64-unknown-linux-gnu` - `aarch64-unknown-linux-gnu` - `x86_64-apple-macosx` クロスコンパイルにはユーザ側で以下を用意する必要があります: 1. **ターゲット用sysroot**(ターゲットのC++ ヘッダ、`libc`、 CRTファイルを含むディレクトリ)。`--sysroot=`で渡す。 2. **ターゲット向けにビルドしたランタイムアーカイブ**。 `--rt-lib=`で渡す。ホスト用`libculebra_rt.a`は ホストtriple向けにビルドされているので使えません。 ターゲット向けランタイムをビルドするには、CMakeにターゲット ツールチェーン(同じソースツリーを、ターゲットsysrootと`cc` で設定)を向けてください。 現状の制約: 各ターゲット向けランタイムは同梱されず、ユーザが自前 CMake / ツールチェーンで生成する必要がある(下の例を参照)。また `--target=`と`Tensor`の併用はrejectされる — ホストの BLASリンクフラグはターゲットでは正しくないため。`Tensor`参照を 外すか、将来のフェーズを待つこと。 #### 例: macOS ホストから Linux x86_64 向け ```sh # 1. ターゲット向けランタイムをビルド(ターゲットごと 1 回) # Linux sysroot が $LINUX_SYSROOT、cross 用 cc が用意済みとする cmake -B build-linux-x86_64 \ -DCMAKE_C_COMPILER=clang \ -DCMAKE_CXX_COMPILER=clang++ \ -DCMAKE_C_FLAGS="--target=x86_64-unknown-linux-gnu --sysroot=$LINUX_SYSROOT" \ -DCMAKE_CXX_FLAGS="--target=x86_64-unknown-linux-gnu --sysroot=$LINUX_SYSROOT" \ -DCULEBRA_ENABLE_JIT=ON # base アーカイブは元々 Tensor-free(弱スタブ)で、cross は Tensor 非対応 # (上の制限参照)なので base をビルドする。 cmake --build build-linux-x86_64 --target culebra_rt # 2. プログラムをクロスコンパイル culebra build my-program.cul \ --target=x86_64-unknown-linux-gnu \ --sysroot=$LINUX_SYSROOT \ --rt-lib=$PWD/build-linux-x86_64/libculebra_rt.a \ -o ./my-program-linux # 3. 確認(Linux ホスト上、またはエミュレータ経由) file ./my-program-linux # ELF 64-bit LSB executable, x86-64, ... ``` ## 2. C++ ホストへの Culebra 埋め込み Culebraはheader-onlyです。ヘッダをincludeし、JITを使う場合は LLVMをリンクすれば、C++ からbytecode VMやJITを駆動できます。 ### 最小例 ```cpp #include int main() { culebra::Runtime rt; culebra::RuntimeScope scope(rt); culebra::vm::Embed embed; // stdlib install 済み・trait 登録済み culebra::vm::Value val; std::vector msgs; embed.run_source("", "1 + 2", val, msgs); // val.to_long() == 3 } ``` includeは``の1つで足ります。パーサ・stdlibのバインディング・ module loader・`CULEBRA_VERSION`・下のセッション、そして `CULEBRA_JIT_ENABLED`のもとではJITの入口まで、これが引き込みます。 セッション自体は``にあり、それだけをincludeすることもできます。 `Embed`の前の2行は省けます。書かなければ、エンジンを最初に使った 時点で**スレッドローカルの既定Runtime**が作られ、以下はそのまま動きます (READMEの例が3行短いのはこのためです)。書くのは、既定では足りない ものが要るときです — 1つのスレッドに独立したエンジンを複数持つ ([後述](#同一スレッドで複数スクリプトを動かす))か、このエンジンにだけ 絞った標準ライブラリを渡す([後述](#runtime-ごとの拡張フック))。 どちらも要らないホストは、2本目のスレッドを立てるまで`Runtime`の存在を 忘れていて構いません([スレッディング](#スレッディング))。 `run_source`は失敗をテキストで返します(CLIが表示するのと同じ 行)。表示先のコンソールを持たないホストにはもう一方の入口が あります。値が戻り値で、失敗は`call`と同じ例外です: ```cpp auto n = embed.eval("1 + 2").as(); // 3 ``` `eval`は`culebra::CulebraError`を投げます。種別・メッセージ・ 位置はそのまま残ります。parse失敗の種別は`SyntaxError`、実行中 の失敗はその種別です。捕まらなかったスクリプトの`throw`だけは 種別`RuntimeError`になり、どのレーンも表示する`uncaught: ...`の 行を持ちます — 投げられたオブジェクトはエンジンの境界で1行の テキストに畳まれるためです。 `vm::Embed`はセッションです。各実行はそれまでの実行が 作ったトップレベル束縛を見ることができ、ホストは実行後にそれを 読み戻したり(`embed.global("x")`)、呼び出したり(後述の `embed.call`)できます。独立したエンジンインスタンスごとに1つの `Embed`を使ってください。`run_source`はソースをコピーして保持 します(parseのASTはそのコピーをstring_viewで参照し、 参照するプログラムが生きている間セッションが所有します)。 ホスト側でparse済みの入力には低レベルの `run(ast, source, ...)`を使い、ASTのトークンが参照するバッファを 渡してください。 CLIと同じようにプログラム全体(import解決・stdlib preambleの splice込み)を動かすには、loaderで読み込んでmodule listを 渡します: ```cpp culebra::ModuleLoader loader; auto modules = loader.load_program(path, entry_source, msgs); culebra::splice_stdlib_preamble(modules); culebra::vm::Value val; embed.run(modules, val, msgs); ``` LLVMレーンを使う場合は、起動時に`culebra::install_jit_stdlib()`を1回 呼んで`culebra::JIT::run(ast)`を使います。どちらも``が宣言し、 `CULEBRA_JIT_ENABLED`のもとにあるのは`JIT::run`だけです(`Embed`は JITの有無に関わらず自分でstdlibをinstallします)。 ### ホストプログラムのビルド インストール手順も、ビルドすべきライブラリもありません。ホストは culebraのチェックアウトに対して直接コンパイルします。ヘッダがvendor配下の ライブラリを参照するのでそれらのディレクトリもinclude pathに入り、stdlibは スクリプトが使うかどうかに関わらずzlibと(macOSでは)3つのシステム フレームワークに届きます。`culebra embed-flags`は、スクリプトを実行する そのバイナリ自身から、この一式を出力します: ```sh c++ $(culebra embed-flags --cflags) host.cpp \ $(culebra embed-flags --libs) -o host ``` 機能はフラグで足します(`--http`・`--tls`・`--sqlite`・`--codegen`・ `--jit`)。`--sources`は、その構成でホストがコンパイルすべきculebra側の `.c` / `.cc`を出力します: ```sh flags="--http --tls --sqlite" cc -O2 -w -c $(culebra embed-flags --sources $flags) -o culebra_sqlite3.o c++ $(culebra embed-flags --cflags $flags) $(pkg-config --cflags openssl) \ host.cpp culebra_sqlite3.o \ $(culebra embed-flags --libs $flags) $(pkg-config --libs openssl) -o host ``` `--sources`が出すファイルは、それぞれの言語のドライバでコンパイルします。 SQLiteのamalgamationはCで、C++コンパイラは最初に現れる`new`という名前で 弾きます。 OpenSSLとLLVMはホスト側のものです。prefixはculebraが決める話ではないので、 `--tls`と`--jit`はculebra側だけを設定し、あちらの指定は `pkg-config` / `llvm-config`に任せます。JITなら`--cflags`の横に `-I "$(llvm-config --includedir)"`です。`--cxxflags`は使いません—— それ自身の`-std=c++17 -fno-exceptions`がculebra側の指定の上に乗ります。 フラグが指すのはチェックアウト——そのバイナリのビルド元か`$CULEBRA_HOME`—— です。ダウンロードしたバイナリはその版のソースをcloneするまでどちらも 持たないので、ビルドされたマシンのパスを出す代わりにその旨を伝えます。 この節の残りは同じ一式をこのページに書き下したものです。中身を見たい 読者と、シェルを介したくないビルドシステムのために残しています。ただし ページに書けるリンク行は1つのプラットフォームのもので、コマンドが出すのは ホストのものです。VMレーンにLLVMは要りません: ```sh c++ -std=c++23 \ -I culebra/include \ -I culebra/vendor/cpp-peglib \ -I culebra/vendor/cpp-unicodelib \ -I culebra/vendor/cpp-tensorlib/include \ -I culebra/vendor/stb \ -I culebra/vendor/cpp-regexlib \ -I culebra/vendor/cpp-fstlib \ -I culebra/vendor/cpp-searchlib/include \ -isystem culebra/vendor/cpp-searchlib/third_party \ host.cpp -lz -o host ``` どれも省けません。`vendor/stb`が無いと`font_ttf.h`で、 `vendor/cpp-regexlib`が無いと`regex.h`で、`vendor/cpp-fstlib`が無いと `fst.h`で、`vendor/cpp-searchlib/include`が無いと`search.h`で、 `vendor/cpp-searchlib/third_party`が無いと`searchlib_segment.h` (`Search.segmenter`の日本語分かち書き)でビルドが止まります — stdlibがどれも無条件に参照するためです。`-lz`はAOTのリンクが 必要とするzlibと同じもので([§1](#ホスト側に必要なもの))、 `Compress`とPNGライタが参照するため、スクリプトが使うかどうかに 関わらずホスト側でリンクします。 macOSでは、stdlibが無条件に参照するシステムフレームワークもリンクに 加えます。`FS.watch`(FSEvents)の`CoreServices`と、`Tensor` バックエンドの`Accelerate`・`Metal`です: ```sh c++ -std=c++23 ...上の-Iリスト... \ host.cpp -lz -framework CoreServices -framework Accelerate \ -framework Metal -o host ``` LLVMレーン(`JIT::run`・`JIT::build_object`)では、該当ヘッダを 有効にするdefineとLLVM本体が加わります: ```sh c++ -std=c++23 -DCULEBRA_JIT_ENABLED \ -I "$(llvm-config --includedir)" \ ...上の-Iリスト... \ host.cpp $(llvm-config --ldflags --libs --system-libs) -lz -o host ``` LLVMは20以降が要ります(`CMakeLists.txt`がculebra自身のビルドに課している 下限と同じです)。複数のLLVMが入ったマシンでは、素の`llvm-config`は `PATH`の先頭にあるものであって、目的のバージョンとは限りません。 `llvm-config-20`のようにバージョン付きの名前か、culebraをビルドした prefix配下のものを名指ししてください。 `-DCULEBRA_JIT_ENABLED`が無ければ`jit.h`と`vm_lowering.h`は プリプロセスの結果が空になります。`culebra.h`はそのままコンパイルでき、 VMも動き、ホストのどこもLLVMを参照しません。culebra自身のno-JITビルドが 使っているのと同じスイッチです。 `Http`と`SQLite`は`CULEBRA_HTTP_ENABLED` / `CULEBRA_SQLITE_ENABLED`を 定義しない限り入りません(`culebra`のビルドでCMakeが設定するのと同じ 名前です)。定義しなければ、この2つの名前はどちらのレーンにも存在 しません。culebraのヘッダをインクルードするTUは、すべて同じdefineで コンパイルしてください。 `Http`はヘッダオンリーです(`vendor/cpp-httplib`)。httpsにはシステムの OpenSSLを使います。`CPPHTTPLIB_OPENSSL_SUPPORT`なしでもhttpの送受信は でき、httpsのURLは`HttpError`になります: ```sh c++ -std=c++23 ...上の-Iリスト... \ -DCULEBRA_HTTP_ENABLED \ -DCPPHTTPLIB_OPENSSL_SUPPORT $(pkg-config --cflags openssl) \ -I culebra/vendor/cpp-httplib \ host.cpp $(pkg-config --libs openssl) -lz -o host # macOS ではさらに: -framework CoreFoundation -framework Security ``` `SQLite`は同梱のamalgamationをホストに組み込みます。 `src/runtime/culebra_sqlite3.c`は、`culebra`本体と同じオプション (外部キー制約が既定で有効、FTS5、R-Treeなど)を付けたamalgamationです。 これを使えば、リンク時にたまたま見つかるシステムのライブラリではなく、 CLIと同じ動作のSQLiteになります。Cとして一度だけコンパイルします: ```sh cc -O2 -w -c culebra/src/runtime/culebra_sqlite3.c -o culebra_sqlite3.o c++ -std=c++23 ...上の-Iリスト... \ -DCULEBRA_SQLITE_ENABLED -I culebra/vendor/sqlite \ host.cpp culebra_sqlite3.o -lz -o host ``` `CodeGen`はヘッダではなくドライバが登録します。wrapのレジストラが 静的初期化子だからです(理由は`src/runtime/culebra_rt_codegen.cc`)。 使いたいホストは、バインディングをTU側で足し、cpp-vmlibのランタイム契約を 一緒にコンパイルし、`vendor/cpp-vmlib`をinclude pathに入れます。 バインディングは`culebra.h`より前にインクルードしてください。macOSでは `FS.watch`が要るCoreServicesのヘッダが`nil`をマクロにし、cpp-vmlibは その名前のローカル変数を持っています。 ```cpp #include // culebra.h より前に #include namespace { const bool codegen_registered = culebra::register_codegen_binding(); } ``` ```sh c++ -std=c++23 ...上の-Iリスト... -I culebra/vendor/cpp-vmlib \ host.cpp culebra/src/runtime/codegen_rt.cc -lz -o host ``` 残るのは、ウィンドウシステムか専用のビルドが要るものです。`Scene`・ `Webview`・`Desktop`は存在せず(それら無しでビルドした`culebra`と同じく `NameError`)、`Canvas`はオフスクリーン描画のみで `Canvas.window_available()`はありません。これらはヘッダではなく、 あなたのコードを組み込んでドライバを作り直す`culebra wrap` ([§3](#3-c-ライブラリのラッピングculebra-wrap))で入ります。 スクリプトが`Sys.argv`として見る値はプロセス全体のホルダです。 起動時に1回入れてください: ```cpp culebra::sys_argv() = {"--verbose", "input.txt"}; ``` ### スレッディング ランタイムは **Runtimeごと**に単一スレッドで動きます。複数の ホストスレッドからCulebraを使うには、各スレッドが独立した処理 を持つ形にします: ```cpp std::thread([&]{ culebra::Runtime rt; culebra::RuntimeScope scope(rt); culebra::vm::Embed embed; // ... メインスレッドとは独立 ... }).detach(); ``` スレッドごとの状態(GC・例外キャリア・PRNG・defer stack・shape registry)は、初回アクセス時に遅延生成されるthread-localの default `Runtime`に格納されます。異なるスレッドの状態は完全に 分離されています。 PEGパーサもthread-localなので、別スレッドでの並列`parse()`も 安全です。 ### 同一スレッドで複数スクリプトを動かす `culebra::Runtime`はVMコンテキスト1個分を所有します。プラグイン 分離、サンドボックスDSL、ドキュメント毎の状態など、同一スレッド 内で複数のVMが必要なときはRuntimeを明示的に作ります: ```cpp culebra::Runtime rt_a, rt_b; { culebra::RuntimeScope scope(rt_a); // ... rt_a のコンテキストでコンパイル・実行 ... } { culebra::RuntimeScope scope(rt_b); // ... 別の状態、別の GC、別の例外キャリア ... } ``` `RuntimeScope`はRAIIで、デストラクタで以前のアクティブRuntime を復元します。後で`rt_a`に戻ったときは、出ていったときの状態が そのまま残っています。 あるRuntimeで作ったValueを別のRuntimeに渡してはいけません。 GCトラッカー、shape registry、例外キャリアは、その値を作った Runtimeに紐付いています。 ### Runtime ごとの拡張フック `culebra::install_extension()`(およびそのラッパー `culebra::install_jit_stdlib()`)は、現在アクティブなRuntimeを ターゲットにします。これにより、Runtimeごとに異なるホストAPIを 公開できます: ```cpp culebra::Runtime trusted, sandbox; { culebra::RuntimeScope s(trusted); culebra::install_jit_stdlib(); // フル stdlib(IO, Sys, Math, ...) } { culebra::RuntimeScope s(sandbox); install_jit_stdlib_restricted(); // 制限されたサブセット } // 各 Runtime は自分のフックセットに対して Math/IO/Sys を解決する。 ``` `RuntimeScope`がアクティブで無い状態で`install_jit_stdlib()`を 呼ぶと、プロセス共通のデフォルトに書き込まれ、overrideを設定していない すべてのRuntimeがそこにフォールバックします — これが従来の 単一VMとマルチスレッドembeddingのパスです。 ### スクリプトの値を読む・書く `vm::Value`はホストがスクリプトの値を持つためのハンドルです。 読み出しには強さの違う2通りがあります。`to_*`系は寛容で、型が 違えば`0`/`false`/`""`を返します(形を確認済みのホストは何も 払いません)。`as()`は同じ食い違いに対してスクリプト自身の アクセサが出すのと同じ`TypeError`を投げます: ```cpp auto port = cfg["port"].as(); // Longでなければ TypeError auto n = cfg["port"].to_long(); // Longでなければ 0 cfg["port"].type_name(); // "Long" — type_of と同じ ``` ObjectとArrayは容器として読めます。意味はスクリプト側と同じで、 Objectの鍵が無ければ`KeyError`、範囲外の添字は`IndexError`、 どちらでもない受け手は`TypeError`です。 ```cpp Value cfg = embed.global("config"); cfg.size(); // エントリ数(Array/Tupleなら長さ) cfg.has("port"); // 参照の可否。クラスのメソッドも答える cfg["db"]["host"].as(); cfg["hosts"][0].as(); cfg["hosts"][-1]; // 負の添字は末尾から cfg.at(key); // 任意のhashableな鍵(Long・Tupleなど) for (const auto& h : cfg["hosts"]) connect(h.as()); for (const auto& [k, v] : cfg["limits"].items()) set_limit(k.as(), v.as()); ``` ObjectとArrayは参照です。`operator[]`が返すのはコピーではなく スクリプトが持っているその値なので、そこへ書けば親に届きます。 何段深くても同じです。 ```cpp cfg.set("port", 8080); // cfg[k] = v cfg["db"].set("host", "localhost"); // 2段下 cfg["db"]["opts"].set("tls", true); // 3段下 cfg["hosts"].push("b.example"); // Arrayへの追加 cfg["hosts"].set(int64_t{0}, "a2.example"); ``` 値の引数は`Value`のコンストラクタが受ける型すべて(に加えて `std::vector`・`std::map`・`std::optional`)から 変換されるので、数値や文字列をそのまま書けます。逆に、数値や 文字列そのものは値です — Objectから読んだ`Long`はコピーなので、 それに書いても何も変わりません。上のように、それを持っている Object側に書いてください。 ホストからの書き込みにもスクリプト側の規則がそのまま効きます。 `mut`なしで宣言されたプロパティは`ImmutableError`になり、Arrayの 末尾より後ろへの添字書き込みは配列を伸ばさずに`IndexError`に なります(伸ばすのは`push`です)。 スクリプトが作った値に手を入れるのではなく、ホスト側で新しい値を 組むには: ```cpp auto opts = Value::object({{"port", 8080}, {"tags", Value::array({"x", "y"})}}); embed.call("connect", opts); ``` ビルダが作るエントリは`mut`付き(`{mut k: v}`であって`{k: v}` ではない)です。ホストが組んだものはホスト自身のデータなので。 `Value`は、ホストが持っている間、中身を生かし続けます。置き場所は 問いません(ローカル変数、ホスト自身のオブジェクトのメンバ、 `std::vector`の要素)。持つのも手放すのも、その値が来たRuntimeの下で 行ってください。 ### C++ からスクリプト関数を呼ぶ 実行後、トップレベルの`fn`や`let f = fn ...`はセッションに 残っています。`Embed::call`でC++ から呼べます: ```cpp // スクリプト: fn update(x, y) { x + y * 2 } std::vector args; args.emplace_back(int64_t{1}); args.emplace_back(int64_t{2}); auto v = embed.call("update", std::move(args)); // v.to_long() == 5 ``` `call`は位置引数を位置パラメータにバインドします。`vm::Value`の 引数は呼び出しが消費します(新しいvectorを渡す)。結果は 返されたハンドルが所有します。 ホストがその場に書く引数ならvectorは要りません。変換は`set`の 値と同じです: ```cpp auto v = embed.call("update", 1, 2); // v.as() == 5 embed.call("connect", "api", opts); // Valueはそのまま通る ``` ### スクリプトエラーの扱い スクリプト内で発生した失敗は`culebra::CulebraError`(`` で定義、`std::runtime_error`のサブクラス)として送出されます。 スクリプト側`try`/`catch`で見える構造化フィールドをそのまま保 持します: ```cpp class CulebraError : public std::runtime_error { public: std::string kind; // 例: "TypeError"、"ArityError"、ユーザ throw した kind long line = 0, col = 0; }; ``` `Embed::call` / `culebra::JIT::run`などユーザコードを駆動する 経路をくるむ形でcatchします(`Embed::run*`は例外でなく`msgs`で 報告します — CLIが印字するのと同じテキストです): ```cpp try { std::vector args; args.emplace_back(int64_t{1}); args.emplace_back(std::string_view("oops")); embed.call("update", std::move(args)); } catch (const culebra::CulebraError& e) { std::println(stderr, "{}: {} at {}:{}", e.kind, e.what(), e.line, e.col); } catch (const std::exception& e) { // culebra 由来でない例外(ホスト側のバグ、std 失敗など) std::println(stderr, "host: {}", e.what()); } ``` スクリプト側ではこれと同じ値が`catch e { ... }`の`e`として バインドされ、`e.kind` / `e.message` / `e.line` / `e.col`プロ パティでアクセスできます — ユーザ側仕様と標準kind一覧 (`TypeError`, `ArityError`, `IOError`, `ValueError`, `NameError`, `IndexError`, `KeyError`, `AssertionError`, `InternalError`)は [言語仕様 §15](language.ja.md) を参照。 `Embed::call`に届いたスクリプト側`throw expr`はホスト向けに 変換されます: 投げられたオブジェクトの`kind`/`message`がそのまま `CulebraError`のフィールドになる(それ以外は未catchの`throw`が表示する のと同じ形で届く)ので、「catch CulebraError」の契約はuser throwにも 通用します。classの`__index__`や`__setindex__`を走らせる`Value`の 読み書きでも同じです。`items()`と`display()`はスクリプトのコードを 走らせません。 (`Embed::run*`は例外でなく`msgs`で報告します — CLIが印字する のと同じテキストです。) ### interruptはエラーではない Ctrl+Cやisolateのcancelはスクリプトの失敗ではなく停止の要求なので、 **`CulebraError`ではありません**: ```cpp class Interrupted { // 基底なし。std::exception ですらない public: const char* what() const noexcept; // "interrupted" / "isolate cancelled" }; ``` 上のどちらのハンドラもこれを捕まえません。意図的です — スクリプト エラーを報告するために書かれたハンドラがこれを捕まえると、押された Ctrl+Cが診断メッセージになって実行が続いてしまい、フラグはone-shot なので何も止まらなくなります。interruptに答えるのはループをhostする コンポーネントだけで、その表明が型を名指すことです: ```cpp try { embed.call("update", std::move(args)); } catch (const culebra::Interrupted&) { // この実行を止める。失敗として報告しない return; } catch (const culebra::CulebraError& e) { ... } ``` Ctrl+Cで何も止めたくない埋め込み側は、単にハンドラを設置しなければ よい(`culebra::install_sigint_handler()`はopt-inでCLI専用)。 スクリプト側の見え方は変わりません: `catch e`は今までどおり `e.kind == "Interrupted"`のエラーオブジェクトをバインドして再開でき、 押下はそのthrow時に消費されるので、catchしたinterruptが二度発火する ことはありません。 プロセスのロケールもホストのものです。スクリプトが扱うパスはUTF-8ですが、 WindowsのCランタイムはロケールがUTF-8でない限りパスをANSIコードページで 読むので、ASCII外の名前(`é.txt`)は別の名前で作られ、別の名前で探されます。 CLIとAOTのバイナリは起動時に`culebra::install_utf8_locale()`を呼びます (Windows 10 1803以降。ほかのOSでは何もしません)。そうしたパスに触れる スクリプトを動かすホストも、これを呼んでください。 ### ホスト関数の定義 `Embed::define`でC++ のcallableをスクリプトから見える関数として 登録します。引数の型と戻り値の型はcallableのシグネチャから 自動推論されます。 ```cpp embed.define("log", [](std::string msg) { std::cout << msg << "\n"; }, {"msg"}); embed.define("host_add", [](int64_t a, int64_t b) { return a + b; }, {"a", "b"}); ``` **引数名は省略できます。効くのはエラーに出る名前だけ**で、束縛は どちらでも位置です。C++23からはラムダが引数を何と名付けたかを見る手段が ありません — `std::source_location`にも`__PRETTY_FUNCTION__`にも名前は 乗りません(引数名は関数の型の一部ではないからです)。つまりホストが 書かなかった名前はどこからも復元できず、メッセージは位置に落ちます: ``` host_add() argument 'a': expected a Long // {"a", "b"} 付きで定義 host_add() argument '_arg0': expected a Long // 名前なしで定義 ``` 呼ぶ側が見て分かる名前だけ渡し、位置で自明なもの(`host_neg(x)`)は 省く、で構いません。一部だけ名付けることもできます — `{"", "b"}`なら 2番目だけです。(名前が**省略できない**のは`culebra wrap`(§3)の ほうです。ラップしたメソッドはキーワード引数をこの名前で束縛するので、 あちらの名前は診断ではなく意味を持ちます。) サポートする引数・戻り値の型: `int64_t`, `long`, `long long`, `int`, `double`, `float`, `bool`, `std::string`, `std::string_view`, `culebra::vm::Value`(透過)。さらに、これらを容れる `std::vector`(Array)・`std::map`(Object)・ `std::optional`(空なら`nil`)も使えます。入れ子も可です: ```cpp embed.define("total", [](std::vector xs) -> int64_t { return std::accumulate(xs.begin(), xs.end(), int64_t{0}); }, {"xs"}); embed.define("resolve", [](std::string host) -> std::optional { return lookup(host); // 空のoptionalはスクリプトにnilで届く }, {"host"}); ``` ホストの状態には、ラムダのキャプチャで届きます。レジストリは callableをプロセスが終わるまで保持するので、キャプチャが指す先は その関数を呼びうるスクリプトより長く生きている必要があります (値でキャプチャするか、それだけの期間ホストが所有するものを参照で キャプチャしてください)。 容器の引数は要素ごとに変換されるので、要素が1つでも合わなければ その要素単体で出るはずの`TypeError`になります。同じ変換は`Value` からの読み出し(`v.as>()`)にも効きます。 誤った型の引数はcallableに入る前に 呼び出し側でcatch可能な`TypeError`として弾かれ、引数の個数違いは `ArityError`になります。バインドは位置引数のみ(ホスト関数は キーワード引数を取りません)。メソッド・ハンドル・キーワード束縛 などのより豊かな表面が要る場合は`culebra wrap`(§3)でクラスを 宣言してください — AOT含む全レーンに効きます。 ### 自分の embedder から AOT 経路を組み込む 通常のembedderに`libculebra_rt.a`は無関係 — ヘッダオンリー includeがサポート経路で、スクリプトをin-processで走らせるだけなら archiveは要らない。必要になるのは1ケースだけ: `culebra build`と 同じく`culebra::JIT::build_object`を駆動して **standaloneバイナリを 出力したい**とき([§1](#1-standalone-バイナリビルドculebra-build))。 archiveは`culebra_aot_bootstrap`と、生成オブジェクトが呼ぶ ランタイムヘルパを供給する。レイアウトは [§4](#4-共有-runtime-archive-レイアウト)。 **入手先。** `-DCULEBRA_ENABLE_JIT=ON`でconfigureしたCMakeビルド は`libculebra_rt.a`(と機能別archive)をビルドディレクトリに出力する。 配布される`culebra`ドライバは同じarchiveを埋め込んで持っていて、 最初の`culebra build`で`$HOME/.cache/culebra//`に materializeする — 自前のlink手順からそのパスを指してもよい。 **オブジェクトの生成。** プログラムは`ModuleLoader`経由で読み込み、 `build_object`に渡す**前に** stdlib preambleをspliceする。この preambleが`println` / `inspect`を定義している。(`Stringer` / `Eq` / `Comparable`の宣言はこれには含まれない — `build_object`が`JIT::run`と 同じく自前でprependする。) ```cpp #include #include #include #include int main() { std::string src = "1 + 2"; // 実際のホストではファイルから読む std::vector msgs; culebra::ModuleLoader loader; auto modules = loader.load_program("prog.cul", src, msgs); culebra::splice_stdlib_preamble(modules); // 必須 — 下記参照 culebra::install_jit_stdlib(); return culebra::JIT::build_object(modules, "prog.o", /*opt_level=*/2); } ``` > **`splice_stdlib_preamble`を飛ばすと黙って失敗する。** オブジェクト > の生成もlinkも通り、バイナリはexit 0で終わる — ただし何も出力 > しない。`println`が何にも解決されなかったため。診断は一切出ない > ので、AOTバイナリが静かに何もしないならこれを疑う。 **リンク。** 生成オブジェクトに必要なのはarchiveとdead-stripと C++ ランタイムだけ(重い機能を使わないプログラムの場合): ```bash # macOS cc prog.o libculebra_rt.a -Wl,-dead_strip -Wl,-x -lc++ -o prog # Linux (オブジェクトは非 PIC なので PIE 既定のディストロでは -no-pie が要る) cc prog.o libculebra_rt.a -Wl,--gc-sections -Wl,-x -no-pie -lstdc++ -lm -o prog # どちらも: 残ったシンボルテーブルを落とす (`culebra build` の既定動作) strip prog ``` 機能のnamespace(`Tensor` / `Http` / `Compress` / `SQLite` / `Regex` / `Canvas` / …、[§4](#4-共有-runtime-archive-レイアウト) の表)を 参照するプログラムは、その機能のarchiveを **force-load** する必要がある — Mach-Oなら `-Wl,-force_load,`、ELFなら`-Wl,--whole-archive -Wl,--no-whole-archive` — さらにその機能の外部ライブラリも要る。 単にappendしても効かない: base archiveの弱シンボルスタブが既に シンボルを満たしてしまい、メンバがloadされない (gatingは [§4](#4-共有-runtime-archive-レイアウト))。 これらの規則を書き写すより、ドライバを一度 `CULEBRA_VERBOSE=1 culebra build prog.cul -o prog`で走らせるのが早い。 実際に使った`link:`コマンドラインが機能archive込みでそのまま 表示され、embedderに必要なのも同じ形。 (`--rt-lib=`はCLI側 の上書き用オプションで、crossビルドしたarchiveなどを指すのに使う。) `CULEBRA_RT_DEFINE_RUNTIME`マクロは、`CULEBRA_RT_INLINE`タグ付き ヘルパを`inline`から`extern "C"`に切り替えてarchive側TUが唯一 のownerになるようにする。ヘッダオンリーembedderは **絶対に defineしてはいけない**。AOT archiveの生成元TU (`src/runtime/culebra_rt.cc`) のみでdefineされるべき。 ### スモークテスト リポジトリには契約を検証する小さなサンプルが含まれます: * [`tests/embedding/mt_smoke.cc`](../tests/embedding/mt_smoke.cc) — 4つのホストスレッドがそれぞれtry/catch付きスクリプトをparse + Embedセッションを実行、加えてJITパスでも4スレッド。合計240並行実行。 * [`tests/embedding/mi_smoke.cc`](../tests/embedding/mi_smoke.cc) — 1スレッド内で2つのRuntime(それぞれ自分のEmbedを持つ)を 交互に切替え、独立したPRNG状態 と独立したJITフックセットを検証。 * [`tests/embedding/define_smoke.cc`](../tests/embedding/define_smoke.cc) — `Embed::define`を経由してスクリプトと`Embed::call`両方から C++ 関数を呼び、推論型による誤引数の拒否も確認。 ## 3. C++ ライブラリのラッピング(`culebra wrap`) `culebra wrap`は、あなたのC++ クラスをビルトインとして組み込んだ **拡張culebraバイナリ**を作ります — ランタイムのforkもplugin ABIも不要です。短い宣言TUを書くとC++ コンパイラがglueを実体化し (pybind11流)、`--vm`・`--jit`・拡張バイナリの`culebra build` が作るAOTバイナリのすべてで同一に動きます。 ### 宣言する 手元のheader-onlyクラスに対して: ```cpp // vec2.hpp — culebra を一切知らない素の C++ namespace demo { class Vec2 { public: Vec2(double x, double y); double len() const; void scale(double k); std::string show() const; Vec2 unit() const; // 値返し static long dims(); }; } ``` 宣言TUを1つ書きます: ```cpp // vec2_binding.cpp #include #include "vec2.hpp" namespace { const bool registered = [] { culebra::wrap("Geo", "Vec2") .ctor({"x", "y"}) .method<&demo::Vec2::len>("len") .method<&demo::Vec2::scale>("scale", {"k"}) .method<&demo::Vec2::show>("show") .method<&demo::Vec2::unit>("unit") .static_method<&demo::Vec2::dims>("dims"); return true; }(); } ``` メンバ関数は*テンプレート*引数(`method<&T::m>`)なので、メソッドごとに 専用のthunkがコンパイルされます。引数名は省略可能で、エラーメッセージと 実行時のキーワード束縛に使われます。 宣言するのは構築(`ctor`)だけで、破棄は宣言しません — `.dtor`ビルダーは ありません。ラップした型の`~T()`は、ハンドルが継承する確定drop機構 (下記参照)が自動的に呼び出すので、書くのはC++ のデストラクタだけです。 ### ビルドする ```sh culebra wrap vec2_binding.cpp -o ext-culebra # ビルド済みライブラリにリンクする場合: culebra wrap mylib_binding.cpp --link "-L/opt/mylib/lib -lmylib" -o ext-culebra ``` `culebra wrap`はculebraのソースツリー(このバイナリのビルド元、または `$CULEBRA_HOME`)にあなたのTUを加えて再ビルドし、`~/.cache/culebra-wrap/` にキャッシュします。ccacheがあれば実質「宣言のコンパイル + relink」で 済みます。`--lto`で最適化バイナリ(ビルドは遅くなります)。 `--build-dir `を渡すと``でビルドします。ccacheは `CCACHE_BASEDIR`からの相対パスでキャッシュを引くので、同じチェックアウトの 既存ビルドと同じ深さのディレクトリを選び、`--lto`の有無も揃えれば、 そのビルドのオブジェクトを再利用できます。 ### 使う — 全 backend で ```culebra # doctest: skip let v = Geo.Vec2(3.0, 4.0) # `Geo.Vec2.new(3.0, 4.0)`と同じ呼び出し inspect(v.len()) # => 5 v.scale(2.0) let u = v.unit() # 値返し -> 新しい所有インスタンス v.drop() # ~Vec2 が「いま」走る(確定的) v.len() # !! ClosedError ``` `ctor`を宣言すると束縛された名前が呼び出し可能になるので、`class`と 同じ書き方で構築でき、`type_of(Geo.Vec2)`は`'Class'`を返します。 `ctor`を宣言せずにbindした型 — ファクトリ経由でしか得られないハンドル — は呼び出し不可で、通常の`Object`のままです。 ラップされたインスタンスは完全なlifetimeモデルを持つリソースです: scope終端の確定drop(循環込み)、冪等な明示`drop()`、use-after-dropの `ClosedError`。`ext-culebra build script.cul`のAOTバイナリにも バインディングが載ります。 ラップされたインスタンスは、他のインスタンスと同じようにクラスを名乗ります。 `type_of(v)`は宣言した名前(`'Vec2'`)で、`v: Vec2`の引数にそのまま渡せます。 名前は値を作ったクラスから来るので、フィールドを書いて名乗ることはできません。 メソッドもクラスの側にあるので、traitが問い合わせる内容は宣言が実際に持つ メソッドと一致します。 状態はC++側にあってフィールドではなく、その通りに見えます。`keys()`は空、 `size()`は0、spreadは何も写さず、`JSON.stringify`は`{}`、表示はクラス名と 空の中身です。それ以上を言いたいときは`__str__`という名前でメソッドを宣言 してください。isolateを越えることはできません(`SendError`)。送り元の スレッドが所有するオブジェクトを指しているためです。 ラップしたC++ボディ(ctor・メソッド・staticのいずれも)が例外を投げても プロセスの外には出ません。`std::exception`は`what()`を持つcatch可能な `RuntimeError`になり、位置は他のエラーと同じく呼び出し箇所です。ボディが `culebra::CulebraError`を投げた場合はkindとメッセージがそのまま通ります。 バインディングとラップ対象ライブラリ(`--link`)がAOTバイナリに載るのは、 スクリプトがラップ名前空間を参照したときだけです。`ext-culebra`でビルドしても ラップクラスを一切使わないプログラムは、ラップ対象ライブラリを一切リンクせず、 素の`culebra build`と同じサイズになります。判定は保守的な識別子マッチ (`Geo`等)なので、過剰リンクはあっても不足リンクはありません。これは [§4](#4-共有-runtime-archive-レイアウト) で説明する`Tensor` / `Http`と 同じusage-gatingを`libculebra_rt_wrap.a`に適用したものです。 ### マーシャリング | C++ | Culebra | |---|---| | `long` / `int` | `Long` | | `double` / `float` | `Float` | | `bool` | `Bool` | | `std::string` / `std::string_view` / `const char*` | `String` | | 値返しの`T`・`std::unique_ptr` | ラップ済み`T`の所有インスタンス | | `std::shared_ptr` | shareを1つ保持するインスタンス | | `U&` / `const U&` / `U*` / `const U*`(ラップ済み`U`、methodの引数のみ) | `U`のハンドル — 後述 | ラップ済みクラスの`T&` / `const T&`返しは`.method`では**コンパイル エラー**です — 参照は所有形状ではありません。`.borrowed_method`で 宣言してください: ```cpp .borrowed_method<&Box::inner>("inner") // Counter& inner() ``` 結果は**借用ハンドル**です: 所有しない(dropはno-op)、存在する間は 親を生かし、アクセスごとに検証されます — 親が明示dropされた、または 借用取得後に非constメソッドで変更された(各インスタンスはgeneration カウンタを持ち、非const dispatchが自動でbump)場合、解放/再配置済み メモリに触れる代わりに`ClosedError`になります: ```culebra let b = __Foreign.Box.new(3) let c = b.inner() b.reset(9) # 非 const -> generation bump c.value() # !! ClosedError ``` 借用を無効化しないことが確実な非constメソッドはopt-outできます: `.method<&T::touch>("touch", {}, culebra::wrap_policy::preserves_borrows)`。 const性やこのフラグの誤宣言は著者の契約違反(sol2/pybind11と同じ 建付け)ですが、その場合も「staleエラーの過剰/欠落」であってculebra 側のメモリ非安全には絶対になりません。 methodの引数はラップ済みクラスを直接名指せます。**引数はその呼び出しの 間だけ呼び出し元のハンドルを借用する — 所有権を移すのは戻り値だけ** です(上の値返し/`unique_ptr`/`shared_ptr`の行の鏡像)。`U&`/`const U&`は `U`の生きたハンドルを要求し、`U*`/`const U*`はそれに加えて`nil`も 受け付けます。非constの`U&`/`U*`は非constのreceiverが自分自身をbumpする のとまったく同じ形で、その引数自身のgenerationをbumpします。ラップ済み クラスの値渡し・`U&&`・スマートポインタ引数には委譲すべき所有権が無いので サポート対象外です(`.method`宣言はコンパイルエラーになります): ```cpp void merge(const Counter& o) { value_ += o.value_; } // 借用のみ、bumpしない void steal(Counter& o) { value_ += o.value_; o.value_ = 0; } // oをbump void add_maybe(Counter* o) { if (o) value_ += o->value_; } // nil可 ``` ```culebra let c = __Foreign.Counter.new(1) let o = __Foreign.Counter.new(2) c.merge(o) # oは変化しない c.add_maybe(nil) ``` 末尾の引数はデフォルト値を持て、呼び出し側は省略できます。宣言する エントリは素の名前でなく`{"name", value}`にします(bool・整数・ float/double・文字列、`U*`/`const U*`引数なら`nullptr`のいずれか — ここには式評価器が無いので、デフォルト値は常にC++側のリテラルです): ```cpp .method<&Counter::bump_by>("bump_by", {{"n", 1L}}) .method<&Counter::scale>("scale", {"k", {"off", 0L}}) ``` ```culebra let c = __Foreign.Counter.new(1) c.bump_by() # nは既定で1 c.bump_by(n: 5) c.scale(2) # offは既定で0 c.scale(2, off: 1) ``` optional引数は末尾に連続していなければならず、この2つの拡張はどちらも `.method`/`.borrowed_method`限定です — `.ctor`/`.static_method`の引数は 常に必須で、まだラップ済みクラスを名指せません。 コンテナ(`std::vector`/`std::map`)とコールバックは自動ではマーシャルされません。それらが要るメソッド —— 呼び返す`Function`や、読み取る任意の`Object`を取るもの —— は`.raw_method(name, thunk, params)`で宣言し、thunkを手で書きます。宣言的な部分はそのまま効くので、パラメータ名・キーワード引数・既定値は変わりません。引き換えにthunkが負うのは、deducedな側がただで得ているABI契約の全てです: - selfと引数を解放する(`JitMethodSelf`/`JitMethodArgs`)。ABIはcallee-consumesなので、これが無いと呼び出しごとに参照が漏れます。 - 引数を使う前に型検査する。`jit_check_args`はdeducedな経路にしかなく、`jit_handle_self`は渡されたものをそのまま解釈するので、ハンドルのスロットに未検査のスカラーが入ると`TypeError`ではなく野良ポインタになります。 - レシーバを`wrap_detail::jit_handle_self`で解決し、引数を読み(`TAG_UNFILLED`のスロットは呼び出し側が省略したもの)、結果は`surface_native_error_at_call_site`を通して`__ret`へ書く。こうすると本体からのC++例外がプロセスを抜けず、呼び出し位置で捕まえられるエラーになります。 `CodeGen.Program.run`の`natives:`表がこのリポジトリ内の実例です。 このワークフローのend-to-end検証は`tests/wrap_test.sh`が行います。 ### Searchにsplitterを挿す `Search`は解析器からsplitterを受け取りますが([stdlib.ja.md §39](stdlib.ja.md#39-search))、 そのsplitterは自分のクラスでもかまいません。`culebra::search::ISplitter`—— ``、culebraの他のヘッダを何もincludeしないヘッダ——を継承し、 上と同じように`wrap`で宣言します。それ以外に宣言するものはありません。契約を 継承していることが、そのハンドルを解析器の`splitter`に渡せるものにし、索引は`split`を ネイティブに呼びます。文書ごとにプログラムへ戻ることはありません。 ```cpp // stemmer_binding.cpp #include #include class Stemmer : public culebra::search::ISplitter { public: // `text`を`offset`から末尾まで切る: 語ごとに emit(term, position, length) を // 開始位置が戻らない順で呼び、消費したバイト数を返す。 size_t split(std::string_view text, size_t offset, const culebra::search::SplitEmit& emit) const override { // ... 解析器のループ ... return text.size() - offset; } }; namespace { const bool registered = [] { culebra::wrap("Text", "Stemmer").ctor(); return true; }(); } ``` ```culebra # doctest: skip let idx = Search.Index.new(analyzer: { splitter: Text.Stemmer.new() }) ``` 契約はヘッダのコメントにあります。同じsplitterが文書を足すときと問い合わせを解析する ときの両方で走るので、決定的で、複数スレッドから呼べる必要があります。範囲は`text`への UTF-8バイトオフセットで、UTF-16で話す解析器は出口で変換します。語は範囲が指すバイト列と 同じでなくてかまいません——形態素解析器は原形を索引に入れ、範囲は表層のままにするので、 原形で検索すると書かれたとおりの文字列がハイライトされます——が、範囲の開始位置は戻らない こと。直前の語と同じバイトから始まる語は同じ語位置の候補になり(複合語とその構成語。 Luceneの`positionIncrement == 0`)、あとから始まる語は範囲が重なっていても次の位置に なります。それ以外をエンジンは信用しません。開始位置が戻る・書記素クラスタの境界で 切れていない語は索引に入れず捨て、両側で同じように扱います。実体はあなたのものです。 ハンドルをdropすると、それで作った索引の次の`add`か`search`は`ClosedError`になります。 ## 4. 共有 runtime archive レイアウト 上記3つのワークフローはすべて、生成バイナリや埋め込みバイナリが LLVM依存を持たずに済むようstatic **runtime archive** を出荷します。 CMakeは`-DCULEBRA_ENABLE_JIT=ON`で、base archive+ 重い機能ごとに 1つ(2^Nの組合せでなくN+1)を出力します: | Archive | 内容 | |---|---| | `libculebra_rt.a` | base — 全部入りだが各機能のchokeは**弱シンボルのスタブ**(ここから呼べるコードはBLAS・OpenSSL・zlib・sqlite3・正規表現エンジンに到達しない)。サブプロセス層、画像デコーダ(stb_image / stb_truetype)、cpp-peglibパーサジェネレータ(`PEG`)は外部ライブラリを引かないのでこのarchiveに直接コンパイルされ、chokeでなく下のnamespace-group単位のdead-strippingに委ねる——3つのうち`PEG`だけは未使用でも固定約53 KBのpeglib RTTI/vtableメタデータを残す(§1参照) | | `libculebra_rt_tensor.a` | 強いtensor choke 3種: バックエンド側(BLAS / Accelerateを引く)、汎用の算術経路が到達してしまうelementwiseカーネル、そしてVJPがtlのeagerなデバイスカーネルを呼ぶ`backward`([§1](#tensor-free--http-free-バイナリ)参照) | | `libculebra_rt_http.a` | 強いhttp choke(OpenSSL + zlibを引く) | | `libculebra_rt_http_notls.a` | 同じchokeをcpp-httplibのTLS無しでビルドしたもの。`--no-tls`のときこちらが代わりにforce-loadされる([§1](#tls抜きでhttpをビルドする)参照)。アーカイブが2本ある唯一の軸で、1回のリンクは必ず片方だけを取り、baseはどちらも持たない | | `libculebra_rt_compress.a` | 強いcompress choke(zlibを引く。`to_png`もこれに乗る) | | `libculebra_rt_sqlite.a` | 強いsqlite choke+sqlite3 amalgamation | | `libculebra_rt_regex.a` | 強いregex choke(cpp-regexlibエンジン、約320 KB) | | `libculebra_rt_search.a` | 強いsearch choke(cpp-searchlibエンジン。転置索引・簡潔データ構造・問い合わせ文法)。下のモデル読み込みは、ここでは弱いスタブ | | `libculebra_rt_search_segmenter.a` | 強いsegmenter choke(`Search.segmenter`の背後にあるcpp-segmentlibのモデルバックエンド、約140 KB)。プログラムが`segmenter`を名指したときだけforce-loadされるので、モデルを読まない検索プログラムはこれを運ばない | | `libculebra_rt_foreign.a` | foreign objectのテストが書かれている`__Foreign` wrapフィクスチャ(静的な`wrap`レジストラなので専用archiveが要る) | | `libculebra_rt_canvas.a` | raylibのwindowバックエンド(windowビルドのみ。baseはheadlessスタブを持つ) | | `libculebra_rt_audio.a` | Audioのバックエンド: raylibの音声モジュールを単独でビルドしたもの。windowのコードもSDLも引かない(baseは無音のスタブを持つ) | | `libculebra_rt_scene.a` | Sceneのwrap registrar(raylibを引く。baseには一切入っていない) | | `libculebra_rt_webview.a` | Webviewのwrap registrar(OSのWebViewフレームワーク。baseには一切入っていない) | | `libculebra_rt_wrap.a` | `culebra wrap`のバインディング | `culebra build`(および拡張された`ext-culebra build`バイナリ)は 常にbaseをlinkし、ソースASTがそのnamespace(`Tensor` / `Http` / `Compress` / `SQLite` / `Regex` / `Canvas` / `Audio` / `Scene` / `Webview`、あるいはラップされたnamespace)を参照する時だけ機能 archiveを **force-load** し、同じ条件でその外部ライブラリ(BLAS / OpenSSL / zlib / …)を付けます。強いchokeがbaseの弱スタブを上書きする ので、これらの機能をどれも使わないプログラムはどれもlinkしません。 標準ライブラリ自身のdispatch表も、namespace単位で同じようにlinkされ ます。各namespaceの行(`Math.abs`や`Isolate.spawn`が実行時に解決する adapter)とその正準シグネチャは、baseアーカイブが定義はするが自分では 参照しない*group*を成し、プログラムのオブジェクトがgroupの一覧(全stdlib namespace分。ソースが名指ししないものはnullエントリ)を運びます。名指し されたgroupの行だけがdead-strippingを生き残り、それらだけが到達していた ものも一緒に落ちます。走査はプログラムとspliceされたpreambleに対する字句 単位なので、`let m = Math; m.abs(x)`は`Math`を名指ししたと数え、lazy モジュール内の`_Canvas`は`Canvas`がそのモジュールを引き込んだ時点で数え ます。isolate間の値転送コード(値のシリアライズ、channelのendpoint、 `Shared`のview)も同じ仕組みに乗ります: 汎用のプロパティ/添字読み取り 経路は`Shared` viewの読み手にviewのコンストラクタが設定するフック経由で 届き、シンボルでは参照しないので、そのコードを参照するのは`Shared` / `Isolate` / `Parallel` / `Net`のadapterだけになり、それらのgroupと一緒に 落ちます。ランタイム自身のメッセージは`std::format`ではなくculebraの小さな フォーマッタ(`include/base/format.h`)で組み立てます。`std::format`の実装は 1つの引数visitorが整数・浮動小数・文字列のformatterをまとめて名指しするので リンク単位が1ブロックになり、到達する呼び出しが1つあるだけでhelloが15% 太るためです。補間の書式指定(`"{x:.2f}"`)を書いたプログラムは今も `std::format`をリンクします — その書式こそ`std::format`のミニ言語だからです。 機能軸と合わせて、これが`print("hello")`のバイナリを0.4 MB前後 に保っています。走査が見落としたnamespaceは黙って`nil`に読めたりはせず、 到達した時点でそのnamespace名を含む`InternalError`になります。 これらのアーカイブはcpp-embedlibによって **`culebra`ドライバに 直接埋め込まれています** — ドライバは単体で完結する1バイナリで、 サイドカーの`.a`ファイルを別途インストールする必要はありません。 deflate圧縮して格納しており、ドライバ内で37.4 MBのところ11.9 MB です。`culebra build`の初回呼び出し時に必要なアーカイブを `$HOME/.cache/culebra//lib*.a`へ展開し、2回目以降は キャッシュを再利用します。fingerprintは埋め込みアーカイブのコンテ ンツハッシュなので、`culebra`を再ビルドすると自動的に旧版のキャッ シュと分離されます。 機能軸が**丸ごと**リンクするサードパーティ製の静的ライブラリも、 同じ置き場に自分の名前で同居します: `Http`のOpenSSL(`libssl.a` / `libcrypto.a`)、`Canvas`のウィンドウバックエンドと`Scene`が使う vendored SDL3 / raylib(`libSDL3.a` / `libraylib.a`)。これらは ランタイムアーカイブの一部ではありません — `-l`で渡すと参照された 分しか引かれず、それでは目的を果たさないからです。かといってパスで 名指すこともできません: それぞれが置かれていたディレクトリは ドライバをビルドしたマシンのもの(ディストリのディレクトリ、 Homebrewのprefix、MSYS2のツリー、CIランナーの依存キャッシュ)で、 リンクが走るのはユーザーのマシンだからです。そこで各機能軸のリンク 断片はこれらを名前で書き(`@libssl.a@`)、`culebra build`が`cc`へ コマンドを渡す前に同じキャッシュへ解決します。素の`-l`のまま残すのは どのマシンにもあるもの — `-lz`・`-lstdc++`・`-lpthread`・OSの フレームワーク — だけです。断片にパスを焼き込むことはCMakeがconfigure 時に拒否し、`tools/checks/check_aot_link_portability.sh`が実際のリンク行を 読み返して1本も無いことを証明します。他のAOTテストはすべてビルド ツリーの中で走るので、焼き込まれたパスがたまたま存在してしまうためです。 `culebra build --target=`は現状ホストアーカイブのみです — 埋め込み/キャッシュされたアーカイブはホスト向けにビルドされています。 クロスターゲットには`--rt-lib=`で対応するarchiveを指定して ください(ターゲットごとのauto-buildはroadmap。手動クロスビルドの 手順は [§1](#1-standalone-バイナリビルドculebra-build) 参照)。