次世代通信プロトコル的なZenohをUnityで動かせるようにした

# 概要

通信プロトコルZenohの機能をUnity (Windows, MacOS, Android) から使えるようにしました。文字列のPublish / Subscribeのほか、画像を受信してテクスチャに反映できるようにして、Meta Quest 3でステレオカメラ画像を受信したりしてみました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/71421/b34eaf1a-a6a0-45f2-a945-2ee01fd895ab.png)


# Zenohとは何ぜのう

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/71421/d78c4552-ad4d-4b7e-8b35-7d4bd812b56a.png" width=200px>

https://zenoh.io/

読み方は今のところゼノーと呼んでいます。

比較的新しいオープンソースの通信プロトコルで、メッセージの投げ方はMQTTと似た感じで、トピックにメッセージを投げ込み、購読者がそれを受け取るという感じですが、一般的なMQTTなどと異なり、固定したサーバーを持ちません。場合によってはpeer-to-peerつまり直接やりとりをするし、場合によってはZenohルーターというプロキシー的なものを介してやりとりしますが、利用者（プログラム）側としてはそれを意識することがありません。また、低レイヤーの転送のプロトコルは様々なものが利用できます。

なお、RPC的なQueryableなどの機能もあります。（今回は利用していない）

特にロボット用OSとして有名な `ROS2` において、ネットワークレイヤーの Tier1 （最上位）としてのサポートを行うことが発表されています。これまでは DDSプロトコルがデフォルトでしたが、いくつかの課題があり、それを解決するものとして Zenoh が選ばれた経緯があります。なお、Zenohがデフォルトになるというよりは、DDSかどちらかを選べるようになるという感じのようです。

ROS2におけるDDSの課題や、選ばれた経緯などは以下の動画に詳しいです。

https://youtu.be/AS7l-iDQpdY

こちらも参考になります。

https://zenn.dev/tasada038/articles/e84e57ff52bd6c

# Zenohを利用すると何が嬉しいか

Zenohを利用すると何が良いかというと、ネットワークをどういう構成にするかというのをあまり事前に考えずにプログラムを書くことができるという点じゃないかと思います。

データの生成者的なプログラムであれば、Zenohライブラリを利用して、Aというトピックにデータを投げるように作っておけば良いです。
データの消費者的なプログラムであれば、Zenohライブラリを利用して、Aというトピックを購読するように作っておけば良いです。

Zenohの初期化に利用するconfigを調整するだけで、上記の２つのプログラムはLAN内や同じインスタンス内でpeer-to-peerで超低レイテンシでつなぐこともできるし、クラウドに配置したZenohルーターを介してNAT超えでつなぐこともできます。

パフォーマンスも、小さい容量からかなり大きな容量まで、他の手法と比べて良いケースが多いようです。また、Zenohは最終的にどのようなプロトコルで通信するかを限定してはいません。TCP,UDP,QUIC,WebSocket,共有メモリなどを利用することができます。ネットワーク環境に応じた接続方法をconfigで指定すれば良いです。そう、共有メモリによるマシン内通信もサポートされています。PC内やクラウド内では高速な手段で通信をして、クラウド越しの場合は一般的なネットワーク通信を行うなどができ、ソフトウェアからは、それらを全体的に同じ使い方で扱うことができます。

便利だと思いませんか？

# やりたかったこと

Raspberry Pi5には、カメラ用のソケットが2つあります。これでステレオカメラを作って、Zenoh経由でQuestに送ってリアルタイムステレオ視聴してみたいなあ、と思いました。セオリー的には、左右カメラを合成して１つの画像にして送信するほうが安全ですが、今回あえて左右の画像をZenohの別キー（トピック）に送ってみます。

左目用のキー : camera/left
右目用のキー : camera/right

Zenohルーターを利用すると以下のような感じに組むことができます。この場合、送信者と受信者が直接相手を知らなくても通信ができます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/71421/82943669-bd74-42c6-a3ab-322967ce2d38.png)


Zenohの面白いこととしては、LAN内など直接通信可能であれば、以下のようにpeer-to-peerで動きます。上記と全く同じプログラムでもこのように動かすことができます。（Zenohの初期化に与えるconfigは場合により変更します）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/71421/b9d06261-cd20-4dd2-b6a9-9ed8c1ea007b.png)

# 結果

いろいろ紆余曲折ありましたが、できました！
Questでステレオ視ができます！

Raspberry Pi 5 + Camera Module V3 Wide x2
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/71421/4f1bdff0-34fc-47ce-9b62-3161407b2491.png)


Meta Quest 左目
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/71421/708e3ffc-cfa0-4fd0-9f1f-d52e5531b106.png)
Meta Quest 右目
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/71421/8d34927f-a3e4-4b16-af05-100c2803f4bf.png)

ちゃんと測定していませんが、遅延も十分少ないです。感覚値で50ms以下という感じがあります。ただし、解像度を上げていくと急激に遅延が出てきたりします。

