【Unity】Swift 経験0から iOS と macOS のNativePluginを作る

## 追記 2023/10/12

Xcode 15 で swift-create-xcframeworkに不具合がありました。
https://github.com/unsignedapps/swift-create-xcframework/pull/93

以下の keijiro さんのメモの手順をやってみたところ動いたため以下推奨です！
(Bundleのプロジェクト作らなくていいし)
https://github.com/keijiro/Memo/blob/main/Pages/UnityNativePlugin.md

細かい手順はまた別の記事を書こうと思います

## 追記 2023/12/21

> 細かい手順はまた別の記事を書こうと思います

ビルド方法について書きました。
SwiftPackage から dylib を生成するため、本記事の Bundle 生成のためのプロジェクト生成の手順は不要です！

https://qiita.com/Teach/items/3002a728b76afe47746c

## 本記事について
ネイティブプラグインを初めて自作し、 iOS の Health Kit から歩数取得して表示するという実装ができた。その過程で得られたプラグインの作り方に関する知見を踏まえて、
知識0 の状態から NativePlugin を作れるまで手順をまとめる。

筆者はXcodeでの開発はほぼ初めて。

## 対象
Unity 開発経験あり
iOS 開発未経験
Makefile を使ったことがある

## 環境

Unity 2021.3.20f1
macOS Venture 13.3 (M2)
Xcode 14.3
(GitHub Copilot 慣れてない言語扱うときはおすすめ)

## NativePlugin が開発できるまでのステップ

- Xcode, Swift の基礎を学ぶ 
- macOS で HelloWorld!
- テスト用のアプリと macOS用のプラグインを作る
- iOS で HelloWorld!

## 準備
NativePlugin を作るためにある程度 Xcode や Swift に慣れた方が良い。
そのため何かしらの初心者向けの iOS アプリ開発の本を1冊やる。

筆者は以下の本をやった。

