kanejaku.org

Tauri を試す - 2022/07/13

13 Jul 2022

少し前に Tauri という Electron 風ツールキットの 1.0 リリースが出た。WebView を使うので Chromium がくっついてこない。バイナリサイズが小さくて済みそうだ。開発体験はどんな感じかと以前 Electron で作ったツールを Tauri を使って書き直している。

ツールは単純なもので、ドラッグ&ドロップされたファイルの文字コードを変換するだけ。GUI 操作だけで日常作業を手早く済ませたいと要望を受けて作ったもの。単純だけど helloworld よりはやる事がある。手間取ったところいくつかがあるので書いておこうと思う。

まずはドラッグ&ドロップ。Electron だとWeb アプリと同じように drop を listen すれば良い。

const { ipcRenderer } = require("electron");
const dragEl = document.getElementById("drag-target");
dragEl.ondrop = event => {
  const files = event.dataTransfer.files;
  let paths = [];
  for (let i = 0; i < files.length; i++) {
    paths.push(files[i].path);
  }
  ipcRenderer.send("convert", paths);
};

Tauri では独自の API を使わないとドラッグ&ドロップを処理できない。

// dist/main.js
const tauri = window.__TAURI__;
tauri.event.listen("tauri://file-drop", (event) => {
  const paths = event.payload;
  tauri.invoke("convert", paths);
});

onFileDropEvent() を使ってもいい。

次は設定の保存。ウィンドウの位置やサイズ、元ファイルを上書きしたいか、などの情報を保存して次回起動時に復元したい。以下のステップを使って実現している:

  • 設定を保存し後で復元するための設定ファイルのパスを求め、
  • 実行中にウィンドウの位置やサイズの変更を検知しつつ、
  • WebView上で起きたイベント (<input> の値の変化) をホストプロセスへ通知、
  • アプリが終了する前に情報をファイルへ書き出す。

順番に見ていく。

設定ファイルのパス

Electron で作ったツールではホストプロセスから app.getPath("userData") を呼んで設定ファイルを保存するディレクトリを取得していた。Tauri は同等の API を公開していない様子。公開しているのかもしれないが自分がざっと見た感じでは見つけられなかった。

ホストプロセスは Rust で書かれたアプリケーションに過ぎないので Tauri が提供する API に縛られる必要はない。適当な crate を使って設定ファイルのパスを決めればいい。依存する crate を減らすために既に Tauri が依存している dirs-next を使う。

// src-tauri/src/main.rs
fn settings_path() -> std::io::Result<Option<PathBuf>> {
    let config_path = match dirs_next::config_dir() {
        Some(path) => path,
        None => return Ok(None),
    };
    let config_path = config_path.join("my-tool");
    fs::create_dir_all(&config_path)?;
    let settings_path = config_path.join("settings.json");
    Ok(Some(settings_path))
}

ウィンドウイベント処理

ウィンドウの位置やサイズの変更はホストプロセス、レンダラプロセスどちらからでも検出できる。

ホストプロセスでウィンドウイベントを検出する場合:

let settings = read_settings(settings_path())?;
let settings = Arc::new(Mutex::new(settings)); // (*) 後述
tauri::Builder::default()
  .on_window_event(move |global_event| match global_event.event() {
    tauri::WindowEvent::Moved(position) => {
      settings.lock().unwrap().window_position = Some(position.clone());
    }
    tauri::WindowEvent::Resized(size) => { /* (omit) */ }
    tauri::WindowEvent::Destroyed => {
      save_settings(settings.lock().unwrap().clone());
    }
    _ => (),
  })
  // ...

on_window_event()Send + Sync + 'static なクロージャを要求する。面倒くさいことは考えたくないので定番の Arc<Mutex<T>>settings を包む。

レンダラプロセスからは "tauri://move" を listen するか onMoved() あたりを使う。

WebView 状態の保存

WebView 上でのイベントはレンダラプロセスでしか拾えない。入力フィールドの値を永続化したい場合はレンダラプロセスからホストプロセスへ状態の変化を通知して、ホストプロセス側でその状態を維持する。 レンダラプロセスから emit() してホストプロセスで listen() する。

