開発環境の準備
開発用コンテナの起動
docker run -it --rm --workdir /work -p 0.0.0.0:7681:7681 rust:1.77.0-slim-bookworm bash
WORKDIR=/work
ttyd のソースコード取得
git インストール。
apt update
apt install -y git
ttyd のリポジトリを clone.
cd $WORKDIR
git clone --depth 1 -b 1.7.5 https://github.com/tsl0922/ttyd
プロジェクトディレクトリへ移動。
cd $WORKDIR/ttyd
ttyd をライブラリとしてビルド
ttyd の README を参照しながら、ビルドに必要なパッケージをインストールする。
apt install -y build-essential cmake git libjson-c-dev libwebsockets-dev
前回の apt install
と重複があるが、気にせず ttyd の
README からそのままコピペ。
CMakeLists.txt
を修正
実行バイナリではなく、 .a
を生成するように
CMakeLists.txt
を修正する。
sed -i -e 's/add_executable(${PROJECT_NAME} ${SOURCE_FILES})/add_library(${PROJECT_NAME} STATIC ${SOURCE_FILES})/' ./CMakeLists.txt
add_executable
: 実行バイナリを生成するadd_library
:.a
を生成する
関数 main
の名前変更
別プログラムに組み込みたいので、エントリーポイントである
main
が存在すると困る。
なので、 main
を ttyd_main
へリネームする。
sed -i -e 's/main(/ttyd_main(/' ./src/server.c
別プログラムから、 ttyd_main(2, ["ttyd", "-W", "bash"])
のような感じで呼び出すイメージ。
ttyd のビルド
mkdir build && cd build
cmake ..
make
これで、 libttyd.a
が生成される。
ttyd を組み込む Rust プログラムを作る
ttydwrapper
というプロジェクトを作って、 固定値で
ttyd -W -t enableSixel=true bash
コマンドを実行するのと同じように ttyd を呼び出すプログラムを作る。
bindgen に必要なパッケージのインストール
C 言語との FFI をするにあたり、 bindgen
というコマンドを利用するので、それに必要なパッケージをインストールする。
apt update
apt install -y libclang-16-dev
Cargo プロジェクトの作成
今回は、 ttydwrapper
という名前で作成。
cd $WORKDIR
cargo new ttydwrapper
cd ttydwrapper
rustup component add rustfmt
FFI 用のヘッダーファイルを作成
Rust から C の ttyd_main
を呼び出すので、
ttyd_main
の定義が記載されているヘッダーファイルを作成する。
Rust では、一般的に、 binding.h
に必要なヘッダーファイルを列挙するらしい。
なので今回は、 ttyd.h
に ttyd_main
を定義し、そのヘッダーファイルを binding.h
へ記載する構成でヘッダーを作成した。
mkdir include
cat << EOF > ./include/ttyd.h
void ttyd_main(int argc, char **argv);
EOF
cat << EOF > ./include/binding.h
#include "ttyd.h"
EOF
バインディングを生成
bindgen
コマンドで、ヘッダーから Rust
のコードを生成する。
cargo install bindgen-cli
bindgen ./include/binding.h > ./src/bindings.rs
以下のような、 binding.rs
が生成される。
/* automatically generated by rust-bindgen 0.69.4 */
extern "C" {
pub fn ttyd_main(argc: ::std::os::raw::c_int, argv: *mut *mut ::std::os::raw::c_char);
}
生成された binding.rs
を lib.rs
からインクルードさせるのが一般的らしいので libs.rs
を作る。
cat << EOF > ./src/lib.rs
include!("bindings.rs");
EOF
ライブラリをコピー
ライブラリは lib
以下に無いとダメなので .a
を移動する。
mkdir $WORKDIR/ttydwrapper/lib
cp $WORKDIR/ttyd/build/libttyd.a $WORKDIR/ttydwrapper/lib/
ライブラリの情報を作成
build.rs
にビルド時に必要なライブラリ情報の列挙をする。
cat << EOF > ./build.rs
fn main() {
println!("cargo:rustc-link-search=native=$WORKDIR/ttydwrapper/lib");
println!("cargo:rustc-link-search=native=/usr/lib/x86_64-linux-gnu");
println!("cargo:rustc-link-lib=static=ttyd");
println!("cargo:rustc-link-lib=dylib=uv");
println!("cargo:rustc-link-lib=dylib=websockets");
println!("cargo:rustc-link-lib=static=ssl");
println!("cargo:rustc-link-lib=static=crypto");
println!("cargo:rustc-link-lib=static=z");
println!("cargo:rustc-link-lib=static=json-c");
}
EOF
ビルド時に
build.rs
の設定を使用するように修正
Cargo.toml
の [package]
の末尾に
build = "build.rs"
を追加する。
(sed で実現できなかったので vim で編集した)
main 関数の作成
Rust の main 関数を作成し、 ttyd_main
を呼び出すプログラムを作る。
cat << EOF > ./src/main.rs
use std::ffi::CString;
use std::os::raw::c_char;
use ttydwrapper::ttyd_main;
fn main() {
let args = vec!["tty", "-W", "-t", "enableSixel=true", "bash"];
let c_args: Vec<CString> = args.into_iter()
.map(|arg| CString::new(arg).expect("CString::new failed"))
.collect();
let mut argv: Vec<*mut c_char> = c_args.iter()
.map(|arg| arg.as_ptr() as *mut c_char)
.collect();
let argv_ptr: *mut *mut c_char = argv.as_mut_ptr();
unsafe {
ttyd_main(5, argv_ptr);
}
}
EOF
ビルド
cargo build
これで ./target/debug/ttydwrapper
が生成される。
動作確認
コンテナ起動時に 7681
をポートフォワーディングしているので、 ttydwrapper
を実行した後 http://localhost:7681
へアクセスすると ttyd
のターミナルが開く。
以上。
ここまで書いたところで、本当にやりたいのは Windows でだった事に気付いた…。