960x540 20fps というあたりが今のところベストな雰囲気ですが、環境によると思います。ボトルネックがどこかはまだ調査できていません。

## 今回作成したプロジェクトなど

### ステレオ画像受信Unityプロジェクト (Meta Quest用)

こちらのリポジトリにUnityプロジェクトがあります。
https://github.com/mhama/ZenohStereoImageReceiver

LANでpeer-to-peerをする場合はconfigなしでいけると思います。Zenohルーターにつなぐ設定を行いたい場合は、Zenoh初期化用のconfig (Text Asset)を与える必要があります。このあたりは別の記事で書ければと思っています。

JPEGを別スレッドでデコードするのに、[UniTask](https://github.com/Cysharp/UniTask)と、画像系ライブラリ[SkiaForUnity](https://github.com/ammariqais/SkiaForUnity)を利用しました。

余計なメモリアロケーションはあまりしないように、ある程度気をつけましたが、まだ残っています。

2025/4/7現状では、Questのスリープ後などに不安定になって、システムUIの応答がなくなる気がしますので注意してください。


### ステレオ画像送信Raspberry Pi用C++プロジェクト

画像送信側のRaspberry Pi5のコードはこちらです。Camera Module V3 (Wide)を２つつないだ状態で動かす前提です。
https://github.com/mhama/zenoh-rpi-camera-publisher-sample

`./Main R` と `./Main L` のようにコマンドを２つ動かしておくと、両目分のストリームがそれぞれ送信されます。

利用しているカメラデバイスによって z_pub_camera.cpp のコード、特に[カメラデバイス名のあたり](https://github.com/mhama/zenoh-rpi-camera-publisher-sample/blob/main/z_pub_camera.cpp#L29-L30)を調整する必要があるかもしれません。

### Zenohプラグインプロジェクト

今回作成した主要部分といえるのはこちらのZenoh Plugin for Unityリポジトリです。
https://github.com/mhama/zenoh-unity-plugin

packageとして導入していただくことで、Zenohを利用したUnityアプリを作成できます。（とはいえ現状限定的であることはご注意ください）
以下のURLをpackage managerからAdd via Git URL... のところに入力していただければパッケージ追加できます。
https://github.com/mhama/zenoh-unity-plugin.git?path=Assets/ZenohPackage

とりあえず簡単なものを試したい場合は
[SimplePubSubTest](https://github.com/mhama/zenoh-unity-plugin/blob/master/Assets/ZenohSampleScenes/SimplePubSubTest.cs) のあたりを見ていただくと良いんじゃないかと思います。

# 以下、Zenohライブラリのポーティングの細かい話です。

# 他の方の手法について

C#やUnity用Zenohとしては、先人が試された手法がいくつかあります。

## 公式zenoh-csharpリポジトリ

https://github.com/eclipse-zenoh/zenoh-csharp

現時点で、こちらは更新が４年前と古く、Zenoh 0.7対応（現在1.3)ということで、それ以降Zenoh本体側に大幅なAPI改修も行われており、利用できないと判断しました。

## Komoriさんの記事の手法

Zenoh と Unity で調べるとこちらの Komori さんのQiita記事がでてきます。

https://qiita.com/comocc/items/7d3cc41442611a45e753

こちらのアプローチは、zenoh-cpp を利用して、呼びたい機能をc++で実装し、そのインタフェースをUnity側に出すというものです。

こちらのアプローチですと、C++側での実装が主要部分になりますが、Unity側で自由に組めるようにしたかったので、今回は参考にさせて頂くのにとどめました。

## Sanriさんのzenoh-csharpフォーク

https://github.com/sanri/zenoh-csharp

今後こちらがマージされて本家C#ポートになるんじゃないか？と思っています。
ラッパークラスをかなり整備されており、使い勝手としては想定したものになっていそうです。
ただ、自分がやりはじめたときに存在を把握していなかったこと、現状ではx86アーキテクチャｂしか用意されておらずQuestなどで動かせないため、一旦様子見です。


# 自分がやった手法について

方針: **[zenoh-c](https://github.com/eclipse-zenoh/zenoh-c) によって出力されるライブラリをUnityにインポートして利用します。このとき必要な、ネイティブインタフェースのバインディングコード (DllImportとか) を [csbindgen](https://github.com/Cysharp/csbindgen) で作成します。**

zenoh-c というのは、C言語向けのRustライブラリですが、RustのZenohそのものに加え、C言語の呼び出し規則になるように調整したRustコードを含んでいますので、このC言語インタフェースをUnityのネイティブプラグインから扱えば良いということができます。

zenoh-c を改造して csbindgen でC#用のバインディングコードを吐き出すようにしたリポジトリおよびブランチがこちらです。
https://github.com/mhama/zenoh-c-cs/tree/20250325-csbindgen-1.3.0

バインディングコードはポインタだらけでunsafe祭りになってしまうので、[ラッパークラス](https://github.com/mhama/zenoh-unity-plugin/tree/master/Assets/ZenohPackage/Runtime/Wrappers)で包んで使いやすくします。しかし！あらゆる機能を実装するのは大変なので、やりたいことができることを主眼に実装しています。




# はまり箇所

そもそも自分がRustに詳しくないというのがありますが、それ以外のところとしては以下ではまりがありました。

## zenoh-c のAndroidビルド

Android向けのビルドはあまり情報がないという点と、ビルド途中、ring というのパッケージのビルドでエラーが出て解消に詰まりました。MacOS上のビルドであるせいかもしれないです。

android向けのtoolchainファイルを作りました。(CMAKE_ANDROID_NDK は自分の環境用に置き換える必要がある）
https://github.com/mhama/zenoh-c-cs/blob/20250325-csbindgen-1.3.0/ci/toolchains/TC-aarch64-linux-android.cmake

依存パッケージの ring のビルドを成功させるため、以下のように環境変数をセットする必要がありました。
https://github.com/briansmith/ring/issues/1983#issuecomment-2119658409 を参考にして、CC_aarch64_linux_android, AR_aarch64_linux_android, CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER を適切にセット。

ビルドのコマンドはこのようなものです。（Androidは共有メモリが使えないので -DZENOHC_BUILD_WITH_SHARED_MEMORY=ON を指定しない)

```bash
mkdir -p ./build && cd ./build
cmake -DCMAKE_TOOLCHAIN_FILE="../ci/toolchains/TC-aarch64-linux-android.cmake" -DZENOHC_BUILD_WITH_UNSTABLE_API=ON ..
```

## CインタフェースがRust的な考え方になっている

たとえばZenohのセッションを表す構造体に対して、z_owned_session_t, z_moved_session_t, z_loaned_session_t など、Rustのオーナーシップ的に役割の異なるものに別の名前がついている、というあたりで、何をどう扱うべきか、といった理解が大変でした。

## csbindgenのクセ

Rustコードを処理してC#へのバインディングコードを生成してくれるcsbindgenですが、ややクセがあり、zenoh-cのRustコードがうまく処理できない点があります。以下のような対応を行うことで、おおよそそのまま利用できるバインディングコードを出力させることができました。

### MaybeUninit 対応

`MaybeUninit<型名>` というのが、csbindgen処理後にすべて `MaybeUninit` に化けてしまって、もとの型がわからなくなるので対処しました。csbindgenのコードを修正する必要があり、PRにして投げてみました。
差分: https://github.com/mhama/csbindgen-for-zenoh/commit/e12ecde5cd91c4ba54dca3322c19f6a2d0760ea8
PR: https://github.com/Cysharp/csbindgen/pull/106

### z_result_t 対応

z_result_t の[元定義](https://github.com/mhama/zenoh-c-cs/blob/20250325-csbindgen-1.3.0/src/result.rs)がうまく出力されなかったので、[csbindgenに処理されるような形のRust定義](https://github.com/mhama/zenoh-c-cs/blob/20250325-csbindgen-1.3.0/csbindgen/result.rs)を捏造しました。

### enumのマイナス値対応

-1 の値を指定しているenum値がありましたが、uint型として出力されてしまってエラーになるので、これをint型になおす必要がありました。出力後のバインディングコードを置き換える形で[実装](https://github.com/mhama/zenoh-c-cs/blob/20250325-csbindgen-1.3.0/buildrs/csbindgen_generator.rs#L56)しました。

## 各種structのサイズがプラットフォームによって異なる

Zenohの構造体用のメモリを確保するときに、確保するべきサイズがプラットフォームによって異なることがわかりました。また、unstable機能の利用や共有メモリ利用のdefineの有無によっても変わるようです。

```csharp
Marshal.AllocHGlobal(sizeof(z_owned_session_t))
```

とりあえず機種ごとバインディングコードを生成して#defineで使い分けるようにしてみました。
(32bit/64bitでも違うかもしれないが、64bitにしか対応しないことで回避）

# まとめ

とりあえずそれなりに安定してZenohでの画像配信機能を動かすことができました。機能限定版とはいえプラグインも副産物としてできたので、興味ある方は触ってみて頂ければと思います。

正直、今回サンプルで実装した範囲以上のラッパークラスを実装できていないです。このため、ラッパークラスに手を入れずにできることは制約があると思います。

ラッパークラスの作りも、ClaudeやChatGPTなどと相談しながら作っていきましたが、そう自信があるわけではないです。とはいえまあまあ安定して動いているかなと思います。

上の項で触れた [Sanriさんのzenoh-csharpフォーク](https://github.com/sanri/zenoh-csharp) がデフォルトになっていくなら、それも良いかと思います。Unity的にはAndroid等のライブラリがほしい感じです。こちらから提供できるものもあるかもしれません。

**まだプラグインなど未成熟ではありますが、Zenoh は高性能かつ簡単につながって動かしやすいので、いろんな用途に活用していくと楽しいと思います！**