RustでmacOSのGUIアプリを作る(ついでにMoltenVKを使ってVulkanを動かす

2018/2/26にMoltenVKがオープンソース化してKhronosに吸収されてmacOS/iOS向けの公式SDKとして出たみたいなので[^mvk-pr]ちょいと触ってみました。そこでRustでObjective-Cと連携する必要があったので諸々のメモ。

Rustで書かれた既存のコードベース([ferrite](https://github.com/Pctg-x8/ferrite))をそのままmacOS上に持ってくるのがメインの目的であるためRustで触りました([appframe](https://github.com/Pctg-x8/appframe))。MoltenVK自体はそこそこ前からあって、Swiftでは触っている人も多いのでここでは解説しません。

## 環境

MacBook Pro (13-inch, 2017)

- OS: macOS High Sierra(10.13.3)
- GPU: Intel Iris Plus Graphics 650

## RustでFoundation/Cocoa APIを触る

macOSのAPI群はほとんどがObjective-Cのフレームワークとして提供されているため、Windowsや\*nixのように単純にFFIで持ち込むことができません。ただ、Objective-Cのランタイム自体はFFIライブラリが存在するので([objc](https://crates.io/crates/objc))、それを経由して色々もちこみます。ということをしたのが[Pctg-x8/appframe:src/appkit.rs](https://github.com/Pctg-x8/appframe/blob/master/src/appkit.rs)になります。今回必要になる`CAMetalLayer`や`CVDisplayLink`などのほか、基本として必要になる`NSApplication`や`NSWindow`などをnewtypeし、一部のメソッドをRustのメソッドとして包み込んでいます。`*mut Object`はObjective-Cにおける`id`と等価です。
Objective-CのオブジェクトはCOMと同じく参照カウントを用いて管理されています。そのため、一部メソッドを標準ライブラリの`Rc`に合わせて、

- `release`は`Drop`
- `retain`は`Clone`

で包み込んでいます。

ちなみに、NSApplication周辺だけが必要な場合には[fruitbasket](https://crates.io/crates/fruitbasket)という選択肢もあります。こちらだとbundle状態でのリソースのパスなんかがいい感じにできるみたいです。ただ、今回はAppDelegateを使いたくて、そうするとNSApplicationに直接アクセスする必要があるので使ってもあんま労力変わらないかなということで使っていません。

### RustでObjective-Cのクラスを実装する

AppDelegateやViewControllerなどをカスタマイズする際にObjective-Cのクラスを実装する必要が出てきます。objcではクラスの定義もサポートしているのでこれも可能です。関数は必ず

- `extern`がつくこと
- `&Object`(`&mut Object` `self`のこと)と`Sel`(セレクタ)のふたつを受け取ること

を守ればそれ以外はそこそこ自由に実装できます。オーバーライドを行う場合は同名のメソッドを追加してやれば大丈夫です。
ただ、メソッドを呼び出して追加するようにしているとあまり可読性がよろしくないので、マクロか何かでDSLを定義してやると読みやすくメンテナンス性も向上すると思われます。

```rust:DSLの例
DeclareObjcClass!{
    // クラスを宣言する。継承は:
    class FeRenderableViewController : NSViewController
    {
        // インスタンス変数の定義。add_ivarに置き換えられる
        ivar server_ptr: usize;
        // メソッドの定義。add_methodに置き換えられる
        // インラインで定義はできないので`- <セレクタ> = <Rust関数>`で実体を与える
        - loadView = load_view;
        // selfを&mut Objectで受け取るための構文
        - mut dealloc = Self::dealloc;
    }
}

vvv 展開するとこう vvv

let parent = Class::get("NSViewController").expect(...);
let mut d = ClassDecl::new("FeRenderableViewController", parent).expect(...);
d.add_ivar::<usize>("server_ptr");
unsafe
{
    d.add_method(sel!(loadView), load_view as extern fn(&Object, Sel));
    d.add_method(sel!(dealloc), Self::dealloc as extern fn(&mut Object, Sel));
}
d.register()
```

## rpath

Rustではrustc-link-searchで指定したサーチパスがバイナリのrpathとして追加されないようで、ビルドスクリプト経由でSDK内のフレームワークパスを指定している場合ロードに失敗します。link_argsを使ってrpathを指定してあげればロードできるようになりますが、諸事情でstableしか使えない場合はビルド後のバイナリをいじってライブラリが見えるようにすれば同じようにロードができるようになります。

`$VK_SDK_PATH`にVulkan SDKのルートディレクトリ(ApplicationsやMoltenVKフォルダがある階層)を指定した状態で、

```sh
install_name_tool -change @rpath/vulkan.framework/Versions/A/vulkan @executable_path/vulkan.framework/Versions/A/vulkan target/debug/examples/with_ferrite
cp -r $VK_SDK_PATH/macOS/Frameworks/vulkan.framework target/debug/examples/
```

## 参考

- iOSの新グラフィックAPI - Metal入門してみる: https://qiita.com/edo_m18/items/0fd6971c98c27a125d6e
- CVDisplayLink によるアニメーション: http://d.hatena.ne.jp/hoshi_takanori/20111022/p3

[^mvk-pr]: https://www.khronos.org/news/press/vulkan-applications-enabled-on-apple-platforms