// dist/main.js
const el = document.getElementById("my-field");
el.addEventListener("change", _ => {
  tauri.event.emit("my-field-changed", el.value);
});
// src-tauri/src/main.rs
let settings_for_setup: Arc<Mutex<Settings>> = settings.clone();
tauri::Builder::default()
  // ...
  .setup(move |app| {
    let main_window = app.get_window("main").unwrap();
    main_window.listen("my-field-changed", move |event| {
      let my_field = event.payload().unwrap_or("").to_owned();
      settings_for_setup.lock().unwrap().my_field = my_field;
    });
  })
  // ...

起動時の状態の復元

二回目以降の起動ではウィンドウの位置とサイズ、WebView の状態を保存した設定ファイルから復元したい。Electron では BrowserWindow を作るコールバックで位置とサイズを設定し、did-finish-load イベントを受け取ったタイミングでページ内の状態を送っていた。これで期待通り動いていた。

Tarui で同じ挙動をさせるのは手間がかかった。

ウィンドウの復元から。最初は tauri::Builder::on_page_load() で位置とサイズを設定する方法を試した。こうするとウィンドウが初期値の位置とサイズで描画された後にウィンドウが移動してリサイズされてしまう。この挙動は受け入れられない。

次に試したのはアプリを起動する前に WindowConfig を上書きするというもの。

let mut context = tauri::generate_context!();
let mut window_config = &mut context.config_mut().tauri.windows[0];
if let Some(ref window_position) = settings.window_position {
  window_config.x = Some(window_position.x as f64);
  window_config.y = Some(window_position.y as f64);
}

手元の環境では挙動が定まらない。たまに期待通りの位置とサイズで表示されることもあるが、変な位置とサイズで表示されることもある。挙動に一貫性がなく、printf デバッグしても原因がつかめない。

結局今は以下のように実装している:

  1. 起動時にはウィンドウを非表示にして、
  2. tauri::Builder::setup() に渡したクロージャで位置とサイズを復元し、
  3. tauri::Window::show() を呼んでウィンドウを表示する。

次に WebView の状態を復元する。on_page_load() はこの用途でも使えなかった。on_page_load()emit() したイベントを WebView 側で listen() しようと試行錯誤したが上手くいかない。タイミングの問題だろうかと dist/main.js が読み込まれた直後に listen() してもイベントハンドラが呼ばれない。

今の実装では tauri::Builder::manage() を使っている。manage() を使うとアプリケーションに任意のデータ (Tauri では state と言っている) を紐づけることが出来る。以下のように settings をアプリケーションに紐づけて:

// src-tauri/src/main.rs
let shared_settings: Arc<Mutex<Settings>> = settings.clone();
tauri::Builder::default()
  // ...
  .manage(shared_settings)
  .invoke_handler(tauri::genrate_handler![get_my_field, /* ... */])
  // ...

WebView 側で設定を取得する。

// dist/main.js
const my_field = await tauri.invoke("get_my_field");

設定を取得するコマンドは以下のような感じ。

// src-tauri/src/main.rs
#[tauri::command]
fn get_my_field(settings: State<'_, Arc<Mutex<Settings>>>) -> String {
    settings.lock().unwrap().my_field
}

Electron は戸惑うことがほぼなく、作りたいものを素直に書けた印象がある。Tauri はちぐはぐ。挙動、設計、コード、ドキュメントのどれをとっても成熟の差が出ていると思った。

余談: この記事を書いている段階での最新版である 1.0.3 には MacOS でウィンドウのリサイズイベントが検出できないバグがある。安定版を名乗るには致命的な感じがあるが、そのうち直ると思う。

2022/06/24

24 Jun 2022

This site stopped using Google Fonts. FOUT is annoying. GDPR is another concern. This site stopped using Google analytics too.

I still believe that web fonts are valuable. I have a dictionary tool that is built on top of the Web technologies. It shows the IPA nicely thanks to web fonts. However, do they bring similar benefits at a cost of loading additional resource at this site and privacy implications? No.

Let’s prioritize readers.

帰省 - 2022/05/31

31 May 2022

便りがないのは無事な証拠、を地でいっているので実家には帰省する時ぐらいにしか連絡しない。年に一度くらいは親に元気にやっている姿を見せようと帰省してきた。

GW 明けだったら安価に帰れるだろうと高を括っていたが同じように考える人が多いのか飛行機の手配に少し手間取る。平日に移動すればチケットが取れそう。趣を変えて空港近くの都市部に滞在してみるのはどうだろうか。経由するだけだった場所をいつもと違う時間と目線で見るいい機会かもしれない。

