catalinaの備忘録

ソフトウェアやハードウェアの備忘録。後で逆引きできるように。

キャリアと本当にやりたいこと

ここ最近はAIにコードを書かせていて、ネタが枯渇気味です。かたりぃなです。

技術ブログとして何を書き残したいか

ここ一年くらい悩んでいたのですが、書き残すことがないなと思っています。 後述するようにAIによって調査や実験フェーズが簡略化されたので、「こんな面白いのあるんだー」みたいな感動をすっとばして「完成しました」になってしまって、そうするとなんかこう書くことないなー。っとなってしまうわけです。

やってみた/作ってみたネタでいくとしても、「AIにこう指示すればできた」はわざわざ記事にするまでもないですし。 そんなことしているうちにちょっとキャリアについても考えてしまったというのが今回のネタになります。

キャリアについて思うこと

組み込みを10年くらい、Webを10年くらいといったハイブリッド型とでもいいましょうか。 中年の危機なのか飽きたのか、なんかこう定年までずっとこのままでいいのかなと漠然とした不安に苛まれることがあります。

いろいろ経験してきてわかったのは、自分はモノづくりが好きなんだなということです。 で、単にモノづくりと言っても幅広いですが、いわゆるエンドユーザーまで届くタイプのモノを作っているときが一番楽しかった気がします。家電だったり、UGCサイトだったりの開発ですね。

逆にBtoBや運用はあまり面白いとは思いませんでした。

言語そのものとか技術そのものがどうこうではなく、プロダクトそのものが完成していくことが楽しいんですよね。

興味の方向性

明確な方向性が示されていて、その方向性の根拠がしっかりしていると安心して仕事できる性分なんですよね。 たとえば小さなところだと「電気通信規格でこう定められているから」とか、もう少し広い範囲でいくと「データ分析の結果こうすることで顧客体験が向上し云々が見込めるからUI変更する」とかですね。

これとは逆にステークホルダーが多いタイプでは興味がわく以前にもっと別の感情が湧いてきてしまいますね。 「A社とB社の連携システムで制約があって、仕方なくこうしている」「部署間のシステム連携で依頼元の都合でこうなっている」とかですね。

仕事だから処理はしますが、私の視点としては後者って「会社間や部署間の政治争いとか、当時の事情とかあったかな?」くらいに見えるわけです。

もちろん、方向性が定まっていない場合も「こうすればうまくいくかも」みたいなのがあるわけです。 一人で技術的に解決できる問題なら試行錯誤を繰り返すことができるのですが、人が絡む問題解決はともう面倒というかやりたくはありませんね。

なんといいますか、社会には色々な考えをもった人がいらっしゃって、まずはどういう状況なのかとか事情をヒアリングとかなるわけですが、そう単純にいかないこともあったりして、うっ。頭が……。嫌な事件だったね。

なので、身も蓋もないことを言ってしまうと、組み込み的な仕事のほうが向いているのかもなと思っていたりもします。少し幅を広げてもtoCのバックエンドとかでしょうか。

人間よりも機械相手にしてるほうがラクだよなと最近特に思うようになってきています。なんというか、機械やプログラムは自分がコントロールできる範囲だってわかってるので。

人間は思い通りにはコントロールできませんから。

本当にやりたいこと

同人活動をして色々発表しているのですが、今の自分はこれが一番たのしいです。

ラフレーズ工房@C108 日曜 南 g25b (@lafratelier) / X

ソフトウェアとは少し毛色が違ったものづくりで、3Dモデリングと3DプリンタやCNCフライス、デジタル加工機械、LEDやギミックなどを組み合わせてコスプレといった世界で表現の幅を広げたいなと試行錯誤しています。

これらはいわゆるフィジカルな演出というものですね。

コスプレという遊びは、一般的に写真を撮って加工してSNSで共有したり写真集を作るという体験が主です。

自分が求めているのはそれだけではなくて、アトラクションとしてその場で楽しめるヒーローショーみたいなものです。

そういったことをだれでも手軽にできたらいいなという夢ですね。HololensとかのARを試していたのもこういうのが目的です。

やりたいことを仕事にすればいいのでは?(1)

最近はフィジカルAIみたいな単語も出てきて、「もしかしたら自分の強みを活かせるかも」くらいには思っていますが、そこまで積極的に動きたいなとは思わないですね。 これは自分自身が一番驚いています。歳とったなぁと。

若いころなら無茶を承知で活動していったかもしれませんが、社会や会社の仕組みみたいなのを知っていくと、なんかこう諦めというのが先に来ちゃうんですよね。

ちょっと長いですが語ってみましょうか。

やりたいことを仕事にしないほうがいい

ゲームのプログラムを作りたくてゲーム会社で働いていたことがありました。

会社での事業活動なので、複数人で分担して各々自分の業務範囲をこなしていくことになりますが、 「どうしてこんなxxにしてるのだろう?」「こっちのほうがいいのでは?」みたいな思いが出てくるわけですね。

どのような仕事であってもこのような感情は湧いてくるものだろうと今であれば理解はできます。 ただ、好きなこと・やりたいことを仕事にしてしまうと、この感情が自分を押しつぶしに来てるような感覚がありました。

そういう感情や意見が出てくるとき、それは他人の作業領域です。自分の作業領域だったら「こういうのどうですか」と堂々と上長に意見すれば済むだけですから。

自分の作業領域外はどうにもならないことが多いんですよね。基本的な社内政治というか組織のルールですね。

組織での立ち回りってどうするべきなのかわからず悩んだり失敗したりを繰り返していましたが、ある日「あ、このままではゲームが嫌いになる」と気づき、その業界を離れることにしました。

やりたいことを仕事にすればいいのでは?(2)

といった経験から、「やりたいことを仕事にするのではなくて、領域が少し被っている程度まで離れたところで仕事をする」が妥当な解決かなと今では思っています。

プログラマを続けているのもそういう理由です。

やりたいことと仕事の領域がかぶる範囲が広いとそれは熱意につながると思います。ただし、熱意をそのまま組織で活かせるかどうかは別の話です。

組織で働くというのは未だに慣れないですし、なぜフリーにならないのかと聞かれたら「厚生年金のため」といった身も蓋もない答え程度しか持ち合わせていません。 組織での動きが得意な人というのは何人も見てきたので、そういうのは得意な人に任せておいて、自分は自分の作業範囲を確実にこなしつつ可能なら+アルファくらいで充分だなと今は思うようになっています。

そういう働き方は静かな退職では?

これも自問自答して悩んでいたのですが、そうかもしれません。

ただ、静かな退職と違うなと思っているのは、自分は次のようなことを考えていて、何かこう変えたいなとおぼろげに思っているところですかね。

  • 自分の好きは何か
  • 仕事と好きの共通点、どのくらいの範囲がかぶっているか
  • 仕事と趣味お互いにいい感じに活かせることはないか
  • 時代の流れとして次どうするか

静かな退職では「最低限のみ」と割り切るようですが、私はそういうのが苦手らしく「どうにかすれば突破口があるかもしれない」という淡い希望を抱いてしまうんですね。これまで何度もそうして打ち砕かれてきたのに、学習能力が低いですね。

現実との折り合い

自分の性格的に組み込み系へ戻ることも考えましたが、待遇面が納得できないんですよね。 汚い言い方をするとその多くが労働集約型になっているので、最近のトレンドである賃上げが困難な業界構造になっています。

待遇を上げるには多重下請け構造のできるだけ上に行く必要がありますが、転職でそこまでするリスクある行動をとって、結果が見合うかというと、疑問符ですね。

そんなこんなで、なんかこう閉塞感を感じるミドルエイジクライシスっぽい内容ですが、やっぱり趣味は楽しいです。

趣味のいいところは「決定権も責任もすべて自分にある」というところです。

世渡りとか人付き合いとかが苦手な自分としては、これが最も良いポイントですね。

すぐ転職とかじゃなくても、自分のやりたいことをやっていたら自然とスキルや知識は積みあがっていくと思っているので、マイペースでやっていきたいなと思っています。

それでは今回はこれくらいで。

モナドを学ぶ

CDNがunwrap()でpanicして落ちたみたいな話を耳にしました。 直接は関係ないのですが、エラーハンドリングって難しいなと思いなおしていたところです。 本業でPHPを書くことがあるのですが、エラーハンドリングまわりを毎度「もっとスマートに書けないかなぁ」って思ってたりします。

そんな時、Rustのunwrap()の話をみて「そういえばResultって、どこかで見たことある。。。なんだっけ。Rust勉強し始めるもっと前、数年前見た気がする」となりました。 調べていくとモナドにぶち当たった次第です。あらためて勉強しなおした記録を残したいと思います。 関数型はそこまで慣れ親しんでるわけではないので誤りがあるかもしれない点、ご注意ください。

Resultとunwrapって?

たとえばC言語とかでこういうコードがあったとします。

char* buffer = (char*)malloc(4096);
if(!buffer){
    exit(-1);
}

メモリ確保できなかったら終わるってやつですね。 Rust風にかくと

let r = my_malloc();
match (r){
    Ok(buffer) => /* 後続の処理 */,
    Err(e) => panic!(e),
}

でしょうか。 こういうエラー処理はunwrapで省略できて

let buffer = my_malloc().unwrap();

と書けます。 失敗したらpanicしてくれってシンプルに書けるのでしたね。

ちなみに、panicではなくreturnして呼び出し元でハンドリングするなら

let buffer = my_malloc()?;

ですね。

OptionやResultはモナド

関数型プログラミングを学習していると出てくるやつです。 はじめてモナドを学ぼうとして挫折したときの説明は多分こんな感じでした。 「モナドの代表であるMaybeについて学びましょう。値があるかもしれない・ないかもしれない、まるでシュレーディンガーの猫です」

「何言ってんだ、こいつ」という気持ちでした。オブジェクト指向に慣れていた自分からすれば「いいからモナドクラスを定義してください」という気持ちです。 定義を読むとモナド則とか出てくるのですが、さっぱりでした。関数型で書かれた例もさっぱり読めないので詰んでました。本は今でも積んでます。

ここで、「値があるかもしれない、ないかもしれない」というものはRustで組み込み型で定義されています。 OptionとResultですね。少し前に学んだScalaでいうEitherも同様ですね。 これらがモナドかどうかは置いといて、どういうものかを復習してみます。

Optionの使い方

Optionは値があればSome,なければNoneを表し、Someの場合は値を取り出して使えます。

let val1 = Some(10);
let val2 = None;
match (val1){
    Some(v) => {println!("value1={:?}", v);},
    _       => {println!("None");},
}
match (val2){
    Some(v) => {println!("value2={:?}", v);},
    _       => {println!("None");},
}

Resultと同じような感じですね。

Optionはモナド

さて、数年前に倒せなかったモナド。今こそ倒せそうです。

説明をわかりやすくするためオブジェクト指向的用語で立ち向かいます。正確性に欠けますが、それは理解が進んでからでいいでしょう。

モナドはpureとbindというメソッドをもつオブジェクトと解釈しましょう。 pureはモナドに値を格納するためのコンストラクタです。格納する値はなんでもいいとしましょう。たとえばT型。 bindはモナド的結合といって、オブジェクト指向の世界でいうメソッドチェーン的なやつでしょうか。Rustではand_then()が相当します。 (少し話がそれますが、MS C++CXでのPPLTaskのthenっぽいですね。。。実際プロミスもそういうことらしいのですが、またの機会に。)

というわけで簡単な例をrustで書いてみました。

// モナドの検証用関数
fn i32_divide(x:i32, y:i32) -> Option<i32> {
    if y == 0 {
        None
    } else{
        Some(x/y)
    }
}

// モナドはコンストラクタのように値に文脈を付与できる
// モナドはメソッドチェーンのようにand_thenで計算グラフを記述できる
fn monad_example() -> () {
    let val = Some(2);
    let r = val
    .and_then(|x|i32_divide(x,0) )   // Noneになる
    .and_then(|x|i32_divide(x,5) );  // この計算は実行されない
    println!("monad result = {:?}", r);
}

最初のvalを作る時点で明示的にSome(2)としていますが、実際のコードではこの値はどこかからもらってくるかもしれません。本エントリの最初でふれたCDNのpanicの話では設定ファイルから読み込んでくる(実際にはもっと細かい話)というわけですね。 そうしたとき、valがOption型であるというのは「値があるかもしれない・ないかもしれない」というMaybeモナドの特性をもっています。

このコードではモナドに対して計算を試みています。 普通にand_thenなしでi32_divide(val,0)を書くとエラーになってしまいます。実装上はOption型に対して四則演算はできないからです。 「値があるかもしれない・ないかもしれない」というコンテキスト文脈を維持したいので、「文脈を維持して処理をしていくよ」というのがand_thenです。 コメントにあるよう、最初の計算でNoneになってしまうので、and_thenが良い感じに早期リターンして後続の処理を実行しないということになります。

で、モナドの何が嬉しいの?

ここで重要なのは - 「値があるかもしれない・ないかもしれない」という文脈を維持できる - 文脈を維持したまま「計算する」というコードを自然に書ける点です。

たとえば手続き的にこんな感じのコードよく見かけると思います。C++です。

auto val = get_config(x);
if(!val)    return;

auto val2 = get_param(val, param_list);
if(!val2)    return;

if(!validate(val1, val2))   return;

process(val1, val2);