[SwiftUI対応 たった2日でマスターできる iPhoneアプリ開発集中講座 Xcode 14/iOS 16/Swift 5.7対応](https://www.amazon.co.jp/gp/product/B0BJQ6RN44/ref=ppx_yo_dt_b_d_asin_title_o06?ie=UTF8&psc=1)

だいたい以下ができれば良いと思う。

- 新規プロジェクトを作る
- プロジェクトの設定方法がわかる
- Swift の基本的な書き方がわかる
- SwiftUI で画面作れる
- 実機ビルドができる
- ログの見方がわかる。


## macOS で HelloWorld

Native Plugin で使える拡張子について

[Documentation / Import and configure plug-ins](https://docs.unity3d.com/2021.3/Documentation/Manual/PluginInspector.html)

ここに使える拡張子が載っている。が、特に使い分けは書かれていないためここに載っているものは使えるんだなぁぐらいの認識で良い。

以下の公式ドキュメントに macOS の NativePlugin について書かれている

[Documentation / Building plug-ins for desktop platforms](https://docs.unity3d.com/2021.3/Documentation/Manual/PluginsForDesktop.html)


ポイントを以下に示す。

> You can deploy macOS plug-ins as bundles

=> 拡張子は.bundle

>  For example: `[DllImport ("PluginName")] private static extern float ExamplePluginFunction ();`

こんな感じでC#から使う

> Note: PluginName should not include the library prefix or file extension (for example, the actual name of the plug-in file is PluginName.dll on Windows and libPluginName.so on Linux).

PluginName に拡張子不要


つまり、Xcode で `.bundle` を作成し、Unityにインポートした上でC#から呼び出せば良いということ。

ということでまずは `.bundle` を作成する

### .bundle の作成

Unityの公式ドキュメント、Apple Developerのドキュメントを見てもBundleの作り方の記事が見当たらなかったため、本章では試してできた作り方を紹介。

![スクリーンショット 2023-06-03 17.20.31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/113504/9ca5207c-1b50-76c0-49bd-6f1308b49935.png)

![スクリーンショット_2023-06-03_17_21_02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/113504/f1d9020d-96b7-60eb-4019-2792d32cb4b9.png)

macOS > Bundle でプロジェクトを作成

![スクリーンショット 2023-06-04 17.20.54.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/113504/4f018aea-41c5-f9b2-74eb-4050948e051f.png)

Organization Identifier はユニークであればいいため、`com.(GitHubのアカウント)`が無難なのではないかなと思っている。

ProductName は HelloPlugin にした(なんでも良い)。

![スクリーンショット 2023-06-04 17.22.55.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/113504/979925c5-268d-ca93-3649-c4ce82aeee6a.png)

最初はくプロジェクトしかない状態。ここからファイルを以下のように作成する。


![スクリーンショット 2023-06-04 17.23.32.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/113504/a637fbe8-5a32-560d-e1f0-8298bad1aeed.png)

左下のプラスボタンから New Group で HelloPluginフォルダを作成

![スクリーンショット 2023-06-04 17.23.55.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/113504/f76313b2-a867-8c4b-e94b-1e7c6fa3faa5.png)


その下にHelloPlugin.swiftを作成

![スクリーンショット 2023-06-03 18.09.47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/113504/92076c3a-00af-8176-cc12-35e594a8dbec.png)

その際に上のようなポップアップがでた場合、特に必要なさそうなため Don't Create。

次に、HelloPlugin.swiftに処理を記述

```HelloPlugin.swift
import Foundation

@_cdecl("hello_plugin_helloworld")
public func hello_plugin_helloworld() -> Int32 {
    print("Hello World!")
    return 2
}

```

`Command + B` でビルド。


Product > Show Build Folder in Finder

![スクリーンショット 2023-06-05 9.12.10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/113504/5ffd9184-3b8a-3573-e29e-b80b1da86244.png)


![スクリーンショット_2023-06-05_9_12_51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/113504/75084862-b3cd-1032-4aee-ad32c0eb1589.png)

ここに拡張子 .bundle のファイルが作られている。

このファイルをUnity側に持っていく


参考

https://qiita.com/fuziki/items/955c2b35514bcfc37969

### Unity `.bundle`を読み込んで実行

プロジェクト名 `MyPlugin` (なんでもいい) で3Dの空のプロジェクト作成。

まず、プラグインを呼び出すための準備を行う。

|画面|project|
|:--:|:--:|
|![スクリーンショット 2023-06-05 16.54.16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/113504/d39f4d66-6bfa-e4e6-c218-eb005facf19c.png)|![スクリーンショット 2023-06-05 16.56.54.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/113504/3764a242-30cb-d545-5767-3e05bd655972.png)|

ボタンを押してログ出すだけのsceneをつくっておく。

```SampleScene.cs
public class SampleScene : MonoBehaviour
{
    public void OnClick()
    {
        Debug.Log("ここでプラグインを使う");
    }
}

```

これで準備OK
ボタンを押したら先ほど作った`.bundle`の hello_plugin_helloworld を呼び出すようにしていく。

![スクリーンショット_2023-06-05_17_03_12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/113504/2ca2bbd3-2e4f-2917-b9b6-e8828f6fda6a.png)


|project|inspector|
|:--:|:--:|
|![スクリーンショット 2023-06-05 17.04.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/113504/f71b7768-7f4a-5124-5a80-96c4a5044b1d.png)|![スクリーンショット_2023-06-05_17_04_30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/113504/df2d2a94-09eb-e857-81fe-d092b1d0de0a.png)|

Include Platforms の Editor にチェックがついていることを確認すること。ついてなかったら上と同様の設定にする。
projectに 追加すると HelloPlugin.bundle は上記のように設定されているはず。
この配置フォルダや、inspector については後でポイントを説明する。

![スクリーンショット 2023-06-05 17.41.30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/113504/7fa1318a-7317-c3fa-8b8c-a40d32ceb473.png)

続いて、NativeMethods.cs と　HelloPlugin.cs の2つを作成

```NativeMethods.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Runtime.InteropServices;

namespace Sample
{
#if UNITY_EDITOR_OSX
    internal static partial class NativeMethods
    {
        const string DLL_NAME = "HelloPlugin";

        [DllImport(DLL_NAME, CallingConvention = CallingConvention.Cdecl)]
        internal static extern int hello_plugin_helloworld();
    }
#else
    internal static partial class NativeMethods
    {
        internal static int hello_plugin_helloworld() => 0;
    }
#endif
}

```

```HelloPlugin.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

namespace Sample
{
    public class HelloPlugin
    {
        public static int HelloWorld()
        {
            return NativeMethods.hello_plugin_helloworld();
        }
    }
}

```

これで NativePlugin を使えるようになった。

```SampleScene.cs
using Sample;
...

    public void OnClick()
    {
        Debug.Log("HelloPlugin.HelloWorld() = " + HelloPlugin.HelloWorld());
    }
}
```

実行するとswift側で返した 2という数字が出力される。

![スクリーンショット 2023-06-05 18.41.15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/113504/2cf564d0-7e0f-8651-ec48-92b3233ab309.png)

swift側で書いた `print("Hello World!")`についてはUnityのコンソールには出力されない。



### `.bundle`作成のポイント

##### `@_cdecl`とは何か

> we've had an undocumented attribute @_cdecl that exposes a Swift function to C.

これ公式のドキュメントなく、いつ消えてもおかしくないattributeっぽい。
以下のフォーラムで議論されている。

https://forums.swift.org/t/formalizing-cdecl/40677/4


[上記のフォーラムの中にあったUnityで@_cdeclを使用する例](https://gist.github.com/troy-lamerton/6499cb8861596281596f2b3604a03359)


[swiftのリポジトリにも使われている箇所がある](https://github.com/apple/swift/blob/swift-DEVELOPMENT-SNAPSHOT-2023-06-03-a/test/PrintAsObjC/cdecl.swift)ため、もし使えなくなったら上記フォーラムか、該当コードが修正されるPRを覗けば良さそう。



突然使えなくなることを回避するために、公開部分に関してはあえて`@_cdecl`を使わず`ObjC++`で実装するのもありだと思う。

https://qiita.com/mao_/items/5b33c90e533a538570b8

実例として、[toio/ble-plugin-unity](https://github.com/toio/ble-plugin-unity/tree/v1.0.0/bleplugin_projects/macOS/bleplugin/bleplugin) では`ObjC++` で公開しているよう。(ObjC++に関してはあまり触ってないので詳しくはわからない)
`

##### Int ではなく Int32 を使う理由

csharp の int は符号付き 32 ビット整数。[ドキュメント](https://learn.microsoft.com/ja-jp/dotnet/csharp/language-reference/builtin-types/integral-numeric-types)

それに対して、swift の Int は platform によって 32bit か 64bitどちらかとして扱われる。　[ドキュメント](https://developer.apple.com/documentation/swift/int)

数値型を同じbit数として扱うために、swift 側では Int32 を使うことでbit数を固定する。

##### 関数名の命名規則

swiftの命名規則ではメソッド名は `lowerCamelCase`。 [ドキュメント](https://www.swift.org/documentation/api-design-guidelines/)

ただしここではあえてこれを無視している。

背景として、公開するメソッド名の頭にプラグイン名をつけたいからというのがある。

iOSビルドするときにはプラグインのメソッドを呼び出す際にプラグインの名前を指定しない(全部__Internal)。
=> メソッド名は全プラグインでユニークにしたい
=> プラグイン名を prefix にしよう。

`helloPluginHelloworld`でもいいのだが、lowerCamel は関数名長いと見づらいと思う。

cdecl で c言語として公開するのであれば swiftの命名規則ではなくc言語らしく宣言してもいいのでは？
=> スネークケースにしよう！

という判断。

### `.bundle`のインポートのポイント
##### 配置するフォルダについて

[ここ](https://docs.unity3d.com/2021.3/Documentation/Manual/PluginInspector.html)の`Plug-in default settings`に フォルダのパスのパターンが書かれている。

が、これは無視すべきだと考えている。詳しくは以下の記事で丁寧に説明されている。

https://qiita.com/su10/items/5e78ec8f6ef0bcafc759

`Assets/Plugins` に置くことは避け、`Assets/(プラグイン名)/Plugins/(Platform)/Pluginそのもの`のようにプラグイン専用のフォルダに置くのが良いと思う。
設定はInspectorでできる。

##### Plugin Inspector
![スクリーンショット_2023-06-05_21_56_56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/113504/029197db-c9fd-87f2-f2c0-96ad1ffd9005.png)

`.bundle`の inspector でみるべき点は2つある。

1つ目は Include Platforms

ここでチェックしたところにのみ含まれる。今回は Editor で使うため　Editor　へのチェックが必須。

2つ目は注意書きの部分

> Once a native plugin is loaded from script. it's never unloaded. if you deselect a native plugin and it's already loaded. please restart Unity.

これは同じファイル名だと2回ロードされないということを意味する。
つまり、swiftのコード修正して再度インポートするときに同じ名前でファイル更新すると変更が反映されないということ。

従って、開発中は HelloPlugin1.bundle のように適当に連番で rename してインポートすることによって回避する。
(手間でしかないため、将来的に改善されてほしい)

### Native Plugin 実行のポイント

https://github.com/teach310/MyPlugin/pull/1

##### NativeMethods クラス

https://learn.microsoft.com/ja-jp/dotnet/fundamentals/code-analysis/quality-rules/ca1060

DLLImport を使用するクラス名は XX にしようっていうお作法があるらしい。
これに従わなくてもとくに問題はないけれど、Plugin との境界はわかりやすい方がコード探しやすいと思う。
したがって DLLImpor を使うクラスは NativeMethods にしている。

##### CallingConventionについて

```.cs
[DllImport(DLL_NAME, CallingConvention = CallingConvention.Cdecl)]
```

https://learn.microsoft.com/ja-jp/dotnet/api/system.runtime.interopservices.dllimportattribute?view=net-7.0

DllImportにはいくつかのフィールドがあり、そのうちの一つが Calling Convention。

https://learn.microsoft.com/ja-jp/dotnet/api/system.runtime.interopservices.callingconvention?view=net-7.0

他はわからないが、この呼び出し規則に関しては常に明示的に示すのが良さそう。(なくてもバグらないけどUnityのPluginの記事の体感半数以上は指定している)
今回は swift 側で`@_cdecl`を使用しているため、Unity側でもcdeclを指定。


## テスト用のアプリ と macOS用のプラグインを作る

NativePlugin を開発するときに試行が Xcode だけで完結したほうが作業捗る。
そのため、テスト用のアプリ と macOS用のプラグインを作る。
ここでは上で作った HelloPlugin とは別に Xcode のプロジェクトを作っていく。
ここから本番。

### Swift Package の作成

Xcode の上部メニューから File > New > Workspace
名前は `SamplePlugin` にする。

続けてXcode の上部メニューから File > New > Package

![スクリーンショット 2023-06-06 16.45.57.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/113504/94af2ec1-cec1-e782-b43a-6063e16a0904.png)

一度 Xcode を閉じて　xcworkspace を Package のフォルダの中へ移動

![スクリーンショット_2023-06-06_16_33_53.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/113504/f061b726-2f45-2985-a7b4-8e8313df90a3.png)

xcworkspace を開くとSamplePluginの文字が赤くなっている。

![スクリーンショット 2023-06-06 16.49.07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/113504/e5c853f9-ce2e-9f88-e642-bd50025a1056.png)

右側のフォルダマークから、フォルダの場所を再設定
![スクリーンショット_2023-06-06_16_49_18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/113504/80ac987d-31ae-86bb-1c71-1464c9860f11.png)

これで workspace の中にPackageが入った状態になる。

![スクリーンショット 2023-06-06 16.51.39.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/113504/e7dd095f-69c8-4e81-102c-2970e0001d1e.png)

(もっとスマートなプロジェクト作成方法はあると思う)

HelloPlugin.swift と同様にとりあえず helloworld するコードを書く。
名前と数字を少しだけ変えている。

![スクリーンショット 2023-06-06 16.59.13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/113504/097bcd02-ee84-6f0c-52d1-b22c377aa44c.png)

```SamplePlugin.swift
import Foundation

@_cdecl("sample_plugin_helloworld")
public func sample_plugin_helloworld() -> Int32 {
    print("Hello World!")
    return 3
}

```


### テスト用のアプリを作成

File > New > Project
名前は SamplePluginApp とした。

![スクリーンショット 2023-06-06 17.07.24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/113504/292b91ca-32ff-e413-fc8f-b22ed8d22f6b.png)

|option|場所|
|:--:|:--:|
|![スクリーンショット 2023-06-06 17.08.36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/113504/383b202f-6d1d-0c9b-d8ae-4efbe2c857e4.png)|![スクリーンショット_2023-06-06_17_11_17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/113504/dea75803-df4d-68d2-4f05-5d1934562a22.png)|

この状態だと SwiftPackage の中に SamplePluginApp がある。

![スクリーンショット_2023-06-06_17_18_24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/113504/fd6717ed-5fbe-f39e-8828-48b71dcc041b.png)

これを消すために、空の Package.swift を作る。追加はvscodeでやった。

```Package.swift
// swift-tools-version: 5.8
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
  name: "empty",
  products: [],
  targets: []
)

```

![スクリーンショット 2023-06-06 17.21.07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/113504/a4f7c43e-997f-21ff-4c6e-9a842c77c87f.png)

workspace を開き直すと消えてる。

![スクリーンショット 2023-06-06 17.21.48.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/113504/f796fe12-67e5-7b9b-0c17-e48fa3dece9f.png)

これでアプリ作成準備OK。
テスト用アプリからSamplePluginを呼び出す。

SamplePluginAppのTARGET > General > Frameworks の +ボタンで SamplePluginを追加

![スクリーンショット 2023-06-06 17.28.57.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/113504/cc7cd1dd-bd1d-c287-fc83-cfafc4a5ab86.png)

ContentView を記述。ほぼ GitHub Copilot に生成してもらった。

```ContentView.swift
import SwiftUI
import SamplePlugin

struct ContentView: View {
    @State var number = 0
    
    var body: some View {
        VStack {
            Text("\(number)")
                .font(.largeTitle)
                .fontWeight(.bold)
                .foregroundColor(.accentColor)


            Button(action: {
                number = Int(sample_plugin_helloworld())
            }) {
                Text("Tap me!")
                    .font(.title)
                    .fontWeight(.bold)
                    .foregroundColor(.white)
                    .padding()
                    .background(Color.accentColor)
                    .cornerRadius(10)
            }
        }
        .padding()
    }
}
```

Canvas で ボタンをタップしたら SwiftPlugin から渡ってきた3という数字が表示されることを確認。

![スクリーンショット 2023-06-06 17.30.46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/113504/f21e69bf-c718-4d67-b97f-c5d8a9f08d55.png)

また、print した結果が console に表示されることを確認。

これで Unity を開かずに動作テストができた。

### `.bundle`用の Project 作成

File > New > Project
macOS > Bundle
名前は SamplePluginBundle とした。

option や場所は SamplePluginApp と同様
![スクリーンショット 2023-06-06 18.25.39.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/113504/e1097b72-14f0-de3b-9dde-e25226d61228.png)

SamplePlugin の追加もする。

![スクリーンショット 2023-06-06 18.38.13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/113504/4c504825-3cf0-cea4-0782-da24c80db1cf.png)

左下の + から New Group で SamplePluginBundleのフォルダを作成
![スクリーンショット 2023-06-06 18.33.34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/113504/a90fae2a-26a4-f47b-efa8-5a2a4b9cf617.png)

右クリック > New File から pchファイルを作成

![スクリーンショット 2023-06-06 18.34.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/113504/3965b665-ccc5-5b21-3330-ee3bf6456522.png)

`@import SamplePlugin` を追加

```PrefixHeader.pch
#ifndef PrefixHeader_pch
#define PrefixHeader_pch

// Include any system framework and library headers here that should be included in all compilation units.
// You will also need to set the Prefix Header build setting of one or more of your targets to reference this file.

@import SamplePlugin

#endif /* PrefixHeader_pch */
```

続いて `.bundle`を作成するが、Unityへのコピーまで一気にやるために今回は Makefile を使う。

```Makefile
BUILD_DIR := Build
BASE_NAME := SamplePlugin
WORKSPACE := $(BASE_NAME).xcworkspace
BUNDLE_NAME := $(BASE_NAME)Bundle

.PHONY: clean
clean:
	rm -rf $(BUILD_DIR)
	rm -rf .build

.PHONY: bundle/build
bundle/build:
	xcodebuild -workspace $(WORKSPACE) -scheme $(BUNDLE_NAME) -destination 'platform=macOS,arch=arm64' -configuration Release -derivedDataPath ./$(BUILD_DIR)/Bundle build
```

```.gitignore.diff
...
.netrc
+/Build
```

これで以下のように .bundle を生成できる

```
$ make bundle/build
xcodebuild -workspace SamplePlugin.xcworkspace -scheme SamplePluginBundle -destination 'platform=macOS,arch=arm64' -configuration Release -derivedDataPath ./Build/Bundle build
2023-06-07 15:07:01.944 xcodebuild[40621:7874424] DVTCoreDeviceEnabledState: 

...

Resolve Package Graph
** BUILD SUCCEEDED **
```

![スクリーンショット_2023-06-07_11_54_55.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/113504/18ba9c66-456e-f9ba-6b7d-65f34a34d5d1.png)

Unityのフォルダにコピーまでするコマンドを用意しておくと楽。

```:Makefile
include .env

...

.PHONY: bundle/cp
bundle/cp:
	cp -r $(BUILD_DIR)/Bundle/Build/Products/Release/$(BUNDLE_NAME).bundle $(UNITY_PLUGIN_MACOS_DIR)/$(BUNDLE_NAME).bundle

.PHONY: bundle
bundle: clean bundle/build bundle/cp

```

私の場合は以下のパス。(後で使うiosのパスもいれちゃう)

```.env
UNITY_PLUGIN_MACOS_DIR=~/UnityProjects/SampleProjects/MyPlugin/Assets/SamplePlugin/Plugins/macOS
UNITY_PLUGIN_IOS_DIR=~/UnityProjects/SampleProjects/MyPlugin/Assets/SamplePlugin/Plugins/iOS
```

https://github.com/teach310/SamplePlugin/commit/ada028ca004a1c0804d52f0332383757ec95eb5e

参考

https://qiita.com/fuziki/items/974f70b663ebfadfb136

### Unity側で .bundle を使う

プロジェクトは HelloPlugin の時と同じ MyPlugin という名前の UnityProject を使用。
呼び方は HelloPlugin のときとほぼ同様なためPRだけはる。

https://github.com/teach310/MyPlugin/pull/2

名前空間同じにしたら DLL_NAME という名前が使えなかったためSAMPLE_PLUGINという名前で定義した。
(DLL_NAMEよりこっちのほうがいいかもしれない)

```.cs
    internal static partial class NativeMethods
    {
        const string SAMPLE_PLUGIN = "SamplePluginBundle";

        [DllImport(SAMPLE_PLUGIN, CallingConvention = CallingConvention.Cdecl)]
        internal static extern int sample_plugin_helloworld();
```

![スクリーンショット 2023-06-07 15.27.40.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/113504/f45541c8-5e7a-55c9-3e81-74a042113352.png)

### Swift Package の作成のポイント

##### Swift Package を使う理由

使わない場合

```
NativePluginのコード1 -> macOS用プラグイン
NativePluginのコード2 -> テスト用ダミーアプリ
```

使う場合

```
SwiftPackage -> macOS用プラグイン
            -> テスト用ダミーアプリ
```

共通化できるのが良い。

##### 構造について

https://zenn.dev/yimajo/scraps/591dbf1ea6a434

TOP が SwiftPackage でその中に App とか入れるのは割と一般的らしい
全然こだわりはないため動けばなんでもいいとは思う。

### .bundle 用の Project 作成のポイント

.bundle の作り方の一次情報は見つからなかったため参考に記載したURLのやり方そのまま真似しています。
ので、[もとURL](https://qiita.com/fuziki/items/974f70b663ebfadfb136)見ていただければ！
(fujiki さんの記事は大変参考になっています。ありがとうございます）

### テスト用のアプリのポイント

##### そもそもこの作業は必要なのか

必要ではない。
が、テスト用アプリを作った方が動作テストが楽。

##### `@_cdecl`をつけたメソッドを swift から呼んでいるがそれは良いのか

本記事では呼んでしまっているが、**あまり良くないと思う**。

理由は swift から c を呼ぶ時のトラブルにぶつかるから。

例えば以下のような。(どういう時に出るのかとかは重要ではないため省略。これを修正するのは大変そうだな感が伝わればOK)

```
A C function pointer cannot be formed from a closure that captures context
```

```
SwiftPackageの関数(c) <- Appから呼ぶ(Swift)
```

ではなく、以下のほうが楽そう。

```
SwiftPackageの関数・クラス(Swift) <- Appから呼ぶ(Swift)
```

AppでテストしたいのはUnityと連携するインタフェースの部分ではなくロジックの部分のため、そのロジックの部分をpublicにして直接とってきてしまえばいいと思う。

なお、Swiftのデフォルトのアクセス修飾子は`internal`のため、コンストラクタを含めて明示的に`public`にする必要がある。

https://docs.swift.org/swift-book/documentation/the-swift-programming-language/accesscontrol/#Default-Access-Levels


### Makefileのポイント
##### bundle/build

```
$ xcodebuild -h
```

で使い方がある程度出る。

```
$ xcodebuild -h
...

xcodebuild -workspace <workspacename> -scheme <schemeName> [-destination <destinationspecifier>]... [-configuration <configurationname>] [-arch <architecture>]... [-sdk [<sdkname>|<sdkpath>]] [-showBuildSettings] [-showdestinations] [<buildsetting>=<value>]... [<buildaction>]...

...

Options:
...

    -workspace NAME                                          build the workspace NAME
    -scheme NAME                                             build the scheme NAME
    -configuration NAME                                      use the build configuration NAME for building each target
...
    -destination DESTINATIONSPECIFIER                        use the destination described by DESTINATIONSPECIFIER (a comma-separated set of key=value pairs describing the destination to use)
...
    -derivedDataPath PATH                                    specifies the directory where build products and other derived data will go
```

<buildaction> は cleanとかbuildとか。
clean は Makefile のコマンドにしてるため build のみ指定した。


指定の場所に、`.bundle`を生成できればいい。他のやり方もあると思う。(.bundle以外の余計なファイル作られすぎてる)

destinationsは以下のコマンドでそれっぽいのが出てきて、上のhelpでカンマ区切りで記述と書いてあったためそれに従った。

```
$ xcodebuild -showdestinations -scheme SamplePluginBundle
...

        Available destinations for the "SamplePluginBundle" scheme:
                { platform:macOS, arch:arm64, id:00008112-001A10243E45401E }
                { platform:macOS, name:Any Mac }
```

## iOS で HelloWorld!

### framework の出力

framework は SwiftPackage を元に以下のツールから出力する。

https://github.com/unsignedapps/swift-create-xcframework

私は brew で mint を入れ、mint を使って swift-create-xcframework をインストールした。

Mintfileを作成

```
unsignedapps/swift-create-xcframework@v2.3.0
```

```
$mint bootstrap
```

これでインストールできた。


```:Makefile
.PHONY: framework/build
framework/build:
	mint run swift-create-xcframework --output ./$(BUILD_DIR)/Framework --platform ios --configuration release
```

Makefileに上記を追記し、実行するとエラーがでる。

```
error: package 'sampleplugin' is using Swift tools version 5.8.0 but the installed version is 5.7.0
Error: fatalError
make: *** [framework/build] Error 1
```

```
$ swift --version
swift-driver version: 1.75.2 Apple Swift version 5.8 (swiftlang-5.8.0.124.2 clang-1403.0.22.11.100)
Target: arm64-apple-macosx13.0
```

swift 5.8 install されているのになぜこのエラーがでるのかは謎。

応急処置的に以下で対応。

```
$ swift package tools-version --set 5.7.0
```

これでビルドできるようになる。

![スクリーンショット_2023-06-08_21_24_00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/113504/cd6bb65b-9ae4-12cc-280e-a0f302c393d4.png)


コピー用のコマンドも bundle と同様に追加しておく

https://github.com/teach310/SamplePlugin/commit/a775cd2441a75df3a236ed465b9726edfdfec166

参考

https://qiita.com/uhooi/items/6a41a623b13f6ef4ddf0

https://zenn.dev/katopan/articles/31d9aac63da8b2

### Unity で iOSビルド

iOS フォルダを作成し、その中に先ほど作成した framework を移動

![スクリーンショット 2023-06-08 21.34.49.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/113504/718775e2-5b13-91b5-0ad8-231f9a8002b1.png)

framework の設定を以下のようにする
![スクリーンショット 2023-06-08 21.36.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/113504/901cb213-7b08-9726-9192-a55a4aaa941b.png)
![スクリーンショット 2023-06-08 21.36.50.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/113504/7710f98f-a985-a00e-2c44-4b4a7ad489c8.png)

また、framework を読み込めるようにコードを修正

```NativeMethods.cs
namespace Sample
{
#if UNITY_EDITOR_OSX || UNITY_IOS
    internal static partial class NativeMethods
    {
#if UNITY_IOS
        const string SAMPLE_PLUGIN = "__Internal";
#else
        const string SAMPLE_PLUGIN = "SamplePluginBundle";
#endif
```

Player Settingsを Signing Team ID

![スクリーンショット_2023-06-10_14_58_07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/113504/7e0dd97d-fa13-ba41-3ad6-4dc3df8697f2.png)

Team ID の調べ方については本記事では省略。

次に ビルドのポストプロセスを記述する。

![スクリーンショット 2023-06-10 15.03.38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/113504/1de040bc-b0b8-a173-f302-97f7c16821f3.png)

```BuildPostprocessor.cs
using System;
using UnityEditor;
using UnityEditor.Build;
using UnityEditor.Build.Reporting;
using UnityEditor.iOS.Xcode;
using UnityEngine;

namespace PluginSampleEditor
{
    public class BuildPostprocessor : IPostprocessBuildWithReport
    {
        public int callbackOrder { get { return 1; } }

        public void OnPostprocessBuild(BuildReport report)
        {
            var buildTarget = report.summary.platform;
            if (buildTarget == BuildTarget.iOS)
            {
                ProcessForiOS(report);
            }

            Debug.Log("BuildPostprocessor.OnPostprocessBuild for target " + report.summary.platform + " at path " + report.summary.outputPath);
        }

        void ProcessForiOS(BuildReport report)
        {
            var buildOutputPath = report.summary.outputPath;
            UpdatePBXProject(buildOutputPath, project =>
            {
                DisableBitcode(project);
            });
        }

        void UpdatePBXProject(string buildOutputPath, Action<PBXProject> action)
        {
            var pbxProjectPath = PBXProject.GetPBXProjectPath(buildOutputPath);
            var project = new PBXProject();
            project.ReadFromFile(pbxProjectPath);
            action(project);
            project.WriteToFile(pbxProjectPath);
        }

        // ENABLE_BITCODE は非推奨のパラメータのためNOにする
        void DisableBitcode(PBXProject project)
        {
            var mainTargetGuid = project.GetUnityMainTargetGuid();
            project.SetBuildProperty(mainTargetGuid, "ENABLE_BITCODE", "NO");

            var unityFrameworkTargetGuid = project.GetUnityFrameworkTargetGuid();
            project.SetBuildProperty(unityFrameworkTargetGuid, "ENABLE_BITCODE", "NO");
        }
    }
}

```

これやらないとエラーが出てしまう。

iPhone接続して、ビルド実行

![スクリーンショット_2023-06-10_15_11_03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/113504/19e92bf2-a50b-bb9b-753a-b646a89d07de.png)

実機アプリのボタンを押すと、Plugin の print と Unity の Debug.Log　が両方 Xcode　のコンソールに出力されることが確認できる。

![スクリーンショット 2023-06-10 15.10.43.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/113504/dac485cc-a666-c267-4b89-b63e0efdddd6.png)

これで Hello World 完了！

本章でやったことの PR

https://github.com/teach310/MyPlugin/pull/3



### framework 作成のポイント

##### swift-create-xcframework の使い方

```
$ mint run swift-create-xcframework --help
```

で確認できる

##### generate-xcodeproj を使わない理由

swift 5.8 でコマンドが削除されているから。

https://github.com/apple/swift-package-manager/issues/6640#issuecomment-1582414768

### PluginInspector による frameworkの設定のポイント

##### Add to Embedded Binaries について

https://docs.unity3d.com/2021.3/Documentation/Manual/PluginInspector.html

設定はここに書いてある。

> When you select Add to Embedded Binaries option, Unity sets the Xcode project options to copy the plug-in file into the final application package. Do this for:
• Dynamically loaded libraries.

Dynamically loaded libraries のときに使って　とのことなのでチェックしてる。

### framework 読み込みのポイント

##### Platform 依存コンパイルについて

https://docs.unity3d.com/ja/2018.4/Manual/PlatformDependentCompilation.html

ここに書いてある。 UNITY_IPHONE は非推奨のため使わないこと。

##### __Internal について

https://docs.unity3d.com/2021.3/Documentation/Manual/NativePlugins.html

ここの Example に書いてある。

> On iOS plugins are statically linked into
the executable, so we have to use __Internal as the library name.
[DllImport ("__Internal")]

### Unity ビルド
##### Bitcode enabled を NO にする理由

> .../SamplePlugin/Plugins/iOS/SamplePlugin.framework/SamplePlugin' does not contain bitcode. You must rebuild it with bitcode enabled (Xcode setting ENABLE_BITCODE), obtain an updated library from the vendor, or disable bitcode for this target. file ...

やらないと Xcode でエラーが出る。

https://qiita.com/temoki/items/92999fcede0a7b89760f

将来的には Unity ビルド時に bitcode が enable に勝手にならなくなると思う。

##### IPostprocessBuildWithReport について

https://docs.unity3d.com/2021.3/Documentation/ScriptReference/Build.IPreprocessBuildWithReport.html

ENABLE_BITCODE を NO にするために多くの記事では [PostProcessBuildAttribute](https://docs.unity3d.com/ja/2021.3/ScriptReference/Callbacks.PostProcessBuildAttribute.html) が使われているが、本記事では使っていない。

とくに大きな理由はなく、IPostprocessBuildWithReportのほうが新しそうだからこちらを使用した。

## おわりに

Unity の Native Plugin は Hello World するだけでとても大変！
本記事が Native Plugin を使いたいと思っている方の助けになれば幸いです。