遠い昔に屋台でラーメンを食べたことを思い出す。屋台の数は年々少なくなっているそうで、たしかに昔はもっと賑やかだったと思う。

会社の福利厚生の一部として申請できると聞いたので宿は良いところにした。

子供のころによく食べていた甘い食べ物たち。久しぶりに食べるとおいしい。

初夏は過ごしやすく、良い旅になった。

縁日 - 2022/05/11

11 May 2022

カタカタといつものようにキーボードを叩いていると外から笛の音が聞こえてきた。ドンドンと太鼓の音もする。換気のために開けていた窓を全開にして外を覗き込むと神輿を引く人々がみえた。

掲示板に大祭の案内が掲示されていたのを思い出す。少しづつ下町の夏が戻ってきている。

地元の子であろう元気な男子が「俺ポテトにするー」とはしゃぎながら駆け抜けていった。うなぎ文は台東区には根付いているよう。

Perfetto via Rust - 2022/04/28

28 Apr 2022

Perfetto は Android platform で使われている tracing フレームワーク。Android platform 以外でも単独でアプリケーションに組み込んで使うことも想定して作られていて、例えば Chrome が使っている。この Perfetto を Rust で書いたアプリケーションから使ってみようと試している。

現状のプロトタイプ実装で HTTP/1.1 のリクエストを投げてレスポンスを解釈する async タスクを以下のように書くと:

async fn h1_get_task(host: String, port: u16) -> anyhow::Result<()> {
    let track = Track::with_name(&host);
    trace_event!(CATEGORY_BENCHMARK, "Request", track, EventType::SliceBegin);

    let mut client = Client::connect(host.clone(), port, track).await?;
    client.send_request().await?;
    let response = client.recv_response().await?;

    trace_event!(
        CATEGORY_BENCHMARK,
        "Request",
        track,
        EventType::SliceEnd,
        |event| {
            trace_event_set_string!("host", &host);
            trace_event_set_string!("status", response.status().as_ptr());
        }
    );

    Ok(())
}

struct Client { /* ... */ }

impl Client {
    async fn connect(host: String, port: u16, track: Track) -> Result<Self> {
        let _s = trace_event_span!(CATEGORY_BENCHMARK, "Connect", track);
        // ...
    }
    async fn send_request(&mut self) -> Result<()> {
        let _s = trace_event_span!(CATEGORY_BENCHMARK, "Send", self.track);
        // ...
    }
    async fn recv_response(&mut self) -> Result<Response> {
        let _s = trace_event_span!(CATEGORY_BENCHMARK, "Recv", self.track);
        loop {
            match self.state {
                State::ReadingHeaders => self.read_headers().await?,
                State::ReadingBody => self.read_body().await?,
                State::Done => break,
            }
        }
        // ...
    }
    async fn read_headers(&mut self) -> Result<()> {
        let _s = trace_event_span!(CATEGORY_BENCHMARK, "ReadHeaders", self.track);
        // ...
    }
    async fn read_body(&mut self) -> Result<()> {
        let _s = trace_event_span!(CATEGORY_BENCHMARK, "ReadBody", self.track);
        // ...
    }
}

こんな感じのトレースが得られる。

trace_event!trace_event_span! が C++ の TRACE_EVENT()に相当する。スコープを扱うのに _s に代入しているのはいびつだけれど、これは Attribute macros で対処できる目途が立っている。

Perfetto には track という概念がある。分散環境を意識した用語でいえば span と同等のもの。 Android での tracing を主な用途とする Perfetto では TRACE_EVENT() で記録される event はデフォルトでスレッドと1:1に対応する track に紐づけられる。

一方 Async Rust では .await の前後でスレッドが異なるエコシステム (tokio) で trace を記録したいことがほとんど。 C++ と同じ感じで track を省略して書くと UI が span を期待通りに表示してくれない。プロトタイプでは明示的に track を要求するようにしている。似た様な問題意識を持つ tracing crate はこのあたりを隠蔽しようと悪戦苦闘している様子だが見込みは薄そう。

Event を引数付きで記録したい場合は event にメタデータを付与するコールバックを渡す。TRACE_EVENT() のように記録したいパラメータを inline で渡せるマクロを提供できるかもしれないが、自分はマクロに詳しくなりたいわけではないので深掘りしていない。

