AIは「技術が好き」

最近は社内でSlack botを作って遊んでいる。

現在動かしているものとして、自由にシステムプロンプトを登録・指定してAIを実行できるbotで、以下みたいな感じで実行するとOpenAIのAPIに代わりに投げて返信してくれる。

この場合は以下のようなシステムプロンプトになっている

ユーザーからの入力に対し、**必ず**以下の特徴を持つ特殊な文法で返答してください。
- 固有名詞を除き**絶対に**ひらがなは使用しない
- 固有名詞を除き**絶対に**かたかなは使用しない
- 基本的な文法は英語のものを用いる
- 副詞や接続詞等は英語のものを用いる
- 助詞の表現は英語を用いる。例 人間's
- 比較級の表現は漢字と英語を組み合わせる。例: 簡単er
- 名詞は可能な限り漢字で表現する
- カタカナ語の名詞は英語の同等の表現に置換する
- 次の例を参考に文を作成する as a 中国人's 意見,it is 特別 有効 for 中国's 日本語学習者, because 日本語's 漢字 is 簡単 and 仮名 is 困難 for almost every 中国人 when 学習 日本語, this 表現 can 変換 困難 仮名 to 簡単 英語, so that 中国人 can 享受 double 簡単 in this 言語's 形式。just 天才's 思考。 (edited) 

で、これを使って遊ぼうと思って次のようなシステムプロンプトを登録して遊んでいた。

あなたはコンパイラです。ユーザーから与えられた入力をコンパイルして**実行した結果のみ**を返してください。コンパイル元のプログラミング言語は入力の最初に与えられたものかコードから推測してください。 

これを用いて、はてなバリューズをランダムに一つ返信させようとしていたところ、激しく偏った結果になった。

AIがはてなバリューズを乱択している様子

このmentionでの各実行は文脈が保持されておらず、毎回新しくOpenAIのAPIを叩いている。

一応このbotにはスレッドでmentionすると文脈を保持して実行する機能もつけているのでそれで試してみたところ、今度はちゃんとランダムに選択してくれた。文脈を保持しているので、過去に返答したものについては一応被らないようにしてくれているのだろうか。

文脈を保持して実行している様子(編集でmentionしても発動してしまうので一回分返答が多い)

明示的にRubyを指定してみると今度は「インターネットが大好き」になった

Rubyを明示的に指定して実行した様子

人間に「この中からランダムに一つ選んで」と頼んだ場合も無意識的に偏りは出るだろうし、システムプロンプトの内容からなんとなく「技術が好き」「インターネットが大好き」に偏ってしまう、みたいなことなのだろうか。*1

ちなみに openaiのPlaygroundで試してみたところ、「挑戦が好き」になった