本当にやりたいことは値をとってきて、計算して、結果を返すだけのはずなのに、やたらと条件が多いですね。 これが先ほどのrustの例のようにとてもシンプルに記述できるのです。

C++ならexceptionという便利な仕組みがあってですね?みたいな話もありますが、今は置いておきます。

モナドモナドであるために

昔倒せずにセーブポイントまで戻るというか、投げ捨てて積読化してしまったモナド則をみてみましょう。

  • 左単位
  • 右単位
  • 結合

pure(コンストラクタ)とbind(メソッドチェーン)相当のものが実装されていて、これを満たしていればモナドの特性を持っているといえるらしいです。 この法則は数学の、圏論とか群論の用語からきているらしいです。

当時これらは全然意味が分かりませんでした。ネガティブなイメージからアレルギーまで生まれてた気がします。当時は挫折しましたが、それから数年、レベルアップして得た群論レベルの知識+αでなんとか分かった気がします。

こちらのサイトが理解の助けになりました。ありがとうございます。 https://zenn.dev/funnycat/articles/e1fa00530ea884 https://zenn.dev/funnycat/articles/d92e16dfc59a49

ここでモナド則を理解するときに自分が躓いたポイントがあります。 モナド則はモナドについて言及していて、モナドの中の数値については言及していないという点です。

何かというと、RustでいうとResultやOptionがモナドであるためにこうだよというのを述べているのであって、その中に入っている具体的な型に対しては無関係ということです。 抽象的に考える必要があるのですが、重要なのは中の型は関係なくて、Result<T>モナド則を満たしていれば、Result<T>モナドと言えると理解しました。 たとえばResult<i32>としたとき、Resultの部分がモナド則を満たす必要があって、i32はがモナド則を満たすかどうかはまた別の話。と理解しました。

すなわち、Result<T>型やOption<T>型こそが、関数型プログラミングHaskell入門で挫折させてくれたMaybeモナドさんです。

説明で具体的な数値を出してくれているサイトが多いですが、その数値はモナド全体を表現しているもので、モナドの中に入っている数値を表しているわけではないのだと。

長かった。。。。

モナドだけでは何もできなくない?

コンストラクタでモナドが作れて、and_thenで連続した計算ができるだけで、正直何がうれしいのかよくわかりません。OOP的にみると、ただのバリデーション機能つきDTOです。ここで一つ仮説を思いつきました。

OOPでいうただのバリデーション機能付きDTOだとすると、データを受け渡すことにしか使えない。 実際にそれを利用するとき、もっといろいろあるのでは?

と。

調べていくと、いろいろなことがわかりました。 雑にいうと、モナドOOPでいうところの継承グラフの末端と考えたとき、モナドクラスの親クラスっぽいやつがいるみたいです。(厳密には親クラスではなくて云々ですが、一旦おいときます。) モナドの親クラスはアプリカティブです。 アプリカティブも親クラスを持っていて、親クラスはファンクタです。

今回は機能面でのみ述べていて、一旦 モナド -> アプリカティブ -> ファンクタ としておきましょう。

ちょっと理解が追いつけてないので、このあたりの話はもう少し理解が進んでから試したいと思います。

あとモナドにもさまざまな種類があるみたいです。状態遷移を表現するStateモナドとか。まるでOOPデザインパターンみたいな感じですね。

モナドの中にあるもの

ここで色々なモナドをみていくと、その中身の値についても面白いことがわかりました。

まだ理解が追いついていないので詳しくは書けませんが、関数型プログラミングではこのあたりをうまい感じにやりくりする方法が「代数的構造」みたいです。 半群とかモノイドとかですね。

モノイドをみていくと、畳み込み(fold)の話がよくわかるというか、初期値どうしよう?みたいな迷いが減りそうです。 さらに詳しく追っていくとカリー化につながりそうです。

感想と今後の予定

モナドと少しだけ仲良くなれました。 まだまだ分からないことだらけで勘違いが入っているかもしれませんが、ゆっくり進みたいと思います。

今すぐ関数型に移行とかそういうのは考えていませんが、AIにコード書かせたときに関数型のほうが読みやすいコードを書いてくれてる気がします。 少なくとも考え方の1つとして知っておいて、状況によって切り替えられるといいかもしれないと思っています。

特に最近はAIにコード書かせることが多いのですが「うせやろ?」みたいなコードを出してくることがあります。 Reactの画面表示の更新管理にフラグ変数を別途設けたとか、WebAPIが状態を持ってたりとか。いわゆるアンチパターンかましてくるわけです。 どうとでも書けちゃう言語だから、その場しのぎ的なコード吐くのはまあAIに限らず人間でもやりがちというかなんというか。(遠い目)

AIでコード書く機会が増えていく中で、関数型の知識はとても強い武器になると思っています。 まだしばらくは動作検証は人間の仕事になるでしょうから、関数型で書かせて、テストや結果を見直すという作業が主流になるのかなと思っています。 実際、私の趣味プロジェクトでもブラウザぽちぽちいじってデバッグしかしてない気がします。

そのさらに未来としてはAIに命令したらいい感じのプログラムが完成すると思いますが、そのときも関数型言語の理解が助けになるのではと考えています。 大昔R言語を勉強したとき「forで回すのではなくて、本当にやりたいこと(畳み込みとか)をapply系に渡すんだ」と教わりました。 それと同じことがAI時代にも活きるのかなと思ってます。「細かなロジックではなく、本当にやりたいことを指示するのだ」みたいな。 CASEツールみたいに終わるのか、それとも進化していくのか等は未知数ですが、新しい知識を習得するのは楽しいものですから、マイペースで楽しみながらやっていきたいと思います。

それでは今回はこれくらいで。 あ、よいお年を!

ClaudeCodeでのアプリ開発

AIでどれくらいコーディングが楽になるのか、過去に作ったARアプリの焼き直し的なことをしてみてどんな感じでAIが使えるかを試してみました。かたりぃなです。

まずはClaude Codeで「ARアプリ作ろうぜ」から初めて、それなりに動くようなものができました。proプラン、月額三千円くらいの価値はあるかとおもいます。

細かなコード修正は自分でやりますが、基本の実装はAIにしてもらいました。

今回はAIを使っていくうで何となく見えてきたことがあるので記事として残しておこうと思います。

 

ちなみに作った機能はこんな感じです。

  • カメラ画像から、カードゲームのカードを認識する
  • 認識したカードの位置と姿勢を推定する
  • 認識したカードが何であるかを分類する
  • カードの位置と姿勢をもとに分類結果に応じた3Dモデルをレンダリングする

ちなみに表示している3Dモデルは、管理画面から登録できるようになっています。

管理画面はこのようになっていて、gltfファイルをユーザーが自由に登録できる仕組みです。

 

さて、AIを使ってここまで作るだけでも結構大変だったので、知見をメモしておこうとおもいます。

 

条件や指示は明確に

これらが不明確だと、期待した実装が得られないことが多い印象です。また条件を付ける場合はその分野の言葉を使ったほうが良い結果が得られやすいと感じました。

たとえば「問題領域は特定物体認識です」とか「分類問題におけるラベルの数は既知です。高々500~1000程度です。」など。

こういった指示するとClaude Codeさんは問題を良い感じに解釈してくれました。逆にこれを指定しておかないと一般物体認識の実装を頑張ろうと機械学習モデルを使おうとしたりして、「まって!トークン無駄遣いしないで!」ってなったりします。

 

問題発生時は少しずつ掘り下げて質問していく

これでも細かな条件がまだ足りていなくて、安直な実装だとうまく動かないのですよね。

で、まあ以前やったことあるので自分はだいたい検討はつくのですがAIに指示するには「ロバストネスな実装を検討して提案してください」とか指示するといくつか提案はしてくれます。

それでもなぜうまくいっていないのかをAIはわかってないことがあるので、そういった場合はさらに掘り下げて質問したり条件を提示すると解決策を出してくれます。

この場合は「環境光が安定していないので、特定物体認識が安定しません。ロバストネスなアルゴリズムへの修正を検討し、提案してください。」

などですね。

 

必要であればアルゴリズムを明確に指定する

カードの分類アルゴリズムなどがそうなのですが、細かく指示せざるを得ない部分があります。PCAで特徴空間上で分類を行うことだったり、比較を行う際の距離アルゴリズムをどのように扱うかの問題などもあります。一般的なものでは二乗和誤差などがありますがマンハッタン距離やユークリッド距離などを使うのであればそれを指示しておくと安定した結果が得られます。

 

非機能要件やアーキテクチャを明確にする

たとえば今回作成したWebアプリはフロントとバックのSPAです。フロントはカメラ制御を行い、バックのAPIで画像処理を行って、その結果をフロントに返し、フロント側で3Dレンダリングを行う仕組みです。

このとき、バックエンドの画像処理は負荷が高いため3Dレンダリングやカメラプレビューの更新に比べてとても遅いことが想定されます。

そのため

・カメラプレビューと3Dレンダリングは30FPS

・バックエンドのAPIレスポンスタイムは0.1秒程度です

などの情報をAIに与えることが重要になります。

 

これを与えないと、3Dモデルが時々しか表示されないといった状況になりました。

デバッグしたところ秒間10回程度しか表示されていなかったので、APIのレスポンスのタイミングでのみ表示されていたというオチでした。

 

このままだとAIさんは何が問題なのかを理解してくれないので、アーキテクチャによる問題点と解決のための糸口を与える必要があります。

この場合は

  • APIのレスポンスが遅いから非同期実行もしくはスレッド化して実行すること
  • 生産者/消費者デザインパターンを適用して実装すること
  • カメラプレビューが画像処理APIを多重呼び出ししないよう保護すること

などを与えてあげる必要がありました。

 

「普通そんな実装しないでしょ」って思っててもAIさんはやらかしてくれる

人間同士でもそうなのですが「普通」って人それぞれなので曖昧なままやるとだいたい事故ります。

 

今回問題が起きたのはカードに3Dモデルを紐づけるための管理画面でした。

カード一覧を表示していて画像にして400枚くらいです。表示するための情報はバックエンドのAPIからもらう実装です。

 

私は「普通に考えて、100枚超える画像を表示するのだからスクロールとか次へボタンみたいな実装にしてくれるといいかな。無限スクロールとかでよくある実装はしなくてもいいけど、まずは見てみよう」

みたいに考えてました。結果はダメでした。

 

API仕様を示さなかったのが悪いのですが、「画像400枚をBASE64エンコードして詰め込んだJSON」を返すAPIエンドポイントができてました。

画像

100MB超えの大きなレスポンス。素敵ですね。

 

ユーザーやターゲットを明確にして指示する

たとえば先ほどの管理画面やAR表示されている画面です。

完成したものはカードは横には並ばずに縦一列のスクロールになっていますが、最初の段階では横にも並ぶ実装になっていました。

ターゲット環境を特に指定していないためで、こういった部分でも方向性や指示をしっかりと伝えておく必要があります。

たとえば

  • モバイル利用前提であること
  • PCからも利用できるようレスポンシブデザインを考慮

などの指示が必要です。

 

ARの画面についても今はまだ開発段階なので

  • 3Dモデリングレンダリングはカメラプレビュー重畳モードと分離してデバッグできるモードを設けること
  • カメラやAPIの状態やカードの認識状態を表示するブロックを設けておくこと
  • カードの認識結果によってブロックサイズが変化する恐れがあるため、このサイズは固定としておくこと

などを明示しておく必要があります。こういうのを油断するとすぐ消そうとしたりします。

 

 

 

対策

ここまでで問題点や使い勝手など色々な知見が出たので、一旦の対策を示していきましょう。

CLAUDE.mdを書こう

CLAUDE.mdはClaude Codeが参照していい感じ動いてくれるための前提が書かれたファイルです。たとえばこういうプロジェクト概要を書いておくと、少なくともここから逸脱した行為はとらなくなります。

## プロジェクト概要

AR(拡張現実)トレーディングカード検出システムです。
 
プロジェクト構成:
- **バックエンド (Rust)**: Axumを使用したREST APIサーバー
  - OpenCVによるリアルタイム画像処理(エッジ検出、カード検出)
  - PCA機械学習を使用したMTGカード分類

- **フロントエンド (React/TypeScript)**: Webベースカメラインターフェース
  - リアルタイムカメラフィード処理
  - Three.jsを使用した3D ARオーバーレイ

逆に言うと、ここに書いてないことは好きにやってという意味になるくらいに解釈しておいたほうがよさそうです。

 

プログラマではなく、アーキテクトやマネージャとして考えよう

AIを使ってコードを書くにあたって、プログラマ的な思想も必要ですが、多くの場面ではアーキテクト的な考え方で指示をしたほうがうまくいくと感じました。

 

プログラマ間でよくある宗教論争みたいな細かな話はもはや重要ではなくて、もう一段上のレベルからAIに指示していくことになるのだろうなと思っています。

 

forとwhileどちらがいいか?動的ポリモーフィズムと静的ポリモーフィズムどちらがいいか?

そういった細かな話よりも、アーキテクチャなどのレベルで抽象的に考えていくようにしたほうが良い気がしました。どうしても実装レベルで細かな指示が必要だとしたら、先に例にあげたデザインパターンレベルまででしょうか。

 

20~30年前のコンパイラIDEが現在のAIなのかもしれない

その昔、マシン語からアセンブラへ移り変わり、Cソースコードコンパイルしてアセンブラマシン語を生成する、そんな時代がありました。

(別系統でインタプリタ方式やバイトコード仮想マシン方式もありますが、ここでは省略します。)

またそれを開発しやすくするためにたくさんの種類のIDEが開発され、さらに上流ではCASEツールなんてのもあったようです。

 

組み込みソフトの仕事をしていたころ、コンパイラは当然使っていたのですが、マイナーなコンパイラなどではバグがあったりして「こういう書き方ではなくて、こう書いたほうが安定する」みたいなことがありました。

 

今のAIは昔のコンパイラと同じくらいの立ち位置にいるのではないかなと思っています。うまく指示しないと動かないのだけど、うまく指示していい感じに動いてくれることが多い。みたいな。

今後AIがどこまで普及するかは未知数ですが、少なくとも作業がすごくラクになったので、この流れは続くのではないかなと思っています。

 

現在の競争が続けばIDEも含めてAIをリーズナブルに利用できる日がくるだろうと想定していますが、その時になってからだときっと出遅れているのだろうなと思っていまs。

 

そういった意味も込めて、もっと使いこなせるように色々試してみたいですね。

 

感想と今後の展望

作りたかったものがなんとなく形になってきて楽しいですね。

今後はこういう方向でもっといろいろ試してみたいと思います。

Claudeに関してはMCPとかアーティファクトも機能が増えたみたいなので、また別途試して記事を書きたいなと思っています。それでは今回はこれくらいで。

AIからアクチュエータを制御する(Claude MCPと Raspberry pi 5を使って)

黄金週間前半ですね。かたりぃなです。 世間ではAIのMCPが流行ってるようなので少し調べてみたところ、とても面白いことができるのではないかと思った次第です。

大昔にモーションを検出してそれにあわせてLEDの色を変更できないかと試行錯誤していました。 これは加速度センサを使ってその値でLEDのRGB色を変更しようという試みです。 https://catalina1344.hatenablog.jp/entry/2014/12/14/220610

この図でいう上半分をAIとMCPに置き換えてみようというのが今回の試みです。

というわけで、目標は

「必殺技や決めセリフに応じて、演出を変更するAI」

ということにします。

ちなみに音声入力をリアルタイムに処理する方法がすぐには準備できなかったので、ClaudeDesktopのチャットにキーボードで入力することで対処します。

構成図を書くのが面倒なので、文字で簡単に載せておきます。

PC側は

ClaudeDesktop -> MCP -> 自作MCPサーバ -> HTTPへ

ラズパイ側は

HTTPから -> Python(FastAPI) -> GPIO -> 魔法陣テープLED

です。

Raspberry Pi 5の初期設定

機材はラズパイのスターターキットです。これで完了です。

https://akizukidenshi.com/catalog/g/g129362

テープLEDとかは冒頭の過去ブログに書いてあるものをそのまま使います。

今からマネする人は2SK2231が廃盤かもしれないので、適当に代替品を見繕う必要があるかもしれません。

OSはスターターキットに含まれている書き込み済みmicroSDカードが手に入るのでそれを使います。

どうしても変更したければラズパイ公式からイメージライターをダウンロードしてきてmicroSDカードに書けばOKです。

小さいラズパイもついでに買ってきたのですが、そちらのOSはイメージライターで書いてセットアップしてふつうに動きました。

セットアップ

HDMIで画面出力ができるので、画面を繋いでキーボードとマウス、あと必要があればLANケーブルを接続して設定します

これだけでした。 一つ気を付けるとすれば、5GHzのwifiは繋がりません。(規格名忘れた) なので、パスワード通らないとかあったら2.4GHzのほうに接続するようにすればOKです。

あまり高度なことしないのでこれで充分でしょう。

Lチカ儀式

とりあえず、GPIOを操作できるマイコンとかワンボードコンピュータを手に入れたら儀式です。 初めて触る言語でhello worldするようなものです。

コードはこんな感じ。 pythonでいけちゃうのとても便利ですね。

import lgpio
import time

# ライブラリを初期化
h = lgpio.gpiochip_open(0)
LED_PIN = 26

# LEDピンを出力に設定
lgpio.gpio_claim_output(h, LED_PIN)

try:
    while True:
        # LEDをオンにする
        lgpio.gpio_write(h, LED_PIN, 1)
        print("LED ON")
        time.sleep(1)
        
        # LEDをオフにする
        lgpio.gpio_write(h, LED_PIN, 0)
        print("LED OFF")
        time.sleep(1)
        
except KeyboardInterrupt:
    print("プログラムを終了します")
    
finally:
    # リソースの解放
    lgpio.gpiochip_close(h)

ちなみにピン番号はラズパイにsshしてpinoutコマンドを打てば教えてくれます。 ボードの向きと1番がどこかを教えてくれてるので間違えないように注意しましょう。 26なので端っこですね。GNDの隣。わかりやすい位置にしました。

,--------------------------------.
| oooooooooooooooooooo J8   : +====
| 1ooooooooooooooooooo      : |USB2
|  Wi  Pi Model 5B  V1.0  fan +====
|  Fi     +---+      +---+       |
|         |RAM|      |RP1|    +====
||p       +---+      +---+    |USB3
||c      -------              +====
||i        SoC      |c|c J14     |
(        -------  J7|s|s 12 +======
|  J2 bat   uart   1|i|i oo |   Net
| pwr\..|hd|...|hd|o|1|0    +======
`-| |-1o|m0|---|m1|--------------'

J8:
   3V3  (1) (2)  5V    
 GPIO2  (3) (4)  5V    
 GPIO3  (5) (6)  GND   
 GPIO4  (7) (8)  GPIO14
   GND  (9) (10) GPIO15
GPIO17 (11) (12) GPIO18
GPIO27 (13) (14) GND   
GPIO22 (15) (16) GPIO23
   3V3 (17) (18) GPIO24
GPIO10 (19) (20) GND   
 GPIO9 (21) (22) GPIO25
GPIO11 (23) (24) GPIO8 
   GND (25) (26) GPIO7 
 GPIO0 (27) (28) GPIO1 
 GPIO5 (29) (30) GND   
 GPIO6 (31) (32) GPIO12
GPIO13 (33) (34) GND   
GPIO19 (35) (36) GPIO16
GPIO26 (37) (38) GPIO20
   GND (39) (40) GPIO21

LEDは秋月とか千石で売ってる適当な安いLEDでいいです。高輝度は面倒なことになるので、普通の緑とか赤がおすすめ。

電子工作に慣れないうちはこの「普通の」というのがとても難しいんですよね。 厳密にいうとI/Oポートの電圧とか電流の上限値とLEDの順電圧と最大定格電流が云々とか面倒な話になるので。

いわゆるプログラミング初心者が環境構築で挫折するようなものです。

というわけで、LED単体はそこまで高いものでもないので、2~3種類買っておいて、うまくいかないときは差し替えるでいいと思ってます。 慣れてくると抵抗とかコンデンサたくさんの種類持つようになるのと同じ。

回路としては

GPIO26 -> ブレッドボード上へ -> LEDアノード -> LEDカソード -> 抵抗1kくらいあればいい -> ラズパイのGNDへ

です。 チカチカすれば儀式完了です。

テープLEDの制御

テープLEDの制御では、先ほどのように単純にGPIOポートから駆動できません。 電流が足りないのです。 ので、電子工作の時間です。

2SK2231というMOSFETトランジスタのをスイッチとして使います。

トランジスタとは?ソフト屋さんが趣味でやるだけなら「ソフトウェアで回路のON/OFFができるやつ」って理解でひとまず充分です。もし気を付けるとすれば、電流量とかですね。

流したい電流量、駆動電圧に応じた半導体素子を選んであげる必要があります。

でかいスイッチは大電流を流せますが、そのスイッチを動かすためには大きな電圧が必要です。 なので、ラズパイの3.3Vとか5VでONにできる半導体で、LEDの電流を流せるやつであれば何でもOKです。

回路としてはフルカラーテープLEDがアノードコモン(電源+側が一本にまとまっていて、マイナス側のRGBが個別のピンになっている)なので、例えばRのピンを制御するにはこうなります。

GPIO -> 2SK2231のゲート
テープLEDのR -> 2SK2231のドレイン
2SK2231のソース -> GND

です。

図には示していませんが、2つ抵抗を入れるのを忘れずに。

  • GPIOとゲートの間のゲート抵抗
  • GPIO-GND間のプルダウン抵抗

入れなくても趣味レベルであれば動きはしますが、お行儀良くないです。

これと同じことをG,Bにも行います。

GNDをラズパイとも繋ぐのを忘れずに。

これでRGBそれぞれで点灯/消灯ができるようになりました。

LEDの輝度をON/OFFじゃなくて段階的に処理したい。

明るさを変更したいのでPWMで輝度変更できるようにします。 ArduinoではPWMポートたくさんあったのですが、ラズパイは2つしかないみたいです。 pythonライブラリの使い方読んでもよくわからなかったので、自力でPWMやります。 こうなりました。

# スタンドアロンで動作するLED制御プログラム

import lgpio
import time
import threading

# GPIOピン番号を指定
h = lgpio.gpiochip_open(0)
LED_PIN = 26

R_PIN = 2
G_PIN = 3
B_PIN = 4

# GPIO設定
lgpio.gpio_claim_output(h, LED_PIN)
lgpio.gpio_claim_output(h, R_PIN)
lgpio.gpio_claim_output(h, G_PIN)
lgpio.gpio_claim_output(h, B_PIN)


class LEDController(threading.Thread):
    def __init__(self, pin, frequency=100, duty_cycle=50):
        super().__init__()
        self.pin = pin
        self.frequency = frequency
        self.duty_cycle = duty_cycle
        self.running = True
        self.lock = threading.Lock()  # デューティサイクル変更用のロック

    def run(self):
        period = 1 / self.frequency
        while self.running:
            with self.lock:
                on_time = period * (self.duty_cycle / 100)
                off_time = period * (1 - self.duty_cycle / 100)

            # LEDをオン
            lgpio.gpio_write(h, self.pin, 1)
            time.sleep(on_time)

            # LEDをオフ
            lgpio.gpio_write(h, self.pin, 0)
            time.sleep(off_time)

    def set_duty_cycle(self, duty_cycle):
        """デューティサイクルを変更する"""
        with self.lock:
            self.duty_cycle = max(0, min(100, duty_cycle))  # 0~100の範囲に制限

    def stop(self):
        self.running = False
        lgpio.gpio_write(h, self.pin, 0)  # LEDを消灯

# LED制御スレッドを作成
led_thread = LEDController(LED_PIN, frequency=100, duty_cycle=10)
led_thread.start()

led_thread_r = LEDController(R_PIN, frequency=100, duty_cycle=10)
led_thread_r.start()

led_thread_g = LEDController(G_PIN, frequency=100, duty_cycle=10)
led_thread_g.start()

led_thread_b = LEDController(B_PIN, frequency=100, duty_cycle=10)
led_thread_b.start()

try:
    # メインスレッドでデューティサイクルを変更する例
    while True:
        new_duty_cycle = int(input("R:新しいデューティサイクルを入力してください (0-100): "))
        led_thread_r.set_duty_cycle(new_duty_cycle)
        print(f"R:デューティサイクルを {new_duty_cycle}% に変更しました")
        new_duty_cycle = int(input("G:新しいデューティサイクルを入力してください (0-100): "))
        led_thread_g.set_duty_cycle(new_duty_cycle)
        print(f"G:デューティサイクルを {new_duty_cycle}% に変更しました")
        new_duty_cycle = int(input("B:新しいデューティサイクルを入力してください (0-100): "))
        led_thread_b.set_duty_cycle(new_duty_cycle)
        print(f"B:デューティサイクルを {new_duty_cycle}% に変更しました")

except KeyboardInterrupt:
    print("プログラムを終了します")

finally:
    # LED制御スレッドを停止
    led_thread.stop()
    led_thread.join()
    
    led_thread_r.stop()
    led_thread_r.join()
    
    led_thread_g.stop()
    led_thread_g.join()
    
    led_thread_b.stop()
    led_thread_b.join()
    
    lgpio.gpiochip_close(h)

デューティー比とか難しい用語使ってますが、ある時間(たとえば100)に対して、どれくらいの期間ON/OFFにするかって話です。

出力が1と0を繰り返せば50%とかそういう話です。

積分した値が明るさに相当するわけです。

ちなみに、無理やりやってるので多少チラつきます。昔の蛍光灯みたいに。 どうしても気になるならコンデンサでも突っ込めばマイルドになると思います。

HTTP APIの実装

ここまででLEDを制御できるようになりました。

とはいえ毎回ラズパイにSSHしてコマンドを実行してってのはあまりに手間です。

APIを公開しましょう。 FastAPIってのがあるらしいので、適当にサンプルコードまねして作ります。 LEDスレッドを立てるところは同じなので、差分だけ乗せます。

import uvicorn
from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()

import lgpio
import time
import threading

#~~~
# スレッドは前述のスタンドアロンプログラムと同じ
#~~~

class RGBValues(BaseModel):
    r: int  # 0~100の範囲
    g: int  # 0~100の範囲
    b: int  # 0~100の範囲

@app.post("/set_rgb")
def set_rgb(rgb: RGBValues):
    """RGB値を受け取り、LEDのデューティサイクルを設定する"""
    # 各値を0~100の範囲に制限
    r_duty = max(0, min(100, rgb.r))
    g_duty = max(0, min(100, rgb.g))
    b_duty = max(0, min(100, rgb.b))

    # デューティサイクルを設定
    led_thread_r.set_duty_cycle(r_duty)
    led_thread_g.set_duty_cycle(g_duty)
    led_thread_b.set_duty_cycle(b_duty)

    return {"message": "RGB values updated", "r": r_duty, "g": g_duty, "b": b_duty}

@app.get("/")
def read_root():
    return {"message": "Hello World"}


if __name__ == "__main__":
    # 0.0.0.0で全てのインターフェースをリッスン
    uvicorn.run(app, host="0.0.0.0", port=8000)

簡単ですね。

とりあえず uvicorn app.main:app --host 0.0.0.0 --reload 起動するみたいです。

--hostを指定しておかないと他PCから叩けないので注意です。

テストするには curl -X POST -H "Content-Type: application/json" -d '{"r": 200, "g": 10, "b": 10}' http://raspberrypi:8000/set_rgb です。

Claude MCPサーバの構築

ラズパイの先にぶら下がってるテープLEDもとい、儀式用の魔法陣をHTTPから制御できるようになりました。 魔力を持たない人々であっても、スマホのアプリ、webアプリ、前述のcurlコマンドで制御できます。

今回はAIさんに制御してもらいます。 AIさんが外界に影響を及ぼす方法としてMCPを使います。

ここでMCPは詳しくは説明しませんが、Claudeのサンプルコードをコピって動かせば何が起きるかわかると思います。

Claude DesktopでのMCPの設定は完了済みとします。 手元の環境でのclaude_desktop_config.jsonはこのようになっています。 大抵の環境ではパスとかを適切に設定すればうまくいくと思います。

{
  "mcpServers": {
    "filesystem": {
      "command": "npx",
      "args": [
        "-y",
        "@modelcontextprotocol/server-filesystem",
        "C:\\Users\\PC_User\\Desktop",
        "C:\\Users\\PC_User\\Downloads"
      ]
    },
    "blender": {
      "command": "uvx",
      "args": [
        "blender-mcp"
      ]
    },
    "weather": {
      "command": "uv",
      "args": [
          "--directory",
          "C:\\Users\\PC_User\\Desktop\\AIWork\\ClaudeMpc\\weather",
          "run",
          "weather.py"
      ]
    }
  }
}

MCPのサンプルコードが動いたら、そこに以下のコードを追加してみます。

単に先ほどのapiを呼んでるだけです。 Claude Desktopを再起動して、ツールが増えていれば成功です。

@mcp.tool()
async def set_rgb_color(r: int, g: int, b: int) -> str:
    """Set RGB color LED.
    Args:
        r: Red component (0-100)
        g: Green component (0-100)
        b: Blue component (0-100)
    """
    url = "http://raspberrypi:8000/set_rgb"
    payload = {"r": r, "g": g, "b": b}
    headers = {"Content-Type": "application/json"}

    try:
        async with httpx.AsyncClient() as client:
            response = await client.post(url, json=payload, headers=headers)
            response.raise_for_status()
            return f"RGB color set successfully: R={r}, G={g}, B={b}"
    except httpx.RequestError as e:
        return f"Failed to set RGB color: {e}"
    except httpx.HTTPStatusError as e:
        return f"API returned an error: {e.response.status_code} - {e.response.text}"

これでClaudeのプロンプトを良い感じに使ってあげれば、魔法陣のコントールができます。

実演

事前のプロンプトで準備させてから、実際のセリフを入れることにします。

実演準備

プロンプト

コスプレ用の電飾アイテムを作っています。地面に置いた魔法陣を、テープLEDでライトアップできるシステムです。
コスプレイヤーさんの声に反応して色を変えられるように実装しました。
set_rgb_colorを使って色を変えられることができます。
これよりコスプレイヤーの音声入力がこのプロンプトに入力されます。
まずはいったん消灯しておいてもらえますか?
次のプロンプトから決めセリフになるので、それに合わせてライトアップしてください。
よろしくお願いします。

これで準備が整いました。 消灯されたので、あとはセリフを入力すればいい感じに推測してライトアップしてくれます。

たとえば

カイザーフェニックス

プロンプト

この世のすべてを焼き尽くす炎、カイザーフェニックス!!!!

Claude

了解しました!炎のテーマですね。カイザーフェニックスの力強い炎を表現するために、赤と黄色を基調とした鮮やかな光で魔法陣を輝かせましょう!

リクエス

{
  `b`: 0,
  `g`: 50,
  `r`: 100
}

レスポンス

RGB color set successfully: R=100, G=50, B=0

Claude

カイザーフェニックスの炎の力が魔法陣に宿りました!鮮やかな炎の色で魔法陣が明るく輝いています。この世のすべてを焼き尽くす炎の力を表現するために、情熱的な赤とオレンジの光を選びました。次の決めセリフをお待ちしています!

エターナル・フォース・ブリザード

プロンプト

全てを凍らす冷厳なる氷結。エターナルフォースブリザード

Claude

了解しました!氷結のテーマですね。エターナルフォースブリザードの厳かな冷気を表現するために、青と白を基調とした冷たい光で魔法陣を輝かせましょう!

リクエス

{
  `b`: 100,
  `g`: 50,
  `r`: 0
}

レスポンス

RGB color set successfully: R=0, G=50, B=100

Claude

エターナルフォースブリザードの力が魔法陣に宿りました!凛とした青の光で魔法陣が神秘的に輝いています。全てを凍らす冷厳なる氷結の力を表現するために、透き通るような青と氷のような光を選びました。次の決めセリフをお待ちしています!

素晴らしいですね。こういうアドリブ的なこともサラっとやってくれるのがさすがAIですね。

ライトアップの反映までタイムラグがどうしても発生してしまいますが、色々面白い使い方ができそうです。

あと、写真撮り忘れましたが、徐々に明るくとかいう指定も良い感じにやってくれます。

感想と今後の展望

Windows11でも音声入力頑張ればできるんですが、手間かかりすぎなのでもう少しうまくやりたいところですね。

手順はこんなでした。

  • Windowsの設定でアクセシビリティ機能を有効化する
  • 「Voice Access Wakeup」とボイス入力

    • 音声入力が起動する
  • 「show numbers」とボイス入力

    • フォーカスできるウインドウに番号が表示される
  • ClaudeDesktopのテキストエリアの番号をボイス入力
    • チャット入力ウインドウにフォーカスする
  • 音声入力でプロンプトを入力
  • 「show numbers」とボイス入力
  • フォーカスできるウインドウに番号が表示される
  • ClaudeDesktopの送信ボタンの番号を入力
    • Claudeの送信ボタンが押せる

ちょっとシーケンス多すぎです。萎えます。

たとえばコスプレイベント会場とかで「写真いいですか~?」ってなったときに、ポーズをとるわけです。 そこで、ポーズをとりつつ必殺技を叫んだらすぐにライトアップしてほしいんですよ。それなのに↑みたいなシーケンスやってたら萎えるじゃないですか?

で、「他のポーズもお願いします~!」ってなってまたシーケンス繰り返すのか!?って話です。

Android版のClaudeなら音声で全部操作できるので、Android版にMCPが追加されるのを期待したいところですね。

あとは、他のアクチュエータもAIで良い感じに制御できる気がします。スマートスピーカー的な使い方もできちゃうわけですね。色々と夢が広がります。

それでは今回はこれくらいで!

TauriでAndroidアプリを作ってみた。

Tauri楽しくなってきました。かたりぃなです。 マルチプラットフォームという言葉にはとてもあこがれを感じます。

一昔前のマルチプラットフォーム

その昔、スマートフォンという言葉が浸透し始めたころ、iPhoneObjective-C, AndroidJava, 両方うまくやれないものか?みたいなことを考えてた時代がありました。

そうこうしているうちにMSからはHololensが出てきて、じゃあC#だな!Unityでいこう!なんならC++で下回り書けばいいかな?とかやってた頃がありました。

そんな当時出ていたマルチプラットフォームな開発環境としては

  • Xamarin
  • Cordova
  • Electron

などでしょうか。 あとはUnityもマルチプラットフォームとして数える人もいるかもしれません。

これらに期待していた頃もありましたが、ちょうどUnity + Hololensで遊んでたので、なんかもうUnityでよくね?みたいな雑な感じになって、調べるのは放棄してました。

Tauriに何を期待してるか?

まず期待しているのはRustです。

純粋にRustが楽しいからですね。あとは本業のWeb開発みたいな感じで開発できるってのがラクでいいですね。

というわけで、早速環境を作って、アプリを作り、インストールするところまでやってみます。

今回やった内容

リストアップしてみると結構多かったです。

そしてほとんどがTauri関係ないですね。

環境

環境はこんな感じです。

  • Windows11
  • WSL2

Tauri含めRust関係のものはひととおりインストール済みとします。

このWSL2上にAndroidの開発環境をインストールしていきます。

WSL2上にAndroidStudioをインストールする準備

WSL2上でGUIアプリ動くらしいので、それでいきます。X-Windowは不要です。最近のWindowsすごいですね。

なぜWSL2上でやるか?ここ最近Windowsの開発をしなくなっていて、何も入ってないWindows上にRustの環境を作るのが辛いからです。

参考資料はこのあたりです。 https://learn.microsoft.com/ja-jp/windows/wsl/tutorials/gui-apps

WSL上にchromeをダウンロードして起動する

AndroidStudioをインストールしたいだけなのですが、WindowsのブラウザからLinux用のAndroidStudioのインストーラが探せません。

AndroidStudioのダウンロードページはとても親切なことにOSに応じたAndroidStudioのダウンロードページに遷移するみたいです。 UA偽装してやればいけるのかもしれませんが、環境構築は安定的な方法でやりたいので、WSL2上にChromeをインストールし、そのChromeでAndroidStudioをダウンロードするという手順をとりました。

Chromeのインストールと起動は上記のサイトの説明のとおり

wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb
sudo apt install --fix-missing ./google-chrome-stable_current_amd64.deb
google-chrome

です。

chromeが起動すると一部文字化けしてたりしますが、細かいことは気にせずAndroidStudioでぐぐってダウンロードページからダウンロードします。

ファイル名からして自動的にlinux版になるはずです。

/home/<wslユーザー名>/Downloads/android-studio-2024.3.1.13-linux.tar.gz

にダウンロードされれていると思います。

AndroidStudioのインストール

適当なディレクトリに移動して展開しました。 どうもこのあたりの流儀よくわからないので、パスとかの扱いはもっと詳しいサイトを調べたほうが良いと思います。 私はAndroid開発にはそこまで力入れるつもりはないので、動けばいいやスタンスです。

今回ほしいのはAndroid向けビルドとデプロイ、アプリへの署名ツールとかだけなので、それが動けばなんでもいいです。

展開して

mv /home/<wslユーザー名>/Downloads/android-studio-2024.3.1.13-linux.tar.gz /opt/
tar zxvf /opt/android-studio-2024.3.1.13-linux.tar.gz

インストーラを起動 ./android-studio/bin/studio.sh

インストール先とか聞かれるので適当に進めます

インストール後のログ

Preparing "Install Android Emulator v.35.4.9".
Downloading https://dl.google.com/android/repository/emulator-linux_x64-13025442.zip
"Install Android Emulator v.35.4.9" ready.
Installing Android Emulator in /home/<ユーザー名>/Android/Sdk/emulator
"Install Android Emulator v.35.4.9" complete.
"Install Android Emulator v.35.4.9" finished.
Preparing "Install Google Play Intel x86_64 Atom System Image API 35 (revision 9)".
Downloading https://dl.google.com/android/repository/sys-img/google_apis_playstore/x86_64-35_r09.zip
"Install Google Play Intel x86_64 Atom System Image API 35 (revision 9)" ready.
Installing Google Play Intel x86_64 Atom System Image in /home/<ユーザー名>/Android/Sdk/system-images/android-35/google_apis_playstore/x86_64
"Install Google Play Intel x86_64 Atom System Image API 35 (revision 9)" complete.
"Install Google Play Intel x86_64 Atom System Image API 35 (revision 9)" finished.
Preparing "Install Sources for Android 35 (revision 1)".
Downloading https://dl.google.com/android/repository/source-35_r01.zip
"Install Sources for Android 35 (revision 1)" ready.
Installing Sources for Android 35 in /home/<ユーザー名>/Android/Sdk/sources/android-35
"Install Sources for Android 35 (revision 1)" complete.
"Install Sources for Android 35 (revision 1)" finished.
Preparing "Install Android SDK Platform 35 (revision 2)".
Downloading https://dl.google.com/android/repository/platform-35_r02.zip
"Install Android SDK Platform 35 (revision 2)" ready.
Installing Android SDK Platform 35 in /home/<ユーザー名>/Android/Sdk/platforms/android-35
"Install Android SDK Platform 35 (revision 2)" complete.
"Install Android SDK Platform 35 (revision 2)" finished.
Preparing "Install Android SDK Build-Tools 35.0.1 v.35.0.1".
Downloading https://dl.google.com/android/repository/build-tools_r35.0.1_linux.zip
"Install Android SDK Build-Tools 35.0.1 v.35.0.1" ready.
Installing Android SDK Build-Tools 35.0.1 in /home/<ユーザー名>/Android/Sdk/build-tools/35.0.1
"Install Android SDK Build-Tools 35.0.1 v.35.0.1" complete.
"Install Android SDK Build-Tools 35.0.1 v.35.0.1" finished.
Preparing "Install Android SDK Platform-Tools v.35.0.2".
Downloading https://dl.google.com/android/repository/platform-tools_r35.0.2-linux.zip
"Install Android SDK Platform-Tools v.35.0.2" ready.
Installing Android SDK Platform-Tools in /home/<ユーザー名>/Android/Sdk/platform-tools
"Install Android SDK Platform-Tools v.35.0.2" complete.
"Install Android SDK Platform-Tools v.35.0.2" finished.
Parsing /home/<ユーザー名>/Android/Sdk/build-tools/35.0.1/package.xml
Parsing /home/<ユーザー名>/Android/Sdk/emulator/package.xml
Parsing /home/<ユーザー名>/Android/Sdk/platform-tools/package.xml
Parsing /home/<ユーザー名>/Android/Sdk/platforms/android-35/package.xml
Parsing /home/<ユーザー名>/Android/Sdk/sources/android-35/package.xml
Parsing /home/<ユーザー名>/Android/Sdk/system-images/android-35/google_apis_playstore/x86_64/package.xml
Android SDK is up to date.
Creating Android virtual device
Android virtual device Medium_Phone_API_35 was successfully created

インストールしたら適当に初期設定しましょう。

$ /opt/android-studio/bin/studio.shAndroid Studioが起動すると思いますが、/bin/studioを使えみたいに言われてますね。

The IDE seems to be launched with a script launcher ('bin/studio.sh'). Please consider switching to a native launcher ('bin/studio') for better experience.

なので以降は/opt/android-studio/bin/studioを使います。

AndroidStudioでのビルドとデバッグ

ビルドはIDEの金槌マーク デバッグは虫マークみたいです

デバッガを開始するとエミュレータが起動するはずなのですが、エラーログがたくさん出ていて起動してこないので修復します。

Androidエミュレータのためにkvmをインストールする

AndroidStudioの起動後、エミュレータを起動するとエラーになってしまいます。 kvmにアクセスできないみたいなエラーなのでインストールと設定をします。

kvmの設定がうまくいかないときはこのあたりを参考に https://help.ubuntu.com/community/KVM/Installation

追加の設定

不足している依存パッケージがあったのでインストールします。

sudo apt update
sudo apt-get install qemu-kvm libvirt-daemon-system libvirt-clients bridge-utils

WSLを再起動

PS> wsl.exe --shutdown

あとは /dev/kvmパーミッション云々言われるときはとりあえず sudo chmod a+rw /dev/kvm してからAndroidStudioを起動すれば一旦は動く。

これでandroidの開発環境ができたので、いったん閉じる。

tauriプロジェクトをつくる

いつもどおりtauriプロジェクトを作りましょう。

$ cargo create-tauri-app
✔ Project name · tauri-android
✔ Identifier · com.tauri-android.app
✔ Choose which language to use for your frontend · TypeScript / JavaScript - (pnpm, yarn, npm, deno, bun)
✔ Choose your package manager · npm
✔ Choose your UI template · React - (https://react.dev/)
✔ Choose your UI flavor · JavaScript

Template created! To get started run:
  cd tauri-android
  npm install
  npm run tauri android init

For Desktop development, run:
  npm run tauri dev

For Android development, run:
  npm run tauri android dev

なんかDesktop開発の場合はこうする、Android開発の場合はこうするって親切にコマンド提示してくれてるので、指示の通りやってみましょう。

Tauriの指示のとおりビルドする

$ npm install

added 67 packages, and audited 68 packages in 10s

9 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities
$ npm run tauri android init

> tauri-android@0.1.0 tauri
> tauri android init

action request:  to initialize Android environment; Android support won't be usable until you fix the issue below and re-run `tauri android init`!
    Have you installed the Android SDK? The `ANDROID_HOME` environment variable isn't set, and is required: environment variable not found
victory: Project generated successfully!
    Make cool apps! 🌻 🐕 🎉

$ npm run tauri dev

ここまでで tauriのデスクトップアプリの画面が出ました。

Android用tauriを動かしてみる(失敗1)

TauriでAndroid開発やってみましょう。

$ npm run tauri android dev

> tauri-android@0.1.0 tauri
> tauri android dev

Have you installed the Android SDK? The `ANDROID_HOME` environment variable isn't set, and is required: environment variable not found: environment variable not found
    Error Have you installed the Android SDK? The `ANDROID_HOME` environment variable isn't set, and is required: environment variable not found: environment variable not found

だめでした。。。 ANDROID_HOME環境変数が設定されてないってことなので設定する。

適当にインストールした結果、どうやらここっぽいですね? /home/<ユーザー名>/Android/Sdk/

なので、 echo "export ANDROID_HOME=~/Android/Sdk" >> ~/.bash_profile して、WSL2端末を再起動。

Android用Tauriを動かしてみる(失敗2)

$ npm run tauri android dev
Have you installed the NDK? The `NDK_HOME` environment variable isn't set, and is required: environment variable not found: environment variable not found
    Error Have you installed the NDK? The `NDK_HOME` environment variable isn't set, and is required: environment variable not found: environment variable not found

NDK_HOME環境変数が設定されてないってことなので設定する。

android開発者のドキュメントのインストールの方法が書いてあるのでそのとおりに。 https://developer.android.com/studio/projects/install-ndk?hl=ja ついでにCMakeも必要らしいので、併せてやっておきます。

さっきと同様、 echo "export NDK_HOME=~/Android/Sdk/ndk/29.0.13113456" >> ~/.bash_profile してから、WSL再起動。

android用tauriを動かしてみる(失敗3)

initからやり直しです。

$ sudo chmod a+rw /dev/kvm
$ npm run tauri android init
$ npm run tauri android dev

> tauri-android@0.1.0 tauri
> tauri android dev

応答なくなりました。

なんかgradlewってコマンドでログ見れるらしいので調べてみます。

/home/<ユーザー名>/tauri-android/src-tauri/gen/android/gradlew --project-dir /home/<ユーザー名>/tauri-android/src-tauri/gen/android assembleDebug

JAVA_HOMEが設定されていないエラーが出てますね? https://developer.android.com/build/jdks?hl=ja#jdk-config-in-studio をもとにandroidstudioの設定を確認します。

androidstudioのJAVA_HOMEは/opt/android-studio/jbrが設定されていたので、これを設定します。

echo "export JAVA_HOME=/opt/android-studio/jbr" >> ~/.bash_profile

WSL再起動です。

android用tauriを動かしてみる(失敗4)

全然反応ないです。 何が正解かわからないのですが、ネットの情報を見ているとエミュレータ上でtauriの画面が出るみたいです。

そういえばtauriに対してエミュレータが起動する設定とかやってないというか、そんな連携してくれるみたいな話は聞いたことないですね?

裏でAndroidStudioとエミュレータを起動した状態でnpm run tauri android devする必要がありそうです。

android用tauriを動かしてみる(成功)

AndroidStudioとエミュレータを起動した状態でいけました!

$ npm run tauri android dev

> tauri-android@0.1.0 tauri
> tauri android dev

    Info Detected connected device: Medium_Phone_API_35 (sdk_gphone64_x86_64) with target "x86_64-linux-android"
    Running BeforeDevCommand (`npm run dev`)

> tauri-android@0.1.0 dev
> vite


  VITE v6.2.1  ready in 136 ms

  ➜  Local:   http://localhost:1420/
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 1.38s
    Info symlinking lib "/home/<ユーザー名>/tauri-android/src-tauri/target/x86_64-linux-android/debug/libtauri_android_lib.so" in jniLibs dir "/home/<ユーザー名>/tauri-android/src-tauri/gen/android/app/src/main/jniLibs/x86_64"
    Info "/home/<ユーザー名>/tauri-android/src-tauri/target/x86_64-linux-android/debug/libtauri_android_lib.so" requires shared lib "libandroid.so"
    Info "/home/<ユーザー名>/tauri-android/src-tauri/target/x86_64-linux-android/debug/libtauri_android_lib.so" requires shared lib "libdl.so"
    Info "/home/<ユーザー名>/tauri-android/src-tauri/target/x86_64-linux-android/debug/libtauri_android_lib.so" requires shared lib "liblog.so"
    Info "/home/<ユーザー名>/tauri-android/src-tauri/target/x86_64-linux-android/debug/libtauri_android_lib.so" requires shared lib "libm.so"
    Info "/home/<ユーザー名>/tauri-android/src-tauri/target/x86_64-linux-android/debug/libtauri_android_lib.so" requires shared lib "libc.so"
    Info symlink at "/home/<ユーザー名>/tauri-android/src-tauri/gen/android/app/src/main/jniLibs/x86_64/libtauri_android_lib.so" points to "/home/<ユーザー名>/tauri-android/src-tauri/target/x86_64-linux-android/debug/libtauri_android_lib.so"
Downloading https://services.gradle.org/distributions/gradle-8.9-bin.zip
............10%.............20%.............30%.............40%.............50%.............60%.............70%.............80%.............90%.............100%
Errors during XML parse:
Additionally, the fallback loader failed to parse the XML.
Checking the license for package Android SDK Build-Tools 34 in /home/<ユーザー名>/Android/Sdk/licenses
License for package Android SDK Build-Tools 34 accepted.
Preparing "Install Android SDK Build-Tools 34 v.34.0.0".
"Install Android SDK Build-Tools 34 v.34.0.0" ready.
Installing Android SDK Build-Tools 34 in /home/<ユーザー名>/Android/Sdk/build-tools/34.0.0
"Install Android SDK Build-Tools 34 v.34.0.0" complete.
"Install Android SDK Build-Tools 34 v.34.0.0" finished.
Checking the license for package Android SDK Platform 34 in /home/<ユーザー名>/Android/Sdk/licenses
License for package Android SDK Platform 34 accepted.
Preparing "Install Android SDK Platform 34 (revision 3)".
"Install Android SDK Platform 34 (revision 3)" ready.
Installing Android SDK Platform 34 in /home/<ユーザー名>/Android/Sdk/platforms/android-34
"Install Android SDK Platform 34 (revision 3)" complete.
"Install Android SDK Platform 34 (revision 3)" finished.

> tauri-android@0.1.0 tauri
> tauri android android-studio-script --target x86_64

<=======------> 60% EXECUTING [1m 51s]
    Info Forwarding port 1420 with adb
1420
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.54s
    Info symlinking lib "/home/<ユーザー名>/tauri-android/src-tauri/target/x86_64-linux-android/debug/libtauri_android_lib.so" in jniLibs dir "/home/<ユーザー名>/tauri-android/src-tauri/gen/android/app/src/main/jniLibs/x86_64"
    Info "/home/<ユーザー名>/tauri-android/src-tauri/target/x86_64-linux-android/debug/libtauri_android_lib.so" requires shared lib "libandroid.so"
    Info "/home/<ユーザー名>/tauri-android/src-tauri/target/x86_64-linux-android/debug/libtauri_android_lib.so" requires shared lib "libdl.so"
    Info "/home/<ユーザー名>/tauri-android/src-tauri/target/x86_64-linux-android/debug/libtauri_android_lib.so" requires shared lib "liblog.so"
    Info "/home/<ユーザー名>/tauri-android/src-tauri/target/x86_64-linux-android/debug/libtauri_android_lib.so" requires shared lib "libm.so"dX86_64Debug
    Info "/home/<ユーザー名>/tauri-android/src-tauri/target/x86_64-linux-android/debug/libtauri_android_lib.so" requires shared lib "libc.so"
w: file:///home/<ユーザー名>/.cargo/registry/src/index.crates.io-6f17d22bba15001f/tauri-2.3.1/mobile/android/src/main/java/app/tauri/plugin/PluginMethodData.kt:11:23 Parameter 'methodDecorator' is never used
Java compiler version 21 has deprecated support for compiling with source/target version 8.
Try one of the following options:
    1. [Recommended] Use Java toolchain with a lower language version
    2. Set a higher source/target version
    3. Use a lower version of the JDK running the build (if you're not using Java toolchain)
For more details on how to configure these settings, see https://developer.android.com/build/jdks.
To suppress this warning, set android.javaCompile.suppressSourceTargetDeprecationWarning=true in gradle.properties.
warning: [options] source value 8 is obsolete and will be removed in a future release
warning: [options] target value 8 is obsolete and will be removed in a future release
warning: [options] To suppress warnings about obsolete options, use -Xlint:-options.
3 warnings
Java compiler version 21 has deprecated support for compiling with source/target version 8.
Try one of the following options:
    1. [Recommended] Use Java toolchain with a lower language version
    2. Set a higher source/target version
    3. Use a lower version of the JDK running the build (if you're not using Java toolchain)
For more details on how to configure these settings, see https://developer.android.com/build/jdks.
To suppress this warning, set android.javaCompile.suppressSourceTargetDeprecationWarning=true in gradle.properties.
warning: [options] source value 8 is obsolete and will be removed in a future release
warning: [options] target value 8 is obsolete and will be removed in a future release
warning: [options] To suppress warnings about obsolete options, use -Xlint:-options.
3 warnings
Performing Streamed Install
Success
Starting: Intent { cmp=com.tauri_android.app/.MainActivity }

いけました!

長かった。。。。ていうかTauri関係ないやん。。。全部Androidのせい。

USBデバッグの設定

https://learn.microsoft.com/ja-jp/windows/wsl/connect-usb

https://zenn.dev/headwaters/articles/b9183cca7e7233

https://zenn.dev/thorie/articles/548mbl-android-studio-on-wsl2

このあたりを参考にやっていきます。

初期設定

WSL側に必要なツールをいれておく。

sudo apt install usbutils linux-tools-virtual hwdata
sudo update-alternatives --install /usr/local/bin/usbip usbip `ls /usr/lib/linux-tools/*/usbip | tail -n1` 20

Windows側からWSL側へUSBデバイスを転送できるように

PS> winget install --interactive --exact dorssel.usbipd-win

以降は毎回やるやつ。

sudo update-alternatives --install /usr/local/bin/usbip usbip `ls /usr/lib/linux-tools/*/usbip | tail -n1` 20

管理者権限のpower shell側から設定

> usbipd bind --busid 1-10
> usbipd.exe attach --wsl --busid 1-10
usbipd: info: Using WSL distribution 'Ubuntu-24.04' to attach; the device will be available in all WSL 2 distributions.
usbipd: info: Detected networking mode 'nat'.
usbipd: info: Using IP address 172.24.128.1 to reach the host.

確認する。

PS C:\Windows\system32> usbipd.exe list
Connected:
BUSID  VID:PID    DEVICE                                                        STATE
1-2    056e:013a  USB 入力デバイス                                              Not shared
1-10   04dd:9ca8  SH-M08, ADB Interface                                         Attached
1-11   26ce:01a2  USB 入力デバイス                                              Not shared
5-2    056e:1061  USB 入力デバイス                                              Not shared

Persisted:
GUID                                  DEVICE

よさそうなので、wsl2側から確認する。

$ lsusb
Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub
Bus 001 Device 007: ID 04dd:9ca8 Sharp Corp. SHM08
Bus 002 Device 001: ID 1d6b:0003 Linux Foundation 3.0 root hub

OK.

adbで認識されてるか確認する。

$ ./Android/Sdk/platform-tools/adb devices
* daemon not running; starting now at tcp:5037
* daemon started successfully
List of devices attached
357779091706398 unauthorized

# ここまで進めるとAndroid端末側でデバッガ許可するかどうか出てくるので、許可ボタンをおす。
# すると、認証済みになる。

$ ./Android/Sdk/platform-tools/adb devices
List of devices attached
357779091706398 device

ちなみに、USB抜き差しとかしてバインドしたのが増えすぎてきたらWindows側から消すとよい。

PS > usbipd.exe list
Connected:
BUSID  VID:PID    DEVICE                                                        STATE
1-2    056e:013a  USB 入力デバイス                                              Not shared
1-11   26ce:01a2  USB 入力デバイス                                              Not shared
5-2    056e:1061  USB 入力デバイス                                              Not shared

Persisted:
GUID                                  DEVICE
db8c4ff9-0b10-4ea5-9a2d-49a6b955975b  SH-M13

PS > usbipd.exe unbind --guid db8c4ff9-0b10-4ea5-9a2d-49a6b955975b

android端末への書き込み

https://v2.tauri.app/ja/distribute/google-play/を参考に。

ビルドする

npm run tauri android build
npm run tauri android build -- --apk

ログ

> tauri-android@0.1.0 tauri
> tauri android build --apk

    Running beforeBuildCommand `npm run build`

> tauri-android@0.1.0 build
> vite build

vite v6.2.1 building for production...
✓ 30 modules transformed.
dist/index.html                   0.46 kB │ gzip:  0.30 kB
dist/assets/react-CHdo91hT.svg    4.13 kB │ gzip:  2.14 kB
dist/assets/index-B3KA1C1_.css    1.37 kB │ gzip:  0.65 kB
dist/assets/index-CrD08m3M.js   144.84 kB │ gzip: 46.54 kB
✓ built in 423ms
   Compiling tauri-android v0.1.0 (/home/<wslユーザー名>/tauri-android/src-tauri)
    Finished `release` profile [optimized] target(s) in 5.29s
    Info symlinking lib "/home/<wslユーザー名>/tauri-android/src-tauri/target/aarch64-linux-android/release/libtauri_android_lib.so" in jniLibs dir "/home/<wslユーザー名>/tauri-android/src-tauri/gen/android/app/src/main/jniLibs/arm64-v8a"
    Info "/home/<wslユーザー名>/tauri-android/src-tauri/target/aarch64-linux-android/release/libtauri_android_lib.so" requires shared lib "libandroid.so"
    Info "/home/<wslユーザー名>/tauri-android/src-tauri/target/aarch64-linux-android/release/libtauri_android_lib.so" requires shared lib "libdl.so"
    Info "/home/<wslユーザー名>/tauri-android/src-tauri/target/aarch64-linux-android/release/libtauri_android_lib.so" requires shared lib "liblog.so"
    Info "/home/<wslユーザー名>/tauri-android/src-tauri/target/aarch64-linux-android/release/libtauri_android_lib.so" requires shared lib "libm.so"
    Info "/home/<wslユーザー名>/tauri-android/src-tauri/target/aarch64-linux-android/release/libtauri_android_lib.so" requires shared lib "libc.so"
    Info symlink at "/home/<wslユーザー名>/tauri-android/src-tauri/gen/android/app/src/main/jniLibs/arm64-v8a/libtauri_android_lib.so" points to "/home/<wslユーザー名>/tauri-android/src-tauri/target/aarch64-linux-android/release/libtauri_android_lib.so"
    Info symlink at "/home/<wslユーザー名>/tauri-android/src-tauri/gen/android/app/src/main/jniLibs/armeabi-v7a/libtauri_android_lib.so" points to "/home/<wslユーザー名>/tauri-android/src-tauri/target/armv7-linux-androideabi/release/libtauri_android_lib.so"
    Info symlink at "/home/<wslユーザー名>/tauri-android/src-tauri/gen/android/app/src/main/jniLibs/x86/libtauri_android_lib.so" points to "/home/<wslユーザー名>/tauri-android/src-tauri/target/i686-linux-android/release/libtauri_android_lib.so"
    Info symlink at "/home/<wslユーザー名>/tauri-android/src-tauri/gen/android/app/src/main/jniLibs/x86_64/libtauri_android_lib.so" points to "/home/<wslユーザー名>/tauri-android/src-tauri/target/x86_64-linux-android/release/libtauri_android_lib.so"

> tauri-android@0.1.0 tauri
> tauri android android-studio-script --release --target aarch64

<=========----> 74% EXECUTING [889ms]
    Finished `release` profile [optimized] target(s) in 0.12s
    Info symlinking lib "/home/<wslユーザー名>/tauri-android/src-tauri/target/aarch64-linux-android/release/libtauri_android_lib.so" in jniLibs dir "/home/<wslユーザー名>/tauri-android/src-tauri/gen/android/app/src/main/jniLibs/arm64-v8a"
    Info "/home/<wslユーザー名>/tauri-android/src-tauri/target/aarch64-linux-android/release/libtauri_android_lib.so" requires shared lib "libandroid.so"
    Info "/home/<wslユーザー名>/tauri-android/src-tauri/target/aarch64-linux-android/release/libtauri_android_lib.so" requires shared lib "libdl.so"
    Info "/home/<wslユーザー名>/tauri-android/src-tauri/target/aarch64-linux-android/release/libtauri_android_lib.so" requires shared lib "liblog.so"
    Info "/home/<wslユーザー名>/tauri-android/src-tauri/target/aarch64-linux-android/release/libtauri_android_lib.so" requires shared lib "libm.so"
    Info "/home/<wslユーザー名>/tauri-android/src-tauri/target/aarch64-linux-android/release/libtauri_android_lib.so" requires shared lib "libc.so"

> tauri-android@0.1.0 tauri
> tauri android android-studio-script --release --target armv7

<=========----> 75% EXECUTING [2s]
   Compiling tauri-android v0.1.0 (/home/<wslユーザー名>/tauri-android/src-tauri)
    Finished `release` profile [optimized] target(s) in 5.96s
    Info symlinking lib "/home/<wslユーザー名>/tauri-android/src-tauri/target/armv7-linux-androideabi/release/libtauri_android_lib.so" in jniLibs dir "/home/<wslユーザー名>/tauri-android/src-tauri/gen/android/app/src/main/jniLibs/armeabi-v7a"
    Info "/home/<wslユーザー名>/tauri-android/src-tauri/target/armv7-linux-androideabi/release/libtauri_android_lib.so" requires shared lib "libandroid.so"
    Info "/home/<wslユーザー名>/tauri-android/src-tauri/target/armv7-linux-androideabi/release/libtauri_android_lib.so" requires shared lib "libdl.so"
    Info "/home/<wslユーザー名>/tauri-android/src-tauri/target/armv7-linux-androideabi/release/libtauri_android_lib.so" requires shared lib "liblog.so"
    Info "/home/<wslユーザー名>/tauri-android/src-tauri/target/armv7-linux-androideabi/release/libtauri_android_lib.so" requires shared lib "libm.so"
    Info "/home/<wslユーザー名>/tauri-android/src-tauri/target/armv7-linux-androideabi/release/libtauri_android_lib.so" requires shared lib "libc.so"

> tauri-android@0.1.0 tauri
> tauri android android-studio-script --release --target i686

<=========----> 75% EXECUTING [9s]
   Compiling tauri-android v0.1.0 (/home/<wslユーザー名>/tauri-android/src-tauri)
    Finished `release` profile [optimized] target(s) in 5.76s
    Info symlinking lib "/home/<wslユーザー名>/tauri-android/src-tauri/target/i686-linux-android/release/libtauri_android_lib.so" in jniLibs dir "/home/<wslユーザー名>/tauri-android/src-tauri/gen/android/app/src/main/jniLibs/x86"
    Info "/home/<wslユーザー名>/tauri-android/src-tauri/target/i686-linux-android/release/libtauri_android_lib.so" requires shared lib "libandroid.so"
    Info "/home/<wslユーザー名>/tauri-android/src-tauri/target/i686-linux-android/release/libtauri_android_lib.so" requires shared lib "libdl.so"
    Info "/home/<wslユーザー名>/tauri-android/src-tauri/target/i686-linux-android/release/libtauri_android_lib.so" requires shared lib "liblog.so"
    Info "/home/<wslユーザー名>/tauri-android/src-tauri/target/i686-linux-android/release/libtauri_android_lib.so" requires shared lib "libm.so"
    Info "/home/<wslユーザー名>/tauri-android/src-tauri/target/i686-linux-android/release/libtauri_android_lib.so" requires shared lib "libc.so"
<=========----> 76% EXECUTING [18s]

> tauri-android@0.1.0 tauri
> tauri android android-studio-script --release --target x86_64

   Compiling tauri-android v0.1.0 (/home/<wslユーザー名>/tauri-android/src-tauri)
    Finished `release` profile [optimized] target(s) in 5.21s
    Info symlinking lib "/home/<wslユーザー名>/tauri-android/src-tauri/target/x86_64-linux-android/release/libtauri_android_lib.so" in jniLibs dir "/home/<wslユーザー名>/tauri-android/src-tauri/gen/android/app/src/main/jniLibs/x86_64"
    Info "/home/<wslユーザー名>/tauri-android/src-tauri/target/x86_64-linux-android/release/libtauri_android_lib.so" requires shared lib "libandroid.so"
    Info "/home/<wslユーザー名>/tauri-android/src-tauri/target/x86_64-linux-android/release/libtauri_android_lib.so" requires shared lib "libdl.so"
    Info "/home/<wslユーザー名>/tauri-android/src-tauri/target/x86_64-linux-android/release/libtauri_android_lib.so" requires shared lib "liblog.so"
    Info "/home/<wslユーザー名>/tauri-android/src-tauri/target/x86_64-linux-android/release/libtauri_android_lib.so" requires shared lib "libm.so"
    Info "/home/<wslユーザー名>/tauri-android/src-tauri/target/x86_64-linux-android/release/libtauri_android_lib.so" requires shared lib "libc.so"
    Finished 1 APK at:
        /home/<wslユーザー名>/tauri-android/src-tauri/gen/android/app/build/outputs/apk/universal/release/app-universal-release-unsigned.apk

どうも全アーキテクチャのものを作るらしいので、時間かかる。

デプロイの準備

ここを参考に、証明書を作って署名する https://developer.android.com/studio/publish/app-signing?hl=ja#generate-key

署名の方法はこのあたり。 https://developer.android.com/tools/apksigner?hl=ja

署名用のキーストアを作る

AndroidStudioでAPKを選択して、jksファイルを生成する。 ~/keystore以下に指定したファイル名で出力される。今回はmykey.jksという名前にした。

アプリに署名する

さきほど作ったキーストアのjksファイルを使ってapksignerで署名する。 このコマンドを実行するにはjavaのパスが通ってる必要があるので、PATH変数に追加しておく。 export PATH=$PATH:/opt/android-studio/jbr/bin

~/Android/Sdk/build-tools/35.0.1/apksigner sign --ks ~/keystore/mykey.jks app-universal-release-unsigned.apk これでapkファイルに署名ができた。

自身のgoogleドライブにアップロードして、Android端末でそのファイルを指定すればインストールできる。

インストールできないときは、adbでインストールを試みることで細かい情報が得られる。

~/Android/Sdk/platform-tools/adb install -r "C:\Users\USERNAME\Downloads\app-universal-release-unsigned.apk"

このコマンドで失敗した場合は原因となるエラーログ出してくれる。 署名されてないとかなんとか。

このapkファイルをAndroid端末にインストールしてアプリを起動すると、こんな画面でました!

感想と今後の展望

AndroidでTauri動かしてみると久しぶりに感動しました!

C言語Hello Worldを印字したとか、アゼンブラでVRAMに直アクセスして1ドット打てたとか、あの時と同じ気分です。これでTauriだけでほぼすべてのデバイス向けのアプリを作れちゃう気がします。

実際問題そう単純にはいかなくて、デバイス固有の対応は必要になるんだろうなとは思っています。

すぐ思いつくやつだと、ファイルの保存とかがそうですね。PC上なら/tmpとかに一旦書いても怒られませんが、スマホというかアプリには固有の保存領域があってですね、みたいな。

そういう問題はありつつも、夢が広がってきた気がします。 次のステップに向けてアプリ作ってみたいと思います。 では今回はこれくらいで。

Scalaのお勉強

お久しぶりですです。かたりぃなです。 今回は関数型プログラミングのお勉強としてScalaをやってみたので、メモとして残したいと思います。

なぜ関数型プログラミング

GitHubCopilotなどのAIを使っていろいろと試してみています。 試していてなんとなく「AIさんも人間も理解しやすいプログラミング言語」をと思ったのがきっかけです。

色々試してみたところ、自分がAIと強調してプログラミングするなら、以下のような言語のほうが使いやすいなと感じました。

それぞれ簡単に説明してみます。

型システム

AIにコードを書いてもらう場合、それを人間が理解しやすいことが重要です。理解するのに手間がかかっていては自分が書いたほうが早いですから。。。

ALGOL系の言語で強い静的型付け言語の有名な例はRust,C/C++,C#などですね。 関数型ではScalaやF#,OCamlあたりもそうでしょうか。

AIによってソースコードを生成した場合、その意味を理解するには型が大きな助けになります。 弱い動的型付け言語の場合は、これをするのが少々難しいなと感じる場面がありました。

プログラミングパラダイム

正直どちのパラダイムでもいいかなと思うのですが、AIによるテストコードの生成は関数型のほうが一歩秀でてる気がしました。 というのも、副作用の排除やモジュールを細かく分割する設計思想とそれを支援する言語の機能がとても優れているためです。

オブジェクト指向で記述されたコードのテストを記述するときに必ずと言っていいほど議論にあがる

  • privateメソッドはテストすべきか否か
  • クラスの内部状態のテストはどこまで実施すべきか?

などの問題も、関数型プログラミングにおいては設計段階からおおむね排除できるというメリットを感じました。 もちろんオブジェクト指向にも大きなメリットがあって、直観的に理解しやすいという多大なるメリットがあります。 こればかりはどちらか一方のパラダイムが良いとするのではなく、状況によって使い分けていけばいいだろうと思います。

実行方式

インタプリタ方式の欠点は、動かしてみるまでそれが正しいかどうかわからないという点です。 そのためのテストだという話でもあるのですが、

それであれば、コンパイラが面倒を見てくれる言語のほうがありがたいです。 これは型システムと組み合わせると力を発揮してくれるもので、論理的な誤りをコンパイラが検出できるということです。 整数型しか受け付けない関数に文字列型を与えるなど、コンパイル段階で誤りに気づいて修正を行うことができます。 コンパイラでライフタイムも管理することでメモリ管理の問題を解決しようという思想がRustであると理解しています。

Scalaの概要

というわけで、これらの条件を満たす関数型言語として、環境も準備しやすいScalaをやってみました。 機能をざっとなめてみただけなので、たいしたものはありませんが、自分がどこまで理解できているかをアウトプットするのが大事だと思っています。

関数型言語として以下のような機能を備えているとのことなので、試してみました。

  • 高階関数
    • 関数を引数にとる関数
    • 関数を返す関数
  • 引数を変更する
    • カリー化
    • 部分適用
  • ジェネリクス
  • 関数合成
    • andThen
    • compose
  • 型クラスと暗黙のパラメータ
  • ケースクラス
  • パターンマッチ

それぞれ順にみていくことにします。 私のバックグランドとしてC/C++が母国語なので、それらに類する言語での類推で理解した内容を表現していきます。

高階関数

高階関数は関数を受け渡しできる関数です。関数型言語の説明で出てくる第一級関数とは?みたいな難しい話は置いといて、「引数に関数を渡せる」「戻り値で関数が返される」とでも思っておけばいいでしょう。

まずは関数を引数にとる関数です。 applyFnは第1引数に関数を、第2引数に配列をとり、配列の各要素に第1引数の関数を適応していきます。 コールバック関数ですね。

    // 関数を引数にとる関数
    def applyFn(f: Int => Int, x: Array[Int]): Array[Int] = {
        x.map(f)
    }

    // 関数を定義
    val double = (x: Int) => x * 2

    // 高階関数を呼び出す
    println("applyfn test")
    val result = applyFn(double, Array(1, 2, 3))
    println(result.mkString(","))

もう1つの高階関数は、関数を返す関数です。 整数値を与えて関数を呼び出すと、整数を受け取り整数を返す関数が生成されます。この関数は生成時に与えられた値を乗算する関数です。 関数オブジェクトを返す関数といったところでしょうか。

    // 関数を返す高階関数
    def CreateMultiplier(factor:Int) : Int => Int = {
        (x:Int) => x * factor
    }

    println("createMultiplier 3 test")
    val triple = CreateMultiplier(3)
    println(triple(10))

    println("createMultiplier 4 test")
    val quadruple = CreateMultiplier(4)
    println(quadruple(10))

考察: c++ではなかなかこういった操作がしづらいです。というのも、クラスのメソッドを関数は呼び出し規約(thiscall)が異なるため、こういった関数に渡すにはstd::bind()などを使って第一引数にthisポインタを束縛するなどのテクニックが必要になったり、関数を返す場合でもラムダでキャプチャしたローカル変数などはどうなるか等の注意が常に必要になるためです。

カリー化と部分適用

カリー化は、複数の引数をもつ関数について、引数を分割して渡せる形の関数にする操作です。 たとえばよくある2引数をとる関数 add(x,y) を通常呼び出すためには add(1,2)のようにするところですが、 この例のようにadd(x)(y)として記述することで引数を1つずつ渡せるようになります。 カリー化した関数の引数を一部分渡す操作は部分適用になる。。。はず。

object CurryFunction{
    def add(x: Int)(y: Int): Int = x+y

    println("curry test")
    val result1 = add(3)(4)
    println(result1)

    val addthree = add(3)(_)
    val result2 = addthree(4)
    println(result2)

    val addfour = (x:Int) => add(x)(4)
    val result3 = addfour(4)
    println(result3)
}

部分適用は複数の引数をとる関数の一部の引数を固定する操作です。 2引数をとる関数add(x,y)の引数の片方を固定することで、 3を足す関数、4を足す関数を生成しています。

object PartialFunction{
    def add(x:Int, y:Int): Int = x+y

    println("partial function test")
    val addThree = add(3, _: Int)
    val result1 = addThree(4)
    println(result1)

    val addFive = add(_: Int, 5)
    val result2 = addFive(4)
    println(result2)

}

このあたりの機能はC++14くらいからのtemplateとかstd::bindを駆使すれば実現できていた気がしますが、もう覚えてないです。 関数を加工するという記述のしやすさはScalaのほうが圧倒的に優れていると思いますので、さすが関数型言語といったところでしょうか。

ジェネリクス

C++でいうテンプレートですね。

使っている記号が <> なのか[]なのかという違いはありますが、本質的にはいわゆるジェネリクスです。

class CustomStack[A]{
    private var elements: List[A] = Nil

    def push(element: A): Unit = {
        elements = element :: elements
    }

    def pop(): Option[A] = {
        elements match {
            case Nil => None
            case head :: tail => {
                elements = tail
                Some(head)
            }
        }
    }

    def peek: Option[A] = elements.headOption
    def isEmpty: Boolean = elements.isEmpty
}

関数合成

いまいちメリットがわかってないのですが、関数型言語ではを合成できます。 加算を行う関数addと乗算を行う関数mulをさまざまな方法で合成しています。

andThencomposeの違いは順序のみのようです。 ALGOL系の言語のような手続き型的に「実行順序が異なる」と解釈するのはあまりよくなくて、 2つの関数add()とmul()を合成した関数、すなわち「合成した関数」として解釈するのが良いようです。 (やっぱり何がうれしいのかわからない)

    def add(x:Int, y:Int): Int = x + y
    def mul(x:Int, y:Int): Int = x * y

    val addAndMul = (x:Int, y:Int) => mul(add(x, y), y)

    // 以下2つは同じ意味になる。
    // andThenを使って関数合成をする
    val addThenMul = (x: Int, y: Int) => ((add _).curried(x) andThen (mul(_, y)))(y)

    // composeを使って関数合成をする
    val mulComposeAdd = (x: Int, y: Int) => ((mul _).curried(y) compose (add(_, x)))(y)

    // 手続き的な記述
    def proceduralAddAndMul(x: Int, y: Int, z: Int): Int = {
        val sum = add(x, y)
        mul(sum, z)
    }

    // 関数型プログラミングの利点を示す例
    def combineFunctions[A, B, C](f: A => B, g: B => C): A => C = f andThen g

    val addThenMulFunction = combineFunctions(add(2, _), (x: Int) => mul(x, 3))
    val result = addThenMulFunction(4) // (2 + 4) * 3 = 18

    // 引数を3つ取る関数を関数合成を使用してvalで定義
    val addThenMulFunction3 = (x: Int, y: Int, z: Int) => {
        val addCurried = (add _).curried(x)
        val addThenMul = combineFunctions(addCurried, (r: Int) => mul(r, z))
        addThenMul(y)
    }

コメントにも記載しましたが、手続き的な記述で合成する場合と、andThenで合成するのとで全く同じ結果が得られるので、何がメリットなのだろう?といった状態です。 おそらくですが、Rustの場合はand_thenではOptionalやResultでラップされた値を返す関数を結合するのがとてもラクになるので、Scalaの場合でもそのあたりのメリットがあるのかなと思ってます。(OptionalとEitherの取り扱いがラクになる的な?)

型クラスと暗黙のパラメータ

難しく記述したポリモーフィズムに見えますが、いわゆる静的ポリモーフィズムです。 C++でいうところのテンプレートの特殊化を使ったパターンが近いでしょうか。 intShowの定義とstringShowの定義によって、型によってパターンマッチして分岐しているイメージです。

trait Show[A]{
    def show(a:A): String
}

object ShowInstances{
    implicit val intShow: Show[Int] = new Show[Int] {
        def show(a: Int): String = s"Int: $a"
    }

    implicit val stringShow: Show[String] = new Show[String] {
        def show(a: String): String = s"String $a"
    }
}

object Show{
    def show[A](a: A)(implicit s:Show[A]): String = s.show(a)
}


object TypeClass{  
    import ShowInstances._
    def printShow[A](a:A)(implicit s:Show[A]): Unit = {
        println(s.show(a))
    }

    printShow(123)
    printShow("abc")
}

import example.TypeClass.ShowInstances._
TypeClass.printShow(123)
TypeClass.printShow("abc")

ケースクラス

タプルっぽい何か

case class Person(
    name: String,
    age: Int
)
val person = Person("Alice", 25)
println(person)
println(person.name)
println(person.age)

パターンマッチ

関数型言語でよくあるやつですね。

  def matchTest(x: Any): String = x match {
    case 1 => "one"
    case "two" => "two"
    case y: Int => s"scala.Int: $y"
    case _ => "many"
  }

感想と今後の展望

関数合成のところでメリットがよくわかりませんでした。 大昔にCの次はC++だということで学び始めて「構造体の中に関数が入って何がうれしいの?」って思ってたのと同じ感覚です。今はわからなくても、きっとどこかで道が開けるのでしょう。

Scalaをざっと触れてみましたが面白い言語だなという感想です。 私はやはり強い静的型付け言語が好きなのだと思います。

まだまだ知らないことばかりですが、AIを使うのが当たり前な世界になっていくことは想像に難くないので、次の武器を磨きつつ、来るべき時代に備えましょう。 火器が主役の戦場に刀や弓で突入するのはあまりにも無謀です。

もしAIを使う未来が来なかったとしても、それまでに学んだことは役に立つはずなので、いろいろ試していきたいと思います。 次の武器はこんな感じで考えてます。状況に応じて武器は持ち替えていけるようにしておきたいですね。

用途 プログラミング言語
サーバーサイド(バックエンド) Scala, Rust
クライアントサイド(フロントエンド) TypeScript,React
スマホ Rust/JS(tauri)
組み込み Rust,C++

それでは今回はこれくらいで。

Rustでgltfをパースしてみた

お久しぶりですかたりぃなです。 今回はRustの勉強をするために適当なパーサを書いてみたいと思います。 いわゆる木構造的なやつを自力でパースすることでRustともっと仲良くなろうという試みです。

お題としてgltfをパースしてみようと思います。

ざっと書いたコードはこちら。 https://gist.github.com/javoren/dece41a559a1f9d5346d303b1d05d521

gltf is 何?

3DCGのデータ格納フォーマットの一つです。

https://www.khronos.org/api/index_2017/Gltf

理解した範囲ではgltfはこんな感じみたいです。

  • 先頭12バイトがファイルヘッダ
  • 後続に2つのチャンクがある
    • chunk0はテキスト形式。JSONが格納されている。
      • 一般的な3DCGの概念レベルが表現される
    • chunk1はバイナリ形式。chunk0のJSONから参照され、ジオメトリなどの実際のデータが表現される。
      • 頂点や法線、マテリアルなどの実データを表現している

このフォーマットのJSONの理解で手間取ったのが次の3つでした。 次のように理解しました。

  • Buffer
    • chunk1のバイナリデータを複数のBufferに分割する
  • BufferView
    • Bufferを複数のViewとして分割する
  • Accessor
    • 上記BufferViewの指し示すバイナリの解釈を与える
      • たとえば頂点や法線データであれば「32bit浮動小数点形式, VEC3型, 頂点数はN個」など

BufferとBufferViewは多対多関係っぽいですね。ぱっと思いつく使い道としてはパーティクル出すときのポリゴン形状は同じだから使いまわしたいとかでしょうかね。 プリミティブ情報はすべての粒子で共有できるはずなので。

というわけで動作確認用のデータとしてBlenderで適当なキューブのみのシーンをエクスポートしてみました。 次のとおりです。

gltfのすべての要素を実装するのは大変なので、まずはこれをうまくパースしてレンダリングするための情報を抽出するところまで実装してみます。

{
    "asset":{
        "g|enerator":"Khronos glTF Blender I/O v4.3.47",
        "version":"2.0"
    },
    "scene":0,
    "scenes":[{"name":"Scene","nodes":[0]}],
    "nodes":[{"mesh":0,"name":"Cube"}],
    "materials":[
        {
            "doubleSided":true,
            "name":"Material",
            "pbrMetallicRoughness":{
                "baseColorFactor":[0.800000011920929,0.800000011920929,0.800000011920929,1],
                "metallicFactor":0,
                "roughnessFactor":0.5
            }
        }
    ],
    "meshes":[
        {
            "name":"Cube",
            "primitives":[
                {
                    "attributes":
                    {
                        "POSITION":0,
                        "NORMAL":1,
                        "TEXCOORD_0":2
                    },
                    "indices":3,
                    "material":0
                }
            ]
        }
    ],
    "accessors":[
        {
            "bufferView":0,
            "componentType":5126,
            "count":24,
            "max":[1,1,1],
            "min":[-1,-1,-1],
            "type":"VEC3"
        },
        {
            "bufferView":1,
            "componentType":5126,
            "count":24,
            "type":"VEC3"
        },
        {
            "bufferView":2,
            "componentType":5126,
            "count":24,
            "type":"VEC2"
        },
        {
            "bufferView":3,
            "componentType":5123,
            "count":36,
            "type":"SCALAR"
        }
    ],
    "bufferViews":[
        {"buffer":0,"byteLength":288,"byteOffset":0,"target":34962},
        {"buffer":0,"byteLength":288,"byteOffset":288,"target":34962},
        {"buffer":0,"byteLength":192,"byteOffset":576,"target":34962},
        {"buffer":0,"byteLength":72,"byteOffset":768,"target":34963}
    ],
    "buffers":[
            {"byteLength":840}
    ]
}

ファイルを読み込む

Rustでのファイル処理は色々あるみたいですが、今回のテスト用ファイルはサイズもたいしたことないので一旦全部読んでから処理してみます。 たとえばファイルヘッダは bytes クレートを使って次のように解釈できます。

    let mut file = File::open("./test.glb")?;
    let mut buf = Vec::new();
    let _ = file.read_to_end(&mut buf)?;

    let mut p = &buff[..];
    let m1 = p.get_u8();
    let m2 = p.get_u8();
    let m3 = p.get_u8();
    let m4 = p.get_u8();
    let magic:[u8;4] = [m1,m2,m3,m4];
    let version:u32 = p.get_u32_le();
    let length = p.get_u32_le();

チャンク分割も同様に処理してみます。

// result = チャンクヘッダstruct
fn parse_chunk(buff:&[u8]) -> Result<(ChunkHeader, Box<dyn std::error::Error>>{
    let mut p = buff;
    let length = p.get_u32_le() as usize;
    let t1 = p.get_u8();
    let t2 = p.get_u8();
    let t3 = p.get_u8();
    let t4 = p.get_u8();
    let magic:[u8;4] = [t1,t2,t3,t4];
    let body = &buff[4*2..4*2+length];
    Ok(
        ChunkHeader{
            length: length,
            chunk_type: magic,
            body: Vec::from(body),}
    )
}

bytesクレートについて

今回使用したクレートはこちら。

https://docs.rs/bytes/latest/bytes/index.html

バイナリファイルの扱いをラクにしてくれました。

たとえば

    let mut p = &buff[..];
    let m1 = p.get_u8();
    let m2 = p.get_u8();
    let m3 = p.get_u8();
    let m4 = p.get_u8();

みたいな記述ができるのは嬉しいです。

またバイトオーダーや幅についても

    let version:u32 = p.get_u32_le();
    let length = p.get_u32_le();

のように記述できるのは嬉しいポイントです。 Rustでもバイナリを簡単に取り扱えるのはうれしい限りです。

JSONパース

ここまででgltfの大枠が解析できました。

次はchunk0のJSONをパースしてみます。 自力でJSONパーサを全部記述するのは大変なので、serde_jsonを使ってみます。

from_strの戻り値をserde_json::Valueとすることで、ツリー構造が格納されます。

        let json_str = str::from_utf8(&chunk0_header.body)?;
        let json_obj:serde_json::Value = serde_json::from_str(&json_str)?;
        println!("json={:?}", json_obj);
        println!("");

        // jsonの要素を取得するテスト2パターン試す。
        // パターン1, 既知のキーを指定して取り出す
        let asset = &json_obj["asset"];
        println!("asset = {:?}", asset);

これをパースするには各要素をトラバースするコードを書けばいいだけでした。

fn parse_json(value: &serde_json::Value) -> (){
    match value {
        Value::Null => (),
        Value::Bool(_b) => (),
        Value::Number(_num,) => (),
        Value::String(_str) => (),
        Value::Array(_v) => parse_json_array(_v),
        Value::Object(_object) => parse_json_object(_object),
    }
}

fn parse_json_array(v: &Vec<serde_json::Value>) -> () {
    for element in v{
        parse_json(element);
    }
}

fn parse_json_object(obj : &serde_json::Map<String, Value>) -> (){
    println!("object");
    for (k,_v) in obj{
        println!("key={:?}", k);
        parse_json(_v);
    }
}

とりあえずこれでobjectとそのkeyがわかります。足りない箇所を肉付けしていけば何でもできそうな気がしてきますね。

jsonをパースしてstructに格納する

上記はシンプルな機能ゆえに手軽に利用できますが、エラーハンドリングなどを考え始めると色々と面倒なことが増えてきます。 実際にはstructを定義してserde_jsonで自動的にやってもらうのがよさそうです。

詳細は公式ドキュメント https://docs.rs/serde_json/latest/serde_json/

今回の実験用JSONに含まれている要素だけざっと記述するとこうなりました。 全体は長すぎるのでルート要素とasset要素だけ示します。

#[allow(dead_code,non_snake_case,non_camel_case_types)]
#[derive(Deserialize, Debug)]
pub struct gltfJson{
    asset:asset,
    scene:u32,
    scenes: Vec<Scene>,
    nodes:Vec<Node>,
    materials: Vec<Material>,
    meshes: Vec<Mesh>,
    accessors: Vec<Accessor>,
    bufferViews: Vec<BufferView>,
    buffers: Vec<Buffer>,
}

#[allow(dead_code,non_camel_case_types)]
#[derive(Deserialize, Debug)]
struct asset{
    version: String,
    generator:Option<String>,
    copyright:Option<String>,
}
~~~~

serde_jsonのfrom_strでパースした結果を受け取る型を明示する形で使えます。

    let json_str = str::from_utf8(&body)?;
    let gltf:gltfJson = serde_json::from_str(&json_str)?;
    println!("gltf = {:?}", gltf);

gltf規格に記述されている構造やフィールド名をそのまま記述していくだけなのでお手軽です。 今回のポイントは次のとおりです。

  • rustではコーディングルールを満たしていないとコンパイラが警告を出すので、意図したものだということをnon_camel_case_typesなどで指定しています。
  • gltf規格で必須ではないフィールド(この例ではgeneratorとcopyright)はOptionで表現する
  • JSONの子要素も同様にして記述していける。
  • JSONのarrayはVecで対応

structで構成された木構造を巡回する

このデータ構造を巡回してレンダリングすることを考えます。 実際に3DCGライブラリ叩いたりとかし始めると本題からそれてしまうので、レンダリングに必要なデータをコンソールにprintするだけにとどめます。

今回パースしたgltfをレンダリングすることを考えた時、次のようになりました。 ただし、この形はエラーを握りつぶしているので良くないです。if let Ok()自体は便利なのですけどね。。。

pub fn render(gltf:GltfComponent) -> Result<(), Box<dyn std::error::Error> > {
    println!("render start!");
    if let Ok(nodes) = traverse_scene(&gltf.json) {          // レンダリング対象のシーンに含まれているノードの集合を得る
        nodes.iter().for_each(
            |node_num| {
                if let Ok(mesh) = traverse_node(&gltf.json, *node_num as usize){             // ノードすべてを見ていく
                    if let Ok(primitives) = traverse_mesh(&gltf.json, mesh){     // ノード中のメッシュ
                        primitives.iter().for_each(                                          // メッシュ中の全プリミティブ
                            |p| {
                                let _ = traverse_primitive(&gltf.json, &gltf.bin, p);
                            }
                        );
                    } else {
                        // エラー発生。どうする?
                    }
                } else {
                    // エラー発生。どうする?
                }
            }
        );

        println!("render finish!");
    } else{
        // エラー発生。どうする?
    }
    Ok(())
}

ここでコメントに記述しているようなエラーを呼び出し元に伝えるには?というシンタックスシュガーを使えます。

pub fn render(gltf:GltfComponent) -> Result<(), Box<dyn std::error::Error> > {
    println!("render start!");
    let nodes = traverse_scene(&gltf.json)?;                        // レンダリング対象のシーンに含まれているノードの集合を得る
    for node_num in nodes.iter(){
        let mesh = traverse_node(&gltf.json, *node_num as usize)?;      // ノードすべてを見ていく
        let primitives = traverse_mesh(&gltf.json, mesh)?;    // ノード中のメッシュ
        for primitive in primitives.iter(){
            let _ = traverse_primitive(&gltf.json, &gltf.bin, primitive);
        }
    }

    println!("render finish!");

    Ok(())
}

関数名(引数)?; の形で記述している箇所がエラーハンドリングのシンタックスシュガーです。 呼び出し結果がErr()の場合は呼び出し元にエラーを返し、https://gist.github.com/javoren/dece41a559a1f9d5346d303b1d05d521#file-gistfile1-txt-L273Ok()の場合は処理は継続されるというものです。

便利で可読性も向上していいですね。

ちなみにunwrap()を使う例もあったりしますが、あれはエラー時にその場でpanic!()するので意味合いが異なると思っています。

C++のエラーハンドリングとの対比で考えると

  • Rustには例外機構はないので、Result型戻り値を使うことで常系か異常系かを伝える
    • 呼び出し元がエラーを検知してさらに呼び出し元に異常を伝えるには?構文が使える
  • Rustのunwrap()は内部的にはC++の exit(-1)が近い挙動か。
    • 外見えの挙動としてはResult型戻り値のパターンマッチした結果の正常系の戻り値のみを返す。異常系の場合はexit(-1)

といったところでしょうか。 Resultの扱いはScalaでいうeitherも近い気がしますね。

Rustのジェネリクスまだよくわからない

gltfのバッファの解釈部分なのですが https://gist.github.com/javoren/dece41a559a1f9d5346d303b1d05d521#file-gistfile1-txt-L273 のように記述しています。

型が異なるだけで中身が同じ処理が並んでいるので、ジェネリクスで解決できないかなと色々試していたのですが無理でした。

具体的にやりたいこととしては

  • gltfアクセサのcomponent_typeに記述されている型と要素数をもとに、それを格納するためのメモリ領域(Rustの世界でのVec)を生成する
  • バイナリチャンクのbody部分を必要なだけ読み込む(前述の型に合わせて)

なので、簡易的な疑似コードで

template <typename T>make_vec() -> std::vector<T>{/*impl*/}
template <typename T>read_bin(std::vector<T>, std::vector<char> bindata) -> std::vector<T> {/* impl */}

// 実際に使うときは
auto actual_data1 = read_bin( make_vec<unsigned char>() , bindata);
auto actual_data2 = read_bin( make_vec<unsigned short>() , bindata);
auto actual_data3 = read_bin( make_vec<float>() , bindata);

みたいなことをしたかったのですが、うまくいきませんでした。 このあたりはまだよくわかってないので勉強が必要なところですね。

感想と今後の展望

Rustのいろいろな機能をつかって簡単なパーサを実装できました。 しかしながら納得いかない部分が多々あるので、今後も学習が必要ですね。 一般的なオブジェクト指向的な書き方であればC++のようにできますが、ジェネリクスや関数型のような記述を織り交ぜようとすると詰まってしまいます。

思い返せば私自身、新しいプログラミングパラダイムを学ぶとき毎回こういう感じだったと思います。

まだまだ学ぶことは多いですが、今後も色々試してみたいと思います。 それでは今回はこれくらいで。