実用には至っていない。困っている点を二つ羅列しておく。

  • 動的に決まる key-value ペア (HTTP response headers とか) を trace event として記録する方法が分からない。Perfetto はオーバーヘッドを減らすべく key を internalize する。Internalize するときに使われるのは key のアドレスのようだ。つまり key は静的にアロケートされているか、動的に確保されたときは同じアドレスが使われていてはいけない。スタックに一時的に key を保存するようなナイーブなやり方だとこの前提を崩してしまい、期待した動きにならない。
  • TRACE_EVENT()perfetto::TracedValue を引数に受け取る lambda を受け付けるように読めるが、マクロを使わずに TraceForCategory() を直に叩くとコンパイルに失敗する。これは単純に自分の C++ 力が足りないだけ。

仕様を読む - 2022/03/02

2 Mar 2022

一月の下旬に流行りものをもらって床に臥せったあと復帰して忙しく過ごしていたらもう三月になっていた。

このところ仕様を読む機会が増えた。

QUIC 関連の RFC は長いこと議論した後に最近標準化が完了したというのもあって読み進めるのにあまり苦労しなかった。一方 living standard を謳う後者は読むたびに「うーん」という感想を抱くことが多く雑念が入りがちであった。


これらの仕様は複雑に思えるという意見を見聞きした。曰くプロトコルやアプリケーションはもっとシンプルに作れるのではないか。牧歌的な古き良き時代と同じコンテキストであればそれはまっとうな批評だと思う。けれどウェブとインターネットは進化しあらゆる人が依存する本当の意味でのインフラとなった。昔の価値観を持ってナイーブな批判をするのは思慮が足りない。Jana Iyengar の言葉を借りれば Thse are as simple as the modern internet demands, which is not very simple in absolute terms といったところ。

HTTP/1.1 を喋る

16 Jan 2022

世の中ほぼ HTTP/2 以降に移行しただろうし、自分で試した感じ問題なさそうと自前の HTTP/2 サーバでこのサイトを運用し始めて数週間経過した。しばらくログを眺めていて気付いた。ほとんどの検索エンジンやフィードを巡回する bot はいまだ HTTP/1.1 しか喋らない。

googlebot は HTTP/2 に対応します、という記事が出たのは2020年だった。進捗は芳しくないよう。少なくともこのサイトの巡回においてはまだ HTTP/1.1 を使っているようだ。

検索はともかくフィードは機能させたいので HTTP/1.1 も理解するようにサーバに手を入れた。雑な実装なので上手く動いていないかもしれない。

5 PM - 2022/01/14

14 Jan 2022

ベランダから望むトーキョー開発。

2022年 正月休み

4 Jan 2022

去年に引き続き今年も東京で新年を迎えた。来年は実家へ帰省したいなあ。

今回の年越しは地域密着でいこうと、近所のお蕎麦屋さんに年越しそばをお願いしておいた。そばを受け取ったあと餅を和菓子屋さんで買い求めた。自宅でそばを茹でる。

一升餅。

おせちはお取り寄せ。雑煮とぜんざいを食べる。

年末に Intel NUC を買ってコードを書くのに使っている。Ubuntu 21.10 は UI がこなれていて不満がない。Windows を使う頻度が減った。

積読を消化するべく Web配信の技術 に手をつけ HTTP キャッシュへの理解を進める。Practical MonitoringLinux Observability with BPF にも目を通したがこれらは今の所冷やかし止まり。

2021年の振り返り

31 Dec 2021

年始の抱負には沿わない一年だったが振り返ると悪くないで年あった。

趣味コーディング

今年の 2 月あたりまでスーパーファミコンのサウンドチップ SPC 700 のエミュレータを書いていた。React で UI を書き、作業用 BGM を流す PWA にするところまで作った。夏のある夕暮れ時に上野公園を散歩しながら Smiles and Tears を作ったアプリで聞いたときは思い出補正も相まって結構感動してしまった。

春先から夏まで HTTP/2 サーバを書いていた。一通り動くところまで書いた後しばらく放置していた。時間がある年末に実用できるまで持っていこうと再度書き直している。年内に一区切りつけたかったのだけど間に合わなかった。