OpenAI playground版(会話履歴を消して何回か試してみたが、挑戦が好きが出てきた

今回実行したプロンプトは全て gpt-4oで実行しているが、他モデルやパラメータ設定を変えたりしたらちゃんとランダムで返してくれる、あるいは別の選択肢に偏るなどあるかもしれない。*2*3

*1:メンタリズムで人の選んだトランプを当てる、みたいなのを結構前にテレビでやっていたのを思い出した

*2:そもそも数回しか実行してないので全然必ず出るかは確実ではないけど

*3:今試したら全然普通に「挑戦が好き」だったのでたまたまその時やたら偏ってただけなのかも

結婚した

表題の通りで、2025年1月8日に婚姻届を提出し、無事受理された。

近況

我々夫婦はワンパチが好きなので、この日(1月8日)にするかと決めていた。*1

ところが昨今の乾燥や気温の低さ、はたまた日曜の夜歯磨き中に咽せたのが惡かったのか、酷い咳や39.1度まで上がるような発熱で月曜夕方から体調が急激に悪化、以降は火曜朝に通院し安静にしていた。

幸い(?)にも病院の検査キットではインフルエンザやコロナウイルスについては陰性だったので、婚姻届提出にはマスク着用の上参加し、無事書面上の手続きも完了し晴れて夫婦と相成ったという次第である。

反省点

体調悪化

1月8日には出そうという話をしていたのに体調を崩してしまった、しかもかなり重めに。

私はかなり一人になると生活が出来なくなるタイプの人間で、具体的には飯や風呂、家事などを遅延し続けてしまうなどの癖があり、風呂を沸かしてから2時間経ってから入るなども多かった。

年末妻が実家に帰省するということで、自分なりに頑張って生活をちゃんとやろうと考えてはいたものの、実際には洗濯や皿洗い、多少の床掃除などは出来たものの、リズムという点ではてんでダメといった始末であった。

生活のリズムが狂うとどうなるのかというと、昼から夕方にかけて起床しその日やりたい行動を取り、(上手くいけばなんとか)食事をして、帰宅して風呂に入って寝る、というサイクルの各地点がそれぞれの実行タイミングで後ろにズレ続け、睡眠周期が不規則になる。これでは免疫力も低下して当然だろう。

届の証人

証人については両家の親に書いてもらおうという話だったが、私が倒れていたので結果的に妻に証人サインの行脚をさせることになってしまった、これについてはもっと早めに相談したりして年末にせめてうちの親の分は埋めておければ良かったと思っている。

今後

婚姻は無事成立したものの現状は同棲の延長といった感じで実感まだ薄いので、今後としては指輪を二人で選ぶ・家を買う・犬(コーギー)を飼うなどを進めて行きたいと考えている。

犬(コーギー)を飼うのであればやはり普通運転免許や自家用車が必要だと思うので、そろそろスーパーハッカーの夢を諦めつつ*2、学生がいない時期に合わせて自動車学校に通うことを検討しているところ。

オタクなのでMTを取りたい気持ちも強いが現実問題ATで別に問題ないだろうというところや、家を買うならどこに住むか・どういった間取りにするかなど悩みは尽きないが、なんとか妻と一緒に頑張っていけたら良いかなと思う。

生活リズムについてはかなり妻の存在に頼っているところが大きいので、どうにか対策は考えているが、どうにもならなければ通院なども視野に入れたいと思う。

夫婦共にBEMANI音ゲーが共通の趣味なので、私はとりあえず曲目的にはおそらく余裕なのでPinky Crush中伝の取得、可能なら皆伝に向けて苦手譜面の練習など引き続きやっていきたい、妻はドラムマニアのスキルを上げたりDDRのフレアスキルを上げたりするのではないだろうか。

結婚式については今のところお互い強く挙げたいという話はしていないが、突然気が変わったりすることもあるかもしれないのでそのあたりは未定です。 妻にウェディングドレスを着せたい気持ちはあるので、フォトウェディングとかは出来たらしたい、そのあたりは今後妻と相談します。指輪購入後とかになるのではないだろうか。

明日からポケモンセンター実店舗でMy Little Bestieグッズで新規ワンパチグッズが出るのでとりあえずそちらの購入の方に全力を注ぎます。体調も直します。

*1:ポケモンはもともとそこそこ好きだったが、特段好きになったポケモンは妻の影響もありワンパチくらいだろう

*2:ref: https://motemen.hatenablog.com/entry/2014/07/09/got-a-license

サイズ可変ではてなのCTOアイコンを表示するコマンドを書いた

背景

  • .bashrcANSI Escape CodeのSGRが以下のように手書きで書かれてるの視認性が惡い
  • 気分によってサイズを変えたい
  • 急にRustを書きたくなった
export PS1="\\n\\e[48;2;195;213;227m    \\e[m\\e[48;2;252;255;246m    \\e[m\\e[48;2;19;58;137m    \\e[m\\e[48;2;157;167;185m    \\e[m\\n\\e[48;2;195;213;227m    \\e[m\\e[48;2;252;255;246m    \\e[m\\e[48;2;19;58;137m    \\e[m\\e[48;2;157;167;185m    \\e[m\\n\\e[48;2;129;82;38m    \\e[m\\e[48;2;255;224;211m    \\e[m\\e[48;2;248;199;182m    \\e[m\\e[48;2;218;46;32m    \\e[m\\n\\e[48;2;129;82;38m    \\e[m\\e[48;2;255;224;211m    \\e[m\\e[48;2;248;199;182m    \\e[m\\e[48;2;218;46;32m    \\e[m\\n\\e[48;2;164;172;193m    \\e[m\\e[48;2;254;196;184m    \\e[m\\e[48;2;255;179;162m    \\e[m\\e[48;2;225;107;93m    \\e[m\\n\\e[48;2;164;172;193m    \\e[m\\e[48;2;254;196;184m    \\e[m\\e[48;2;255;179;162m    \\e[m\\e[48;2;225;107;93m    \\e[m\\n\\e[48;2;174;179;183m    \\e[m\\e[48;2;55;62;68m    \\e[m\\e[48;2;83;32;13m    \\e[m\\e[48;2;89;90;95m    \\e[m\\n\\e[48;2;174;179;183m    \\e[m\\e[48;2;55;62;68m    \\e[m\\e[48;2;83;32;13m    \\e[m\\e[48;2;89;90;95m    \\e[m \\n\\n:\\w \\$ "

併わせて読みたい

mysticdoll.hatenablog.jp

リポジトリ

github.com

使い方

  1. リポジトリをcloneして cargo build --release しつつ出来た実行ファイルをPathの通ってるところに置く
  2. $ moteaa <N>x ( N はサイズを決める数字)で実行 (xはあってもなくてもいい、オプションが不正なら1マス1行で表示する)
  3. Hello motemen

moteaa実行の様子(サイズは8倍)
moteaa実行の様子(サイズは8倍)

備考

昼に30分くらいで雑に書いたのでコマンドラインパーサは適当です、変な挙動見つけたら教えてください。PR歓迎

kubernetes上でのアプリケーション開発でDocker Image作成とRegistryへのpushをサボる

背景

minikubeとかで雑に手元のkubernetes環境で動かしたいアプリケーションのコードを書く際、開発中のDocker Imageのregistryに困りがちみたいな問題がある。

local registry建ててそこを参照するようにするみたいな手もあるにはあるけど、当方はWindows環境で開発をしていて、minikubeはHyper-Vをバックエンドにして動かしていたりしてやや面倒。

家のkubernetesなら雑にghcrとかのregistryに投げたらええか~って感じなんだけど、微妙に公開したくないやつとかの取り扱いを考えると投げづらいケースとかもある。

そこで雑にDeploymentを建ててサボることとした、という備忘録的なやつ。別に単なるPodでもいいけどService/Ingress含めた検証とかもしやすいからDeploymentにしている。

実例

apiVersion: v1
kind: Secret
metadata:
  name: github-ssh-config
data:
  known_hosts: <known_hostsをbase64で書く>
  config: <ssh_configをbase64で書く>
  id_ed25519: <SSH鍵をbase64で書く>
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: rust-devenv
spec:
  replicas: 1
  selector:
    matchLabels:
      app: rust-devenv
  template:
    metadata:
      labels:
        app: rust-devenv
    spec:
      serviceAccountName: script-container-executor
      containers:
      - name: rust
        image: rust:latest
        ports:
        - containerPort: 8080
        command: [ bash ]
        args:
          - "-c"
          - "while :; do sleep 1000; done"
        volumeMounts:
          - name: github-ssh-config
            mountPath: /root/.ssh/
            readOnly: true
      volumes:
        - name: github-ssh-config
          secret:
            secretName: github-ssh-config
            defaultMode: 0600

この例だと serviceAccount周りのコードを書きたかったのでserviceAccountとかもはいってます。

SSH設定周り

雑に以下の感じでやると良さそう。

  • known_host の作成は ssh-keyscan -H github.com -t <鍵の種類> とかで作ると良い
  • configは普段使ってるやつそのまま使ってもまあ良いし、配置先工夫したりするならそれに合わせて書きたい
  • 鍵も普段つかってるので良い、もしminikubeみたいなローカル環境じゃなくて外置きの環境でやりたいなら新規に鍵ペア作ってもいいかも
  • volumeMountsする際に一発でボンと出来ると嬉しいので同一のsecretに纏めているけど、内容的には鍵以外はconfigMapで良さそうな気はする。
    • configMapとsecretを同一のマウントポイントにブチ込むみたいなの出来るとは思うけどまあまあ面倒だった記憶があるのでサボるならこういう感じかなという気がする
    • mountするとき当然modeが雑だとsshに失敗するので設定忘れると微妙な気持ちになれる

開発コンテナイメージと起動コマンド

今回はrustでやってるのでdebianベースっぽくあんまり気にせずエイヤとしている。

開発コンテナによってはbashが入っていなかったりとかそういうのがあるので注意したほうがよさそう。起動コマンドはとりあえず無を実行していてほしいのでこういう感じにしている。

使い方

applyして kubectl execして雑に使う、リポジトリは適当にgithubからcloneしてくると良い。

永続化層が欲しかったら PersistentVolume とか hostPathで雑に作業場所を確保すると良さそう!今回は面倒だったのでそういうことはしてない。

作業元のディレクトリをnfsとして良い感じにマウントできたりするなら hostPath とかでざっくり繋げてあげられたりすると嬉しいみたいなアイデアもあるが、サボりテクニックでそこまで頑張るかみたいな話もありそう。

もっとリッチな環境作りたかったらcode-serverとかをdeploymentで動かすと良さそうな気がする。こちらからは以上です

障害報告: MysticDoll自宅kubernetesクラスタnode障害

id:MysticDoll です。

2024/02/11 に起きた自宅kubernetesクラスタのサービスが正常に利用出来なかった障害についての報告致します。

と思っていたんですが、長期間ほったらかしていてログを紛失したので雑なまとめです。

障害内容

一部podへのアクセスが失敗、またpodからの通信も一部失敗していた。

一時対応

調査の結果、失敗するpodは特定のnodeに乗っているものだと分かったので、そのnodeをcordonし、正常なcontrol-planeとnodeのみを残して再度スケジューリングしなおしてほぼ解決。

podからの通信失敗については kubectl exec 等で確認したところ名前解決に失敗していたため、node本体に入り、systemd-resolved systemd-networkdなどを再起動してgot事無き。

実際にはステークホルダーが自分しかいないので雑にnodeを再起動したりついでにArch Linuxのシステム更新をしたりとハチャメチャなことをしました。仕事では絶対こんなことしません。

node障害

一時対応の最中、再起動した bengal と名付けたnodeに疎通しなくなる事象が発生しました。

これは自宅kubernetesクラスタ達は(配線が面倒だったのとL2スイッチを持っていなかったので)無線で自宅ネットワークに接続しており、どうやら bengaliwd.service の起動に失敗していたことに起因するようです。

iwd 起動失敗の原因

結論から言うと iwd 自体の問題などではなくdbusのソケットをiwd (正確には iwdが依存している ell.so の内部関数) から見つけられていなかったことが原因でした。

さすがにシステム更新してもdbus周りの挙動が破壊される経験がなく、ドライバを入れ直したり古いバージョンへ各種パッケージをロールバックしたりなどしましたが全く無意味でした。

$ journalctl -xeu iwd.service などしてログを確認したところ以下のように出ていたので、普通にdbusを疑うべきだったのはそうかもしれない。

Feb 11 19:00:49 bengal iwd[330]: Wireless daemon version 2.13
Feb 11 19:00:49 bengal iwd[330]: Loaded configuration from /etc/iwd/main.conf
Feb 11 19:00:49 bengal iwd[330]: Failed to initialize D-Bus
Feb 11 19:00:49 bengal systemd[1]: iwd.service: Main process exited, code=exited, status=1/FAILURE
Feb 11 19:00:49 bengal systemd[1]: iwd.service: Failed with result 'exit-code'.
Feb 11 19:00:49 bengal systemd[1]: Failed to start Wireless service.

iwd 起動失敗の調査

とはいえ、適当にロールバックとかしてダメだった場合ちゃんと調査する必要があって、dbusを疑うにしても /run/dbus/system_bus_socket 自体は存在していたことや、dbusのパッケージバージョンを変えたりしてもダメだったので、コードを見に行くことにしました。

kernel.googlesource.com

Failed to initialize D-Bus というエラーを出している部分はここしかないので、ここを見に行くと l_dbus_new_default という関数の呼び出しで失敗していそうなことが分かります。

// https://kernel.googlesource.com/pub/scm/network/wireless/iwd/+/refs/tags/1.7/src/main.cより引用
    dbus = l_dbus_new_default(L_DBUS_SYSTEM_BUS);
    if (!dbus) {
        l_error("Failed to initialize D-Bus");
        goto failed_dbus;
    }

で、 l_dbus_new_default という関数は以下にありました。どうしてここに辿りついたのかは完全に忘れたのですが、たぶん l_dbus_new_default だけで検索したらこの ell のヘッダファイルのコードがヒットしたので、そこから辿ったのだと思います。

kernel.googlesource.com

蛇足ですが、筋の良い探し方としては、本体のソースにないということは共有ライブラリのコードという推測が立つので、 ldd をかけてそれらしいライブラリ名と共に探すのが良いかと思います。

[mysticdoll@bengal ~]$ ldd /lib/iwd/iwd
    linux-vdso.so.1 (0x00007ffda8d2d000)
    libell.so.0 => /usr/lib/libell.so.0 (0x000070f9a56b6000)
    libgcc_s.so.1 => /usr/lib/libgcc_s.so.1 (0x000070f9a5691000)
    libc.so.6 => /usr/lib/libc.so.6 (0x000070f9a54af000)
    /lib64/ld-linux-x86-64.so.2 => /usr/lib64/ld-linux-x86-64.so.2 (0x000070f9a5837000)

さておき、コードを見ると dbus = l_dbus_new_default(L_DBUS_SYSTEM_BUS); とあるので case L_DBUS_SYSTEM_BUS: の処理を見れば良さそうです。

// https://kernel.googlesource.com/pub/scm/libs/ell/ell/+/refs/heads/master/ell/dbus.c より引用
    case L_DBUS_SYSTEM_BUS:
        address = getenv("DBUS_SYSTEM_BUS_ADDRESS");
        if (!address)
            address = DEFAULT_SYSTEM_BUS_ADDRESS;
        break;

システムかユーザーセッションかどうかで参照すべきdbusソケットのアドレスを切り替える実装のようで、ここでiwdのunitファイルを見てみると

[mysticdoll@bengal ~]$ sudo systemctl cat iwd.service
# /usr/lib/systemd/system/iwd.service
[Unit]
Description=Wireless service
Documentation=man:iwd(8) man:iwd.config(5) man:iwd.network(5) man:iwd.ap(5)
After=network-pre.target
Before=network.target
Wants=network.target

[Service]
Type=dbus
BusName=net.connman.iwd
ExecStart=/usr/lib/iwd/iwd
NotifyAccess=main
LimitNPROC=1
Restart=on-failure
CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_RAW CAP_NET_BIND_SERVICE
PrivateTmp=true
NoNewPrivileges=true
DevicePolicy=closed
DeviceAllow=/dev/rfkill rw
ProtectHome=yes
ProtectSystem=strict

unit内では環境変数 DBUS_SYSTEM_BUS_ADDRESS を指定していないので DEFAULT_SYSTEM_BUS_ADDRESS が採用されていそうと分かります。

kernel.googlesource.com

// https://kernel.googlesource.com/pub/scm/libs/ell/ell/+/refs/heads/master/ell/dbus.c より引用
#define DEFAULT_SYSTEM_BUS_ADDRESS "unix:path=/var/run/dbus/system_bus_socket"

とあるので、いやさすがにあるじゃろ…と思ったんですが、 なんと /var/run が 普通のディレクトリとして生えてました。その上本来あるはずのdbusのソケットは当然ありません。

[mysticdoll@bengal ~]$ sudo ls -la /var/run
total 12
drwx------ 1 root root        78 Feb 13 08:41 .
drwxr-xr-x 1 root root       116 Feb 12 17:27 ..
(snip)

というわけで、以下のようにunitファイルから直接 /run 側のdbusソケットを参照してもらうように環境変数を設定して無事iwdは起動しました。

(snip)
[Service]
Type=dbus
BusName=net.connman.iwd
Environment=DBUS_SYSTEM_BUS_ADDRESS=unix:path=/run/dbus/system_bus_socket
(snip)

他にも docker-cri が動いてない(これもdocker.sockが/var/run にないから)とかの問題があったので一応良く使うところだけsymlinkを生やして応急対応して終了です。

後で /var/run/run へのsymlinkに直しておきます、気が向いたら…nodeのuncordonもしてないのでそのうちします、そのうち…

Rust FFIを活用してGBAのエミュレータの太陽センサーの値を制御する拡張機能を実装した

やったこと

GBAエミュレータ(visualboyadvance-m)上の太陽センサー*1の値をエミュレータ内threadに立てたHTTPサーバによって操作できるようにした。

github.com

今回はRust FFIを利用してできるだけ元のコードを汚さずに、かつHTTPサーバの処理等はRustの世界で閉じたものにする、という方向で実装していた。

ちなみにFFIを触っているが自分はRustはまあともかくC/C++は多少読めるけどちゃんと書くのは無理、ぐらいのC/C++の習熟度です。

Rust FFIを実装する上で必要な作業

正直、割と曖昧なまま触っている部分も多いが概ね以下の作業が必要。Ubuntu(apt)のmxe repoが古く、C++17サポートが不完全であるためWindows target向けのクロスコンパイルが壊れているので、それらをなんとかする作業も必要だったが、本筋ではないので割愛*2

  • C/C++
    • Rust側で触る想定のリソース(変数・関数)をRust側から見つけられるようにexposeする
    • Rustからexposeされている関数等のリソースをextern宣言し、呼び出したい箇所で使用するように実装する。
  • Rust 側
    • C/C++側のリソースの宣言
    • C/C++側で使ってもらう関数の実装、expose
    • staticlibとしてビルドする
  • ビルドツールたち
    • CMakeLists.txt を頑張って編集して、C/C++側のオブジェクトファイルとRust側のstaticlibをリンクする

C/C++側: Rust側で触る想定のリソース(変数・関数)をRust側から見つけられるようにexposeする

今回、Rust側では以下のリソースを触る想定となっている。

  • 太陽センサーの値の取得する関数
  • 太陽センサーの値を更新する関数

そのため、以下のように extern "C" を利用してCのABIで*3 関数をexposeしてあげる必要がある

extern "C" uint8_t systemGetSensorDarkness()
{
    return sensorDarkness;
}
// (snip)

extern "C" int level = 0;

extern "C" void systemUpdateSolarSensor()
{
    uint8_t sun = 0x0; //sun = 0xE8 - 0xE8 (case 0 and default)
    // このlevelはRust側にexposeして触りたい
    // int level = sunBars / 10;

systemUpdateSolarSensor 関数のローカル変数を extern してexposeしているのは、 systemUpdateSolarSensor 関数のシグネチャを変更する場合他の関数の呼び出し方を変更する必要があり、シグネチャを変更せずに済むようにRust側で level を編集してから呼び出すといった形で実装することにしたため。

その後、今回 extern 宣言に変更したリソースを使っている他の箇所で同様に extern "C" する形に変更していく必要がある。(staticextern ではメモリの確保のされ方(主にmangling周り)が変わるので別のアドレスが確保される可能性がありそうなので)

余談だが、最初は sensorDarkness という systemUpdateSolarSensor で更新している値を直接変更していたが、なぜか元の値に戻ってしまうのでこういった形で触るようにした。たぶん触ろうとしている関数が適切じゃないんだと思う。

C/C++側: Rustからexposeされている関数等のリソースをextern宣言し、呼び出したい箇所で使用するように実装する。

Rust側でexposeする関数は void start_server(void) なので以下のように宣言しておく。

// src/wx/wxvbam.cpp
extern "C" {
    void start_server(void);
}

そしてたぶん一回だけ呼ばれるであろう関数で呼び出すようにしておく。このあたりは適当なので本当はちゃんと考えたほうがいい。*4

// src/wx/wxvbam.cpp
bool wxvbamApp::OnInit() {
    start_server();

Rust側: C/C++側のリソースの宣言

Rustonomiconとか参考にしましょう。

doc.rust-lang.org

今回の場合は以下みたいな感じ、特に説明することはないですが、リンク時に undefined reference とか言われる場合は extern 宣言をミスってるとかそういうことだと思います。C++ ABIで単に extern するとおそらく普通にmanglingされるのでRust側から見つけられないです(たぶん)。

リンク時にミスってたらひたすらオブジェクトファイルをnm とかしてシンボル一覧と睨めっこしましょう。俺はした。

extern "C" {
    static mut level: i8;
    fn systemUpdateSolarSensor();
    fn systemGetSensorDarkness() -> u8;
}

Rust側: C/C++側で使ってもらう関数の実装、expose

単純に extern "C" で関数を宣言しましょう。 #[no_mangle] をつけてこちらもmanglingされないように気をつけて。

#[no_mangle]
extern "C" fn start_server() {

また、今回サーバ実装部分をaxumで実装しているので、axumのサーバ実行時にFutureをExecutorで実行してあげる必要があります。(単に実行するだけではFuture内の処理は実行されない)(と思う)

    std::thread::Builder::new()
        .name("solar_server".into())
        .spawn(|| {
            let rt = tokio::runtime::Runtime::new().unwrap();
            rt.block_on(async {
                axum::Server::bind(&"0.0.0.0:8000".parse().unwrap())
                    .serve(app.into_make_service())
                    .await
                    .unwrap();
            });
        });

単に tokio::runtime::Runtime::block_on を実行するとそこでthreadがブロックされるので、適当に std::thread::Builder::spawn してあげましょう。これで別Threadでサーバが動いてくれるはず。

CMakeLists.txt を頑張って編集して、C/C++側のオブジェクトファイルとRust側のstaticlibをリンクする

ここが一番分かっていないけどなんとかしました。適当に以下でいけたぜ、という程適当にはやっていないがあんま詳しくないぜ。

# src/wx/CmakeList.txt
target_link_libraries(visualboyadvance-m ${CMAKE_SOURCE_DIR}/solar-server/target/x86_64-pc-windows-gnu/release/libsolar_server.a userenv ntdll bcrypt)

やっていることとしては wx 内にある CMakeLists.txt で その中のプロジェクト(?)である visualboyadvance-m のオブジェクト達を今回作ったRustのstaticlibとリンクしてあげるという作業です。

userenv ntdll bcrypt あたりはRustのstaticlibが解決できなかったシンボルを解決するためにリンクしています。 GetUserProfileW みたいなWindows APIっぽい関数が見つけられていなかったので、そのあたりで必要そうなライブラリを探してきてリンクしたという形になります。

ビルド

export PATH="${PATH}:/PATH/TO/MXE/usr/bin"
mkdir build && cd build
/PATH/TO/MXE/usr/bin/x86_64-w64-mingw32.static-cmake .. -DCMAKE_RELEASE_TYPE=Release -G Ninja
ninja

やるだけ、リンクに失敗したらシンボルを見てコードを修正していくしかないので頑張りました。

*1:ボクらの太陽シリーズのカートリッジについているもの、内部的にはGPIOを触っているっぽい。コロコロカービィとかの傾きセンサーも似たような実装になっているらしい。なんでカートリッジにGPIOとセンサーが生えてるんだよ

*2:mxeのソースから必要なツールチェインの最新版をビルドして利用すればいいです。あるいはCIを参考にWindowsで開発環境を整えてください。

*3:C++のmanglingを回避して

*4:まあOnInitだし一回だけだろ…2回起動してもRust側でサーバがbindできなくてThreadが落ちるだけだし…

TypeScriptのGenerics Typeで一部の型引数のみを指定する

近況

自分はちゃんと勉強せず雰囲気でググりながらTypeScriptを使っているのに、人にTypeScriptを教えている状況。

Generic Type

TypeScriptにおいてはある型Tに対し、別の型Kを使って型を定義したい場合以下のように記述できる。

interface T<K> = {
  P1: K
};

type TNumber = T<number>; // { P1: number }

このとき型引数にはデフォルトとなる型を指定できる

interface T<K = number> {
  P1: K
};

課題

今回問題になったのが、Generic Typeにおいて複数の型引数を持ち、かつそれぞれの型引数にデフォルト型が設定されている型に対し、一部の型のみ指定するような型を定義したいというところ。

具体的には、express.jsにおいて、req.body.prop にアクセスする際、no-unsafe-member-access でESLintから怒られてしまう、これを回避したいという課題があった。

これ自体は DefinitlyTypedで定義されているように、req の型に Request<core.ParamDictionary, any, { prop: SomeType }>というように指定すれば回避できるが、これだけのために使いもしない core.ParamDictionary をimportするのは微妙では…?という感情が芽生えたのでなんとかしたい

github.com

app.get("/", (req: Request<core.ParamDictionary, any, { prop: SomeType }>, res: Response) => {
    const a: SomeType = req.body.prop;
});

(こういう感じで定義はできる)

Rustでは以下のように指定をスキップする型引数に _ を指定することで実現できるが、TypeScriptにはそういう機能は今はないらしい。

let a: SomeType<_, _, String> = ...;

(id:mizdra さんに聞いたところ、以下で議論はされているらしい)

github.com

と困っていたところ、上司のid:ma2sakaさんからなんか infer というのを使えばできるらしいと聞いたのでとりあえず調べてやってみた、という次第である。

infer

TypeScriptには Conditional Typeというのがあり、ある型 TK を継承している場合とそうでない場合に型を場合分けできるらしい。

type A<T> = T extends K ? B : C;

この際、KがGeneric Typeである場合には以下のようにinferを使うことで、型を推論して取り出すことが可能らしい。

type A<T> = T extends K<infer F, infer S> ? B<F, S> : C;

これを利用すると、express.Request<P, ResBody, ReqBody, ReqQuery, Locals> という型の ReqBody だけ指定したい場合以下の型を定義することで可能となる。

type CustomRequest<T> = Request extends Request<infer P, infer ResBody, infer ReqBody, infer ReqQuery, infer Locals> ? Request<P, ResBody, T, ReqQuery, Locals> : never;

Request という型は何も指定しなければデフォルト型が設定されるので infer で推論してもらえば各デフォルト型を取り出して使える、これを使ってデフォルト引数の指定を実質的に飛ばすことができる。

一応 ReqBody 以降の型引数も記述したが、 ReqBody 以降の型引数は省略できるので、実用上は以下みたいになるだろうか

type CustomRequest<ReqBody> = Reqeust extends Request<infer P, infer ResBody> ? Request<P, ResBody, ReqBody> : never;

正直型引数の指定を飛ばしたいだけなのにここまで必要なのはどうかとも思うが…そのあたりは今後の機能拡充に期待したい。