そのほかツールを細々と整備している。自分用の辞書アプリを Actix から tokio を使った質素な自前の HTTP/1.1 サーバへと移行した。Actix はコンパイルにやたらと時間がかかっていて(たぶんmiddlewareを自作していたから)、ちょっとした改修が億劫になっていた。この移行で修正が気楽にできるようになり満足している。今年はサーバを Rust async で二つ書いたので作例から一歩踏み込んだ理解が得られたと思う。

昔書いた NES エミュレータに音源を可視化する機能をつけてツイートしたところ、プチバズりする経験もした。

Write Code Every Day を実践した年でもある。数年前にも一度やった気がする。やって良かったかと振り返ると、良くなかった。頭を使う作業よりも自明な変更を優先しがちになる。難しいこと考えたり理解したりする時間が減ってしまい成長を阻害していた感覚がある。来年は毎日コードを書くのにはこだわらないようにしよう。

総じて普段使いの自作ツールの完成度を上げれて良い感じの一年であった。

読書

読み終えたと言えそうな本のリストだけ。積読が溜まる一方でどうにかしたい。簡単な感想も書こうと思っていたが力尽きてしまった。

クリエイティブ

模倣ではない何かをやりたい。オリジナルのドット絵を描きたい。DAWを使って曲を作りたい。昨年末にそんな思いを持っていたのだけれど自分には難しそうだ。

自分は出るか出ないか分からないもの(=オリジナリティのある何か)への時間の投資よりも、期待値が一定数あるもの(=誰がやっても似た様な物になる何か)に時間を割きがちなのを改めて認識した。幻想を追うのをやめて個性だと受け止める。

ブログ

春先から更新できず。元々文章を書くのは得意ではない上に、仕事で文章を書く頻度が増えたのでブログを書く意欲が湧かなかった。気が向いたら書く、ぐらいを維持していきたい。

仕事

熱心に働いた。会社の評価はふつうとのこと。

年初めに違うチームへ移動した。ちょっとした移動なので所属に変化はなく、関わるプロジェクトがネットワーク寄りになった感じ。

Early Hints の実装が主な仕事だった。design → implementation → experiment と通しで主導できたのは楽しかった。外部の開発者とのミーティングやメールでのやり取りが増え、新たに学ぶことが多かった。Cloudflare のブログエントリが出るときは冷や汗をかく折衝をしたのもいい思い出になっている。この時は PM のみなさんに大変お世話になった。なお、このページでも試験的に Early Hints を送って css とフォントを preload している(はず、動いていれば)。クライアント側もサーバ側も書いて動作を確認できたのはちょっぴり嬉しい。

後半は WebTransport の ship に向けた作業のお手伝いをやっていた。主にやっていたのは Web Platform Tests というブラウザ共通のテストのインフラの整備。合意形成に時間をとられる感じであった(#85, #96)。無駄に WPT のインフラに詳しくなった。

組織再編による棚から牡丹餅感はあるものの //net の owner になれたは素直に嬉しかった。

Early Hints 以外の今年書いたコードは細々としたものが多い。来年は自明じゃないコードを書けると良いなあ。

ほぼ100%リモートワークで働いた。会社には2-3回行ったくらい。リモートワークは慣れると生産性高く仕事できる気がする。でも会社がまともに開いたらやっぱり会社で働くのがいい、と思うかもしれない。来年どう思うか。

趣味の延長である Chromium Mojom Language Server を VSCode Marketplace に置けるようになったと聞き及んだので公開した。

主業務以外だと採用関連も少しお手伝いしている。インターン向けの採用委員会があり、そこで細々とした活動をしている。時間をとられて大変ではあるものの、学ぶことが多く良い経験をさせてもらっている。

英語

単純な技術話題を読んだり書いたり聞いたりするのはもう困らない感覚がある。

Hacker News や Reddit とかにありがちな情緒を含んだ表現を使われると分からなくなるけれど DeepL にかければ理解できる。新聞やニュースを英語で理解したい欲求は自分にはそんなに無いので英語での入力には問題を感じなくなってきた。

話すのと書くのはまだ足りてないので引き続き頑張る。

運動

去年よりさらに運動量が増えた。朝の散歩を一部ジョギングに変えたのと、Fit Boxing 2 を春先に買ったのでそれを日課としてやっている。距離換算で 7600 km は我ながら動きすぎだなと思った。

来年の抱負

なし。年末に抱負を書いても毎回未達なので抱負を立てるより日々の充足を追うことにする。