Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationEventAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
61
Help us understand the problem. What is going on with this article?
@mao_

【Unity】iOSネイティブプラグイン開発を完全に理解する

最近、iOSネイティブアプリ開発に触れる機会が多く、それと合わせてUnity向けのiOSネイティブプラグインを実装する機会もあったので、これを機に備忘録として知見を纏めてみようと思います。

正直ここで解説する情報自体に目新しいものは無く、基本的には調べれば出てくる情報が多いかもしれません。
ただ、iOSネイティブアプリ開発及びUnity向けのプラグイン実装に於いて全くの初学者である自分が「調べている上で色々と疑問に思ったポイントや気付いた点」は少なからずとも出てきたので、そこらを補足しつつ体系的に解説していければと思います。

NOTE: 「完全に理解した」の意味について
タイトルに「完全に理解する」とありますが、ここで言う完全理解は「実装するためのチュートリアルを完了出来た」までの意味を指します。
参考: https://twitter.com/ito_yusaku/status/1042604780718157824?s=20

バージョン

  • Unity 2019.4.20f
  • Xcode 12.3

この記事で解説する内容について

本記事では以下の目次順をベースに所々に補足を交えつつ解説していきます。

  • 最小構成から見るネイティブプラグイン基礎
    • 例として「"Hello World"とログ出力しつつ、戻り値として整数型を返すプラグイン」を実装
      • Objective-C++の実装例
      • Swiftの実装例
  • 幾つかの実装例
    • ネイティブコード側でインスタンス化したオブジェクトの管理
    • ネイティブコードからC#のメソッドを呼び出す

他にも本記事の大きめな補足として以下の付録記事も用意してます。
こちらについては、ある程度本文を読み進めていく上で得られる予備知識を前提に書いているので、先ずは本文に沿って読んでいくのをオススメします。

記事のターゲット

基本的には初学者をターゲットとしており、主に以下の方を想定してます。

  • iOSビルドまでは出来るが、ネイティブプラグイン開発については全然分からん
  • 何回か作ったことあるけど、コピペに近い雰囲気レベルの理解なのでちゃんと把握しておきたい

この記事の目的

上記に該当する方がiOSネイティブプラグインを実装する上で必要となる知識(iOSネイティブ周りの文脈/実装の流れ/etc..)を広く把握し、自分で実装したり深く調べていく際の足がかりとするところまでを目的としてます。

解説しない内容

記事を読み進める上では以下の前知識はある程度前提としてます。
ここらについては記事中では解説しないのでご了承下さい。

  • Unityの基本的な操作
    • e.g. Scriptの追加や実行方法、ビルド方法など
  • C# 基礎
    • ※但しP/Invokeと言ったプラグイン開発に関わってくるであろう箇所は記事中でも触れていく
  • iOSビルド/実機動作までの手順

追記するかも..?

今は解説してませんが...元気があったら追記するかもです...
今は他の記事を参照して頂ければと思います... :bow:

  • Objective-Cの構文について
  • Swiftの構文について
  • マーシャリング周り

記事中で解説する言語について

記事中ではiOSのネイティブプラグインとして実装される「ネイティブ側のコード全般」を「ネイティブコード」と言う呼び名で記載していきます。

解説するネイティブコードは主に以下のものとなります。1
合わせて拡張子記事中での略称の対応表を載せておきます。

言語 略称 拡張子
Objective-C ObjC .m
Objective-C++ ObjC++ .mm
Swift Swift .swift

【補足】 ObjCObjC++の違いについて

初学者の方の中には「Objectie-Cは聞いたことあるけど、Objective-C++は聞いたことないかも」という方も居るかもしれないので簡単な解説を以下の記事に纏めてみました。
気になる方はこちらを御覧ください。

【余談】 拡張子の'm'の意味について (クリックで展開)

ふと「ObjCの拡張子の.mって何の略?」「言語名とmって一文字もかぶってなくない?どこから出てきたの?」と思って調べてみたところ...「メソッドの略」「メッセージの略」と様々な説が出てきた。
かなり曖昧な感じだったので、気にしないほうが良さそう。。

最小構成から見るネイティブプラグイン基礎

先ずは基礎として最小構成のネイティブプラグインをベースに概要を解説していきます。

実装するプラグインは「"Hello World"とログ出力しつつ、戻り値として整数を返す」だけのシンプルな物であり、ネイティブコード側はObjC++で実装していきます。

ここでは基礎となる以下のポイントを理解するのが目的です。

  • 一通りの実装の流れ
    • ネイティブコードをC#から呼び出すための関数の外部宣言(extern "C")
  • C#からP/Invokeで呼び出す際のポイント

Swiftで実装しないのか?

いきなりObjC++と聞いて、恐らくはこの疑問を抱く方がいるかもしれません。
現にUnityでiOSネイティブプラグインを実装する際にも「Swiftを用いて実装すること自体は可能」です。

しかし、Swiftで実装するにあたっては以下の要件が必要であり、ObjC++と比べると少し手間と言うか...複雑になっているため、先ずは構成がシンプルに済むObjC++の解説から入っていきます。

  • C#からSwiftのコードを直接呼び出すことは出来ず、間にObjC++が挟まる
  • ビルド結果に対して、ある一定の設定を適用するEditor拡張を実装する必要がある

Swift編についてはObjC++編で必要な予備知識を解説した後に入っていきます。

この章のサンプルプロジェクト

サンプルプロジェクトは以下のリポジトリのminimum-example-objc++ブランチにあります。

プロジェクト構成
[Assets]
    L [MinimumExample]    // この章で解説するサンプル一式
        L [Plugins]
            L [iOS]
                L Example.mm
        L [Scenes]
            L MinimumExample.unity
        L [Scripts]
            L Exsample.cs

MinimumExampleシーンにはuGUIで実装されたボタンが一つ配置されており、それを押下することでネイティブプラグインを呼び出す構成になってます。

各ソースの役割としては以下のようになります。(関連性と言った詳細は後述)

  • Scripts/Exsample.cs
    • シーン中のGameObjectにアタッチされており、ボタン押下時にネイティブプラグインの処理を実行
  • Plugins/iOS/Exsample.mm
    • ObjC++で実装されたネイティブコード2

実装の流れ

プラグインの実装の流れとして、以下の順に解説していきます。

▼ ネイティブコードの実装

  • ①. ObjC++で必要な処理を実装 (クラスの定義/実装など)
    • → 今回で言うと「"Hello World"とログ出力しつつ、戻り値として整数を返す」処理を実装
  • ②. ①の処理をC#から呼び出せるようにする

▼ C#からの呼び出し

  • ③. C#からP/Invokeで②の処理を呼び出す

ネイティブコードの実装

該当するソースはExample.mmです。
ソース全体はこちら。

Example.mm (クリックで展開)
Example.mm
#import <Foundation/Foundation.h>

// MARK:- interface (クラスの宣言部)

@interface Example : NSObject

/// ログに"Hello World"と出力して2を返す。
/// NOTE: ここではクラスメソッド(静的関数)として実装
+ (int)printHelloWorld;

@end


// MARK:- implementation (クラスの実装部)

@implementation Example

+ (int)printHelloWorld {
    // ログ出力
    NSLog(@"Hello World");

    // 戻り値を返す
    return 2;
}

@end


// MARK:- extern "C" (Cリンケージで宣言)

#ifdef __cplusplus
extern "C" {
#endif

// NOTE: この関数が実際にUnity(C#)から呼び出される
int printHelloWorld() {

    // 上記で宣言・実装した`Example.printHelloWorld`を呼び出す。呼び出し時の構文はObjC形式となる。
    // NOTE: クラスメソッド(静的関数)として実装しているので、クラスをインスタンス化せずに直接呼び出せる
    return [Example printHelloWorld];
}

#ifdef __cplusplus
}
#endif

■ ①. ObjC++で必要な処理を実装 (クラスの定義/実装など)

先ずはネイティブコード側でプラグインの目的である「"Hello World"とログ出力しつつ、戻り値として整数を返す」処理を実装していきます。

ソース中で言うと以下の箇所が該当する部分であり、要点を纏めると以下のようになります。

  • @interface Example : NSObject ~ @endで囲まれた部分がクラスの宣言部
    • コメントにある通り、printHelloWorldクラスメソッド(静的関数)として宣言してます
  • @implementation Example ~ @endで囲まれた部分がクラスの実装部
    • printHelloWorldの実装箇所では「Hello World」とログ出力しつつ、戻り値として2を返してます
Example.mm
#import <Foundation/Foundation.h>

// MARK:- interface (クラスの宣言部)

@interface Example : NSObject

/// ログに"Hello World"と出力して2を返す。
/// NOTE: ここではクラスメソッド(静的関数)として実装
+ (int)printHelloWorld;

@end


// MARK:- implementation (クラスの実装部)

@implementation Example

+ (int)printHelloWorld {
    // ログ出力
    NSLog(@"Hello World");

    // 戻り値を返す
    return 2;
}

@end

一先ずはプラグインの肝となる機能は用意できました。
あとはprintHelloWorldメソッドをC#から呼び出すことが出来れば目的達成」と言ったところですが...これだけだとC#から呼び出すことは出来ません。

では「どうやったらC#から呼び出せるようになるのか?」について次で解説していきます。

【補足】 NSObjectとは?

しれっとNSObjectと言うクラスを継承してますが、このNSObjectについては以下の章で簡単に補足してます。
気になる方は御覧ください。

■ ②. ①の処理をC#から呼び出せるようにする

「ではどうやったらC#からExampleクラスのメソッドを呼び出せるのか?」と言う話についてですが、前提としてC#からネイティブコードを呼び出す際にはextern "C"で外部宣言されている関数である必要があります。

ソース中から該当箇所を切り出すと以下の部分であり、extern "C"で囲われているinit printHelloWorld()と言う関数が実際にC#からP/Invokeで呼び出される関数になります。
この外部宣言している関数内ではObjC++で実装したクラスを呼び出すことが可能となるために、この関数を経由してExampleクラスのメソッドを呼び出します。

Example.mm
// MARK:- extern "C" (Cリンケージで宣言)

#ifdef __cplusplus
extern "C" {
#endif

// NOTE: この関数が実際にUnity(C#)から呼び出される
int printHelloWorld() {

    // `Example.printHelloWorld`を呼び出す。呼び出し時の構文はObjC形式となる。
    // NOTE: クラスメソッド(静的関数)として実装しているので、クラスをインスタンス化せずに直接呼び出せる
    return [Example printHelloWorld];
}

#ifdef __cplusplus
}
#endif

ちなみに「ObjC++のクラス実装」と「外部宣言関数」は同一ソース内にて実装可能となるので、今回の例ではExample.mmの1ソースだけで済んでます。

【補足】 extern "C"で外部宣言を行う必要がある理由

extern "C"で外部宣言を行う必要がある理由については以下の記事で解説してます。

C#からの呼び出し

該当するソースはExample.csです。
ソース全体はこちら。

Example.cs (クリックで展開)
Example.cs
using System.Runtime.InteropServices;
using UnityEngine;
using UnityEngine.UI;

namespace MinimumExample
{
    /// <summary>
    /// 最小構成のサンプル呼び出し
    /// </summary>
    sealed class Example : MonoBehaviour
    {
        [SerializeField] Button _buttonHelloWorld = default;

        void Start()
        {
            _buttonHelloWorld.onClick.AddListener(() =>
            {
#if !UNITY_EDITOR && UNITY_IOS
                // ネイティブプラグインの呼び出し
                var ret = PrintHelloWorld();
                Debug.Log($"戻り値: {ret}");
#else
                // それ以外のプラットフォームからの呼び出し (Editor含む)
                Debug.Log("Hello World (iOS以外からの呼び出し)");
#endif
            });
        }

        #region P/Invoke

        // ObjectiveC++コードで実装した`Example`クラスのP/Invoke

        /// <summary>
        /// `printHelloWorld`の呼び出し
        /// </summary>
        /// <remarks>
        /// NOTE: Example.mmの「extern "C"」内で宣言した関数をここで呼び出す
        /// - iOSのネイティブプラグインは静的に実行ファイルにリンクされるので、`DllImport`にはライブラリ名として「__Internal」を指定する必要がある
        /// - `EntryPoint`に.mm側で宣言されている名前を渡すことでC#側のメソッド名は別名を指定可能
        /// </remarks>
        [DllImport("__Internal", EntryPoint = "printHelloWorld")]
        static extern int PrintHelloWorld();


        // NOTE: ちなみに`EntryPoint`を指定しない場合は、以下のようにC#側も.mm側と同名に合わせる必要がある
        // [DllImport("__Internal")]
        // static extern int printHelloWorld();

        #endregion P/Invoke
    }
}

■ ③. C#からP/Invokeで②の処理を呼び出す

こちらはP/Invokeを用いて②で外部宣言した関数を呼び出すだけです。

以下のコードで言うstatic extern int PrintHelloWorld()を呼び出したらExample.mmにあるint printHelloWorld()が呼び出されます。

Example.cs
        // ObjectiveC++コードで実装した`Example`クラスのP/Invoke

        /// <summary>
        /// `printHelloWorld`の呼び出し
        /// </summary>
        /// <remarks>
        /// NOTE: Example.mmの「extern "C"」内で宣言した関数をここで呼び出す
        /// - iOSのネイティブプラグインは静的に実行ファイルにリンクされるので、`DllImport`にはライブラリ名として「__Internal」を指定する必要がある
        /// - `EntryPoint`に.mm側で宣言されている名前を渡すことでC#側のメソッド名は別名を指定可能
        /// </remarks>
        [DllImport("__Internal", EntryPoint = "printHelloWorld")]
        static extern int PrintHelloWorld();

あとは上記で定義したメソッドをuGUIのボタン押下時に呼び出すようにすれば完了です。

Example.cs
            _buttonHelloWorld.onClick.AddListener(() =>
            {
#if !UNITY_EDITOR && UNITY_IOS
                // ネイティブプラグインの呼び出し
                var ret = PrintHelloWorld();
                Debug.Log($"戻り値: {ret}");
#else
                // それ以外のプラットフォームからの呼び出し (Editor含む)
                Debug.Log("Hello World (iOS以外からの呼び出し)");
#endif
            });

ここまでのまとめ

以上が一通りの実装の流れになります。
ここまでの流れの振り返りとして、C#からネイティブプラグインを呼び出す流れを図に示すと、以下のようなイメージになります。

relation_chart_4.png

あとは補足として以下の章を記述しているので、宜しければこちらも御覧ください。

実は省略できる部分がある

解説したサンプルですが、実は省略できる部分があります。
今回の要件程度であれば、以下の様に①の手順を抜かして外部宣言関数だけで完結させることが可能です。

Example.mm
// MARK:- extern "C" (Cリンケージで宣言)

#ifdef __cplusplus
extern "C" {
#endif

// NOTE: この関数が実際にUnity(C#)から呼び出される
int printHelloWorld() {

    // NOTE: [Example printHelloWorld]を実装して呼び出さずに、直接相応の処理を呼び出す

    // ログ出力
    NSLog(@"Hello World");
    // 戻り値を返す
    return 2;
}

#ifdef __cplusplus
}
#endif

簡単な呼び出し程度であれば外部宣言関数内で完結可能

今回は一通りの流れに触れておくために敢えて手順①を含めて解説しましたが、実はこちらの手順は慣習的に行う必要はありません。
簡単な処理の呼び出し程度であれば外部宣言関数内だけで完結させることが可能です。

では具体的に「どういったプラグインを作る際に外部宣言関数だけで完結させることが可能か?」と言うと、個人的には以下の要件を満たす場合には外部宣言関数だけでも完結できるかなと思ってます。

  • 静的変数/静的関数として実装されているiOSネイティブAPI/ライブラリの呼び出し
  • その他、状態を持たずに呼び出せそうなネイティブAPI/ライブラリの呼び出し

逆に「メンバ変数などに状態を持つ必要があり、素直にクラスで実装したほうが良さそう」ならObjC++(若しくは次で解説するSwift)でクラスを実装した上で呼び出すようにしたほうが良いかもです。3

幾つかの実装例

上記の流れを踏まえて、外部宣言関数だけで完結している幾つかの実装例を用意しました。
宜しければ参考にしてみてください。

Swiftでのサンプル実装

ここまではネイティブコードをObjC++のみで完結させる場合の解説でした。

とは言え、今現在に於いては「もしネイティブコード側でクラスを実装するなら4保守観点からSwiftで実装したい場合もあるかと思います。
ここでは最小構成サンプルの要件自体はそのままに、クラスの実装をSwiftに置き換えて解説していきます。

Swiftを組み込むにあたって、以下のポイントを理解するのが目的です。

  • Swiftで実装できる箇所の把握
  • Swiftを実装するにあたって必要な設定

この章のサンプルプロジェクト

サンプルプロジェクトは以下のリポジトリのminimum-example-swiftブランチにあります。

プロジェクト構成
[Assets]
    L [MinimumExample-Swift]    // この章で解説するサンプル一式
        L [Plugins]
            L [iOS]
                L Example.mm
                L Example.swift
        L [Scenes]
            L MinimumExample_Swift.unity
        L [Scripts]
            L [Editor]
                L XcodePostProcess.cs
            L Exsample.cs

名前などが変わってますが、シーンの構成自体は前の章と同じです。
新規で追加されているXcodePostProcess.csと言うEditor拡張については後述します。

実装の流れ

大まかに言えば、前の章の流れで言うところの①の部分がSwiftの実装に置き換わるイメージになります。
あとは②の外部宣言に関しても、Swiftの呼び出しに合わせて多少変更が入ってきます。

呼び出しの流れを図に示すと、以下のようなイメージになります。

relation_chart_2.png

C#からの呼び出しについては変更点が無いので、それを踏まえて以下の順に解説していきます。
(③は新規で増える手順となります。詳細は後述)

▼ ネイティブコードの実装

  • ①. Swiftで必要な処理を実装 (クラスの実装など)
    • → 今回で言うと「"Hello World"とログ出力しつつ、戻り値として整数を返す」処理を実装
  • ②. ①の処理をC#から呼び出せるようにする

▼ Editor拡張の実装

  • ③. [PostProcessBuild]でSwiftのバージョン指定を自動化

■ ①. Swiftで必要な処理を実装 (クラスの実装など)

Swiftで「"Hello World"とログ出力しつつ、戻り値として整数を返す」処理を実装していきます。
ほぼほぼ前の章のObjC++での実装をSwiftに置き換えるイメージになります。

詳細は次で解説しますが、こちらのSwiftで実装したクラスは最終的にはObjC++のソースファイル(.mm)から呼び出す必要があるために、実装する上では以下のポイントを守る必要があります。

Example.swift
import Foundation

// NOTE: ObjC/ObjC++に公開する物はアクセスレベルを`public` or `open`に設定する必要あり

// ObjC/ObjC++に公開するクラスには`NSObject`を継承させる
public class Example: NSObject {

    // ObjC/ObjC++に公開するメソッドには`@objc`を付ける

    /// ログに"Hello World"と出力して2を返す。
    /// NOTE: ここではクラスメソッド(静的関数)として実装
    ///
    /// - Returns: 2固定
    @objc public static func printHelloWorld() -> Int {
        // ログ出力
        print("Hello World")

        // 戻り値を返す
        return 2
    }
}

■ ②. ①の処理をC#から呼び出せるようにする

こちらも前回同様にSwiftで実装したクラスだけだとC#から呼び出すことが出来ないので、printHelloWorldを呼び出すにはObjC++のソースファイル(.mm)にて外部宣言した関数を経由して呼び出す必要があります。
※ちなみに外部宣言はC言語/C++の機能となるために、Swiftのソースコード上から外部宣言するのは不可能と言う認識

こちらもほぼ前の章で解説した外部宣言と変わりありませんが、Unity2019.3以降は#import <UnityFramework/UnityFramework-Swift.h>を追加する必要があります。

Example.mm
#import <Foundation/Foundation.h>

// 2019.3からはこちらをimportする必要がある
#import <UnityFramework/UnityFramework-Swift.h>

// MARK:- extern "C" (Cリンケージで宣言)

#ifdef __cplusplus
extern "C" {
#endif

// NOTE: この関数が実際にUnity(C#)から呼び出される
int printHelloWorld() {

    // `Example.swift`で実装した`Example.printHelloWorld`を呼び出す。呼び出し時の構文はObjC形式となる。
    // NOTE: クラスメソッド(静的関数)として実装しているので、クラスをインスタンス化せずに直接呼び出せる
    return [Example printHelloWorld];
}

#ifdef __cplusplus
}
#endif

■ ③. [PostProcessBuild]でSwiftのバージョン指定を自動化

UnityがiOSビルドで出力するXcodeプロジェクトファイル(.xcodeproj)は内部的に設定されているSwiftのバージョンが古い時があり、利用するXcodeのバージョンが新しかったりするとUnspecified扱いになることがあります。

なので、[PostProcessBuild]から利用するSwfitバージョンを明示的に指定するEditor拡張を用意しておくと便利です。5
→ この例では5.0を指定

XcodePostProcess.cs
        /// <summary>
        /// Swiftを実装するにあたって必要な設定を自動で適用する
        /// </summary>
        [PostProcessBuild]
        static void OnPostProcessBuild(BuildTarget target, string path)
        {
            if (target != BuildTarget.iOS) return;

            var projectPath = PBXProject.GetPBXProjectPath(path);
            var project = new PBXProject();
            project.ReadFromString(File.ReadAllText(projectPath));

            // 2019.3からは`UnityFramework`に分離しているので、targetGuidはこちらを指定する必要がある。
            // NOTE: 前バージョンと共存させたい場合には「#if UNITY_2019_3_OR_NEWER」で分けることも可能
            var targetGuid = project.GetUnityFrameworkTargetGuid();

            // Swift version: 5.0
            // NOTE: 明示的に指定しないと3.0ぐらいの古いのが設定されるっぽいので、Xcodeによっては`Unspecified`扱いになる
            project.SetBuildProperty(targetGuid, "SWIFT_VERSION", "5.0");

            File.WriteAllText(projectPath, project.WriteToString());
        }
【補足】 Xcodeの設定の自動化について

この様にXcodeプロジェクトファイルの設定はUnityのPostProcessBuildを使うことで自動化することが可能です。
更に詳しい話については以下の別記事にて纏めているので、宜しければこちらを御覧ください。

ここまでのまとめ

以上が最小構成を題材にした実装例となります。
とりあえずは以下の要点を掴んでおけばOKです。

▼ ネイティブコード

  • 外部宣言されている関数が実際にC#から呼び出される
    • 簡単なiOSネイティブAPI/ライブラリの呼び出し程度であれば、外部宣言した関数内だけで完結可能
    • クラスを実装する場合にはObjC/ObjC++ or Swiftで実装した上で、最終的にはこちらから呼び出しを行う
      • 「どちらで実装するのが良いのか?」についてはこちらで補足

▼ C#

  • ネイティブコード側で外部宣言されている関数をP/Invokeで呼び出すだけ

【補足】 ネイティブプラグインを実装する上での設計について

今回解説した最小構成のサンプルは運用/保守を見越した実装にはなってません。
(C#からの呼び出し時の処理とかプリプロセス命令で愚直に分岐して直に呼び出したりしている)

ここらの詳細については、以下の記事で別途補足します。

【補足】 ネイティブコードってどうやって実装していけばよいのか?

ここまでの流れで取り扱っていない範囲として、そもそもとして「ネイティブコードってどう実装していけば(効率含めて)良いのか?」というのがあります。

例えば一例として「Assets配下に.mmを直接置いてコーディング → Unityでビルドしてネイティブプラグインの動作確認」と言う流れでも出来なくはないですが...動作確認の効率としては少し悪いです。

自分のやり方にはなりますが、以下の記事で別途補足してます。

幾つかの実装例

上述の最小構成の例では「ネイティブコード側のクラスメソッド(静的関数)を呼び出す」 or 「外部宣言関数だけで完結」と言った比較的シンプルな例でしたが、実際にネイティブプラグインを利用する上では「ネイティブコード側で実装しているクラス(以降、ネイティブクラスと表記)をインスタンス化したい」「更にインスタンス化したオブジェクトのインスタンスメソッドを呼び出したい」と言った要件が出てくるかと思います。

この章では以下の要件について解説していきます。

  • ネイティブコード側でインスタンス化したオブジェクトの管理
  • ネイティブコードからC#のメソッドを呼び出す

サンプルコードはObjC++で解説します。

ネイティブコード側でインスタンス化したオブジェクトの管理

表題の件について、C#からの呼び出しを踏まえると「そもそもインスタンス化したオブジェクトはどうやって管理するのか?」「C#側でインスタンス化したオブジェクトを管理できるのか?」と言った疑問が出てくるかと思います。(実際に自分は出てきた)

この章では以下のポイントを理解するのが目的です。

  • ネイティブコード側でインスタンス化したオブジェクトをC#上で管理する方法
    • インスタンスの生成と解放について
  • C#上で管理しているネイティブコードのオブジェクトからインスタンスメソッドを呼び出す方法

この章のサンプルプロジェクト

サンプルプロジェクトは以下のリポジトリのinstanceMethod-example-objc++ブランチにあります。

基本的な構成は最小構成サンプルとさほど変わりはありませんが、新たに以下の変更を加えてます。

  • シーン中に数値入力用のInputFieldを配置
  • Hellow Worldボタンを押したら"Hello World : [(InputFieldで入力した数値)]"と言う文字列を出力しつつ、入力値を戻り値として返す

実装のポイント

ソース中から必要な箇所を掻い摘んで解説していきます。
全ソースコードについては以下を参照。

Example.mm (クリックで展開)
Example.mm
#import <Foundation/Foundation.h>

// MARK:- interface (クラスの宣言部)

@interface Example : NSObject

// メンバ変数に値を設定
- (void)setMember:(int)value;

// ログに"Hello World"と出力してメンバ変数(_member)に設定された値を返す
- (int)printHelloWorldWithMember;

@end


// MARK:- implementation (クラスの実装部)

@implementation Example {

    // メンバ変数
    int _member;
}

// イニシャライザ(インスタンスの初期化)
- (instancetype)init {
    self = [super init];
    if (self) {
        _member = 0;
    }

    return self;
}

- (void)setMember:(int)value {
    _member = value;
}

- (int)printHelloWorldWithMember {
    // ログ出力
    NSLog(@"Hello World : [%d]", _member);

    // 戻り値を返す
    return _member;
}

@end


// MARK:- extern "C" (Cリンケージで宣言)
// NOTE: ここで外部宣言している関数が実際にUnity(C#)から呼び出される

#ifdef __cplusplus
extern "C" {
#endif

// インスタンス化
// NOTE: 戻り値のポインタをC#側でIntPtrなどで保持し、インスタンスメソッドの呼び出し時に渡して使う
Example* createExample() {
    Example* instance = [[Example alloc] init];
    CFRetain((CFTypeRef) instance);
    return instance;
}

// 解放
void releaseExample(Example* instance) {
    CFRelease((CFTypeRef) instance);
}

// 以下はインスタンスメソッドの呼び出し
// NOTE: 第一引数にはインスタンス化した際に保持しているポインタを渡す

void setMember(Example* instance, int value) {
    [instance setMember:value];
}

int printHelloWorldWithMember(Example* instance) {
    return [instance printHelloWorldWithMember];
}

#ifdef __cplusplus
}
#endif

Example.cs (クリックで展開)
Example.cs
using System;
using System.Runtime.InteropServices;
using UnityEngine;
using UnityEngine.UI;

namespace InstanceMethodExample
{
    /// <summary>
    /// インスタンス化及びインスタンスメソッドの呼び出しサンプル
    /// </summary>
    sealed class Example : MonoBehaviour
    {
        [SerializeField] Button _buttonHelloWorld = default;
        [SerializeField] InputField _inputField = default;

        IntPtr _instance = IntPtr.Zero;

        void Start()
        {
            _buttonHelloWorld.onClick.AddListener(() =>
            {
#if !UNITY_EDITOR && UNITY_IOS
                // ネイティブプラグインの呼び出し
                var ret = PrintHelloWorldWithMember(_instance);
                Debug.Log($"戻り値: {ret}");
#else
                // それ以外のプラットフォームからの呼び出し (Editor含む)
                Debug.Log("Hello World (iOS以外からの呼び出し)");
#endif
            });

            _inputField.onEndEdit.AddListener(text =>
            {
                if (int.TryParse(text, out var num))
                {
#if !UNITY_EDITOR && UNITY_IOS
                    // ネイティブプラグインの呼び出し
                    SetMember(_instance, num);
#else
                    Debug.Log($"{num} (iOS以外からの呼び出し)");
#endif
                }
            });

#if !UNITY_EDITOR && UNITY_IOS
            _instance = CreateExample();
#endif
        }

        void OnDestroy()
        {
            if (_instance != IntPtr.Zero)
            {
                ReleaseExample(_instance);
            }
        }

        #region P/Invoke

        // ObjectiveC++コードで実装した`Example`クラスのP/Invoke

        // ネイティブコード側にあるExampleクラスのインスタンス化
        // NOTE: 戻り値はインスタンスのポインタ
        [DllImport("__Internal", EntryPoint = "createExample")]
        static extern IntPtr CreateExample();

        // インスタンスの解放
        [DllImport("__Internal", EntryPoint = "releaseExample")]
        static extern void ReleaseExample(IntPtr instance);

        // `setMember`の呼び出し
        [DllImport("__Internal", EntryPoint = "setMember")]
        static extern int SetMember(IntPtr instance, int value);

        // `printHelloWorldWithMember`の呼び出し
        [DllImport("__Internal", EntryPoint = "printHelloWorldWithMember")]
        static extern int PrintHelloWorldWithMember(IntPtr instance);

        #endregion P/Invoke
    }
}

■ ネイティブコード側でインスタンス化しつつ、インスタンス自体はC#側で持つ

ネイティブクラスのインスタンス化はネイティブコード側で担当しつつも、そのインスタンス自体のポインタは戻り値としてC#側に返すようにします。

C#側ではこの戻り値のポインタをIntPtr型として受け取ってインスタンスの管理を行います。

※ちなみにサラッと出ていているCFRetainと、この後出てくるCFReleaseについてはこちらの章で補足してます。

Example.mm
extern "C" {
#endif

// インスタンス化
// NOTE: 戻り値のポインタをC#側でIntPtrなどで保持し、インスタンスメソッドの呼び出し時に渡して使う
Example* createExample() {
    Example* instance = [[Example alloc] init];
    CFRetain((CFTypeRef) instance);
    return instance;
}

#ifdef __cplusplus
}
#endif
Example.cs
        // ネイティブコード側で作られたインスタンスのポインタを保持
        IntPtr _instance = IntPtr.Zero;

        void Start()
        {
#if !UNITY_EDITOR && UNITY_IOS
            _instance = CreateExample();
#endif
        }

        #region P/Invoke

        // ObjectiveC++コードで実装した`Example`クラスのP/Invoke

        // ネイティブコード側にあるExampleクラスのインスタンス化
        // NOTE: 戻り値はインスタンスのポインタ
        [DllImport("__Internal", EntryPoint = "createExample")]
        static extern IntPtr CreateExample();

        #endregion P/Invoke

■ 解放時には保持しているポインタを渡してネイティブコード側で解放

インスタンス化がそうであるように、解放処理自体もネイティブコード側で行います。
具体的には以下のようにC#から解放処理を呼び出す際にポインタを渡すことで、実際の解放処理はネイティブコード側で行うようにします。

今回の例だと簡易的なサンプルなのでC#側はOnDestroy()で解放処理を行ってますが、実際に利用するにあたってはIDisposable辺りを実装して解放処理を呼び出す形にするのが良いかもしれません。

Example.mm
extern "C" {
#endif

// 解放
void releaseExample(Example* instance) {
    CFRelease((CFTypeRef) instance);
}

#ifdef __cplusplus
}
#endif
Example.cs
        // ネイティブコード側で作られたインスタンスのポインタを保持
        IntPtr _instance = IntPtr.Zero;

        void OnDestroy()
        {
            if (_instance != IntPtr.Zero)
            {
                ReleaseExample(_instance);
            }
        }

        #region P/Invoke

        // ObjectiveC++コードで実装した`Example`クラスのP/Invoke

        // インスタンスの解放
        [DllImport("__Internal", EntryPoint = "releaseExample")]
        static extern void ReleaseExample(IntPtr instance);

        #endregion P/Invoke

■ インスタンスメソッドの呼び出しも同様に、C#側で保持しているポインタを渡してネイティブコード側で呼び出す

インスタンスメソッドの呼び出しについても、C#からはインスタンスのポインタを渡すだけであって、実際の処理はネイティブコード側で呼ぶようにします。
今回の例では「インスタンスメソッドを呼び出すための外部宣言関数」を用意しつつ、引数から受け取ったインスタンスのポインタを利用して間接的にインスタンスメソッドを呼び出します。

※ちなみにサンプル中でObjC++で実装されているExampleクラスについては以下を参照。

Exampleクラスについて (クリックで展開)

MinimumExampleにあったprintHelloWorldと言うクラスメソッドを廃止しつつ、インスタンス化していることを多少分かりやすくするためにメンバ変数を利用するようなメソッドを追加してます。

  • (void)setMember:(int)value;
    • 引数として受け取ったvalueをメンバ変数である_memberに格納
  • (int)printHelloWorldWithMember;
    • ログに"Hello World : [(InputFieldで入力した数値)]"と出力しつつ、_memberを戻り値として返す
Example.mm
#import <Foundation/Foundation.h>

// MARK:- interface (クラスの宣言部)

@interface Example : NSObject

// メンバ変数に値を設定
- (void)setMember:(int)value;

// ログに"Hello World"と出力してメンバ変数(_member)に設定された値を返す
- (int)printHelloWorldWithMember;

@end

// MARK:- implementation (クラスの実装部)

@implementation Example {

    // メンバ変数
    int _member;
}

// イニシャライザ(インスタンスの初期化)
- (instancetype)init {
    self = [super init];
    if (self) {
        _member = 0;
    }

    return self;
}

- (void)setMember:(int)value {
    _member = value;
}

- (int)printHelloWorldWithMember {
    // ログ出力
    NSLog(@"Hello World : [%d]", _member);

    // 戻り値を返す
    return _member;
}

@end

Example.mm
#ifdef __cplusplus
extern "C" {
#endif

// 以下はインスタンスメソッドの呼び出し
// NOTE: 第一引数にはインスタンス化した際に保持しているポインタを渡す

void setMember(Example* instance, int value) {
    [instance setMember:value];
}

int printHelloWorldWithMember(Example* instance) {
    return [instance printHelloWorldWithMember];
}

#ifdef __cplusplus
}
#endif
Example.cs
        // ネイティブコード側で作られたインスタンスのポインタを保持
        IntPtr _instance = IntPtr.Zero;

        void Start()
        {
            _buttonHelloWorld.onClick.AddListener(() =>
            {
#if !UNITY_EDITOR && UNITY_IOS
                // ネイティブプラグインの呼び出し
                var ret = PrintHelloWorldWithMember(_instance);
                Debug.Log($"戻り値: {ret}");
#else
                // それ以外のプラットフォームからの呼び出し (Editor含む)
                Debug.Log("Hello World (iOS以外からの呼び出し)");
#endif
            });

            _inputField.onEndEdit.AddListener(text =>
            {
                if (int.TryParse(text, out var num))
                {
#if !UNITY_EDITOR && UNITY_IOS
                    // ネイティブプラグインの呼び出し
                    SetMember(_instance, num);
#else
                    Debug.Log($"{num} (iOS以外からの呼び出し)");
#endif
                }
            });

            ........
        }

        #region P/Invoke

        // `setMember`の呼び出し
        [DllImport("__Internal", EntryPoint = "setMember")]
        static extern int SetMember(IntPtr instance, int value);

        // `printHelloWorldWithMember`の呼び出し
        [DllImport("__Internal", EntryPoint = "printHelloWorldWithMember")]
        static extern int PrintHelloWorldWithMember(IntPtr instance);

        #endregion P/Invoke
    }
}

ネイティブコードからC#のメソッドを呼び出す

例えばネイティブコードからコールバックとしてC#のメソッドを呼び出したいとき、ネイティブプラグインとしては以下の2つのやり方があります。
(公式ドキュメントで言うとこちらのページに有る「Calling C# back from native code」で解説されている内容)

  • UnitySendMessage経由で呼び出し
  • ネイティブコードにdelegateを渡して呼び出してもらう

この章では後者の「ネイティブコードにdelegateを渡して呼び出してもらう」やり方を中心に解説していきます。
UnitySendMessageは補足として後述します。

主に以下のポイントを理解するのが目的です。

  • ネイティブコードに渡すデリゲート(C#)対応する関数ポインタ(ObjC++)の関連性
    • iOSビルド(IL2CPP時)に於ける制約
  • ネイティブコード側で渡されたコールバックを保持する方法 + 呼び出し方について

この章のサンプルプロジェクト

サンプルプロジェクトは以下のリポジトリのcallback-example-objc++ブランチにあります。

こちらも基本的な構成は最小構成サンプルとさほど変わりはありませんが、シーン中には登録したコールバックを呼び出すためのCallボタンを配置してます。

ちなみに今回のサンプルではただコールバックを渡して呼び出すだけではなく、渡したコールバックをネイティブクラスのメンバ変数として保持し、インスタンスメソッド経由で呼び出すような実装にしてます。

実装のポイント

ソース中から必要な箇所を掻い摘んで解説していきます。
全ソースコードについては以下を参照。

Example.mm (クリックで展開)
Example.mm
#import <Foundation/Foundation.h>

// MARK:- 関数ポインタ

// NOTE: C#側で定義している以下のデリゲート型に対応する関数ポインタ
// > delegate void SampleCallbackDelegate(int num);
typedef void (* sampleCallbackDelegate)(int);


// MARK:- interface (クラスの宣言部)

@interface Example : NSObject

// コールバックの登録
// NOTE: メンバ変数`_sampleCallbackDelegate`にC#から渡されるコールバックを登録
//       (C#風に言い換えるとデリゲート型のフィールドにメソッドを渡すイメージ)
- (void)registerSampleCallback:(sampleCallbackDelegate)delegate;

// `_sampleCallbackDelegate`に登録してあるコールバックを呼び出す
- (void)callSampleCallback;

@end


// MARK:- implementation (クラスの実装部)

@implementation Example {

    // メンバ変数
    // NOTE: `registerSampleCallback`から渡されるC#のコールバックを持つ
    sampleCallbackDelegate _sampleCallbackDelegate;
}

// イニシャライザ(インスタンスの初期化)
- (instancetype)init {
    self = [super init];
    if (self) {
        _sampleCallbackDelegate = NULL;
    }

    return self;
}

- (void)registerSampleCallback:(sampleCallbackDelegate)delegate {
    _sampleCallbackDelegate = delegate;
}

- (void)callSampleCallback {
    // コールバックが登録されているなら呼び出し
    if (_sampleCallbackDelegate != NULL) {
        _sampleCallbackDelegate(2);
    }
}

@end


// MARK:- extern "C" (Cリンケージで宣言)
// NOTE: ここで外部宣言している関数が実際にUnity(C#)から呼び出される

#ifdef __cplusplus
extern "C" {
#endif

// C#から渡されるコールバックをインスタンスに登録
void registerSampleCallback(Example* instance, sampleCallbackDelegate delegate) {

    // NOTE: こういう感じに直接呼び出すこともできる
    //delegate(1);

    // インスタンスに登録
    [instance registerSampleCallback:delegate];
}

// インスタンスに登録したコールバックを呼び出し
void callSampleCallback(Example* instance) {
    [instance callSampleCallback];
}

// インスタンスの生成・解放

// インスタンス化
// NOTE: 戻り値のポインタをC#側でIntPtrなどで保持し、インスタンスメソッドの呼び出し時に渡して使う
Example* createExample() {
    Example* instance = [[Example alloc] init];
    CFRetain((CFTypeRef) instance);
    return instance;
}

// 解放
void releaseExample(Example* instance) {
    CFRelease((CFTypeRef) instance);
}

#ifdef __cplusplus
}
#endif

Example.cs (クリックで展開)
Example.cs
using System;
using System.Runtime.InteropServices;
using UnityEngine;
using UnityEngine.UI;

namespace CallbackExample
{
    /// <summary>
    /// コールバックの登録・呼び出しサンプル
    /// </summary>
    sealed class Example : MonoBehaviour
    {
        [SerializeField] Button _buttonCall = default;

        IntPtr _instance = IntPtr.Zero;

        void Start()
        {
            _buttonCall.onClick.AddListener(() =>
            {
#if !UNITY_EDITOR && UNITY_IOS
                // ネイティブプラグインの呼び出し
                CallSampleCallback(_instance);
#else
                // それ以外のプラットフォームからの呼び出し (Editor含む)
                Debug.Log("iOS以外からの呼び出し");
#endif
            });


#if !UNITY_EDITOR && UNITY_IOS
            _instance = CreateExample();
            // インスタンスにコールバックを登録しておく
            RegisterSampleCallback(_instance, SampleCallback);
#endif
        }

        void OnDestroy()
        {
            if (_instance != IntPtr.Zero)
            {
                ReleaseExample(_instance);
            }
        }


        #region P/Invoke Callback

        // 登録するメソッド(ここで言う`SampleCallback`)と同じフォーマットのデリゲート
        // NOTE: ネイティブコード側で定義している以下の関数ポインタに対応する
        // > typedef void (* sampleCallbackDelegate)(int);
        delegate void SampleCallbackDelegate(int num);


        // 実際にネイティブコードから呼び出されるメソッド
        // NOTE: iOS(正確に言うとAOT)の場合には「staticメソッドな上で`MonoPInvokeCallbackAttribute`を付ける必要がある」
        [AOT.MonoPInvokeCallbackAttribute(typeof(SampleCallbackDelegate))]
        static void SampleCallback(int num)
        {
            Debug.Log($"ネイティブコードから呼び出された : {num}");
        }

        #endregion P/Invoke Callback


        #region P/Invoke

        // ObjectiveC++コードで実装した`Example`クラスのP/Invoke

        // `registerSampleCallback`の呼び出し
        [DllImport("__Internal", EntryPoint = "registerSampleCallback")]
        static extern int RegisterSampleCallback(IntPtr instance, SampleCallbackDelegate callback);

        // `callSampleCallback`の呼び出し
        [DllImport("__Internal", EntryPoint = "callSampleCallback")]
        static extern int CallSampleCallback(IntPtr instance);


        // ネイティブコード側にあるExampleクラスのインスタンス化
        // NOTE: 戻り値はインスタンスのポインタ
        [DllImport("__Internal", EntryPoint = "createExample")]
        static extern IntPtr CreateExample();

        // インスタンスの解放
        [DllImport("__Internal", EntryPoint = "releaseExample")]
        static extern void ReleaseExample(IntPtr instance);

        #endregion P/Invoke
    }
}

ネイティブコードに渡すデリゲート対応する関数ポインタの定義

先ずC#側ではネイティブコードに渡したいコールバックのデリゲート型を定義します。
→ 以下のコードで言うdelegate void SampleCallbackDelegate(int num);が該当

Example.cs
// 登録するメソッド(ここで言う`SampleCallback`)と同じフォーマットのデリゲート
// NOTE: ネイティブコード側で定義している以下の関数ポインタに対応する
// > typedef void (* sampleCallbackDelegate)(int);
delegate void SampleCallbackDelegate(int num);

それと合わせてネイティブコード側にも同じフォーマットの関数ポインタを定義します。
→ 以下のコードで言うtypedef void (* sampleCallbackDelegate)(int);が該当

コード上では便宜的に(キャメルケースこそ違えど)名前を同名にしてますが、フォーマットさえ合っていれば別名でも問題ないです。

Example.mm
// MARK:- 関数ポインタ

// NOTE: C#側で定義している以下のデリゲート型に対応する関数ポインタ
// > delegate void SampleCallbackDelegate(int num);
typedef void (* sampleCallbackDelegate)(int);

■ C#側で「実際にネイティブコードからコールバックとして呼び出されるメソッド」を定義

C#側では実際にネイティブコードからコールバックとして呼び出されるメソッドを定義します。
→ 以下のコードで言うSampleCallback(int num)が該当

ポイントとしては以下です。

  • iOS(と言うよりはIL2CPP)の場合には、staticメソッド且つAOT.MonoPInvokeCallbackAttribute属性を付ける必要がある
  • AOT.MonoPInvokeCallbackAttributeの引数にはネイティブコードに渡したいコールバックのデリゲート型のTypeを渡す
    • 今回で言うとSampleCallbackDelegateが該当
Example.cs
// 実際にネイティブコードから呼び出されるメソッド
// NOTE: iOS(正確に言うとAOT)の場合には「staticメソッドな上で`MonoPInvokeCallbackAttribute`を付ける必要がある」
[AOT.MonoPInvokeCallbackAttribute(typeof(SampleCallbackDelegate))]
static void SampleCallback(int num)
{
    Debug.Log($"ネイティブコードから呼び出された : {num}");
}
【補足】 JITだと上記の制約は無い

今回はiOSビルドを対象としているので、IL2CPPによるAOTコンパイルがほぼ強制となるために上記の制約が付きますが、例えばStandaloneと言ったMonoバックエンドが許される環境に於いては上記制約が無く、普通にコールバックにラムダ式を渡すことが出来たりします。

因みにこの制約はXamarinも同様な模様。

■ ネイティブコードにコールバックを渡す

C#側にてRegisterSampleCallbackメソッドからネイティブコード側にSampleCallback(int num)を登録します。
(この例ではStart()内で登録)

Example.cs
        IntPtr _instance = IntPtr.Zero;

        void Start()
        {

#if !UNITY_EDITOR && UNITY_IOS
            _instance = CreateExample();
            // インスタンスにコールバックを登録しておく
            RegisterSampleCallback(_instance, SampleCallback);
#endif
        }

        #region P/Invok

        // `registerSampleCallback`の呼び出し
        [DllImport("__Internal", EntryPoint = "registerSampleCallback")]
        static extern int RegisterSampleCallback(IntPtr instance, SampleCallbackDelegate callback);

        #endregion P/Invoke

        #region P/Invoke Callback

        // 登録するメソッド(ここで言う`SampleCallback`)と同じフォーマットのデリゲート
        // NOTE: ネイティブコード側で定義している以下の関数ポインタに対応する
        // > typedef void (* sampleCallbackDelegate)(int);
        delegate void SampleCallbackDelegate(int num);


        // 実際にネイティブコードから呼び出されるメソッド
        // NOTE: iOS(正確に言うとAOT)の場合には「staticメソッドな上で`MonoPInvokeCallbackAttribute`を付ける必要がある」
        [AOT.MonoPInvokeCallbackAttribute(typeof(SampleCallbackDelegate))]
        static void SampleCallback(int num)
        {
            Debug.Log($"ネイティブコードから呼び出された : {num}");
        }

        #endregion P/Invoke Callback

ネイティブコード側ではExampleクラスのメンバ変数に関数ポインタを持たせることで、C#から渡されるコールバックをメンバ変数として保持できるようにしておきます。

Example.mm
@implementation Example {

    // メンバ変数
    // NOTE: `registerSampleCallback`から渡されるC#のコールバックを持つ
    sampleCallbackDelegate _sampleCallbackDelegate;
}

// コールバックの登録
- (void)registerSampleCallback:(sampleCallbackDelegate)delegate {
    _sampleCallbackDelegate = delegate;
}

@end

あとは外部宣言関数であるregisterSampleCallback内にて、Exampleクラスが持つ登録関数にC#から渡ってくるコールバックを渡してやります。

NOTEとアノテーションが付いたコメントにもありますが、渡されたコールバックはこの時点で呼び出すことも可能です。

Example.mm
#ifdef __cplusplus
extern "C" {
#endif

// C#から渡されるコールバックをインスタンスに登録
void registerSampleCallback(Example* instance, sampleCallbackDelegate delegate) {

    // NOTE: こういう感じに直接呼び出すこともできる
    //delegate(1);

    // インスタンスに登録
    [instance registerSampleCallback:delegate];
}

#ifdef __cplusplus
}
#endif

■ ネイティブコードからコールバックを呼び出す

検証用に「ネイティブコード側で保持してあるコールバック」を呼び出せる仕組みを実装していきます。

ネイティブコード側のExampleクラスにはメンバ変数に保持しているコールバックを呼び出せる関数を実装しておき、合わせてP/Invoke用の外部宣言関数の方も用意しておきます。

Example.mm
@implementation Example {

    // メンバ変数
    // NOTE: `registerSampleCallback`から渡されるC#のコールバックを持つ
    sampleCallbackDelegate _sampleCallbackDelegate;
}

// `_sampleCallbackDelegate`に登録してあるコールバックを呼び出す
- (void)callSampleCallback {
    // コールバックが登録されているなら呼び出し
    if (_sampleCallbackDelegate != NULL) {
        _sampleCallbackDelegate(2);
    }
}

@end

#ifdef __cplusplus
extern "C" {
#endif

// インスタンスに登録したコールバックを呼び出し
void callSampleCallback(Example* instance) {
    [instance callSampleCallback];
}

#ifdef __cplusplus
}
#endif

あとはC#から外部宣言関数であるcallSampleCallbackP/Invokeで呼び出すことで、ネイティブコード側で保持されているSampleCallbackが呼び出されます。

今回の例だと即時的にコールバックが呼び出されますが、例えばこちらの方法を用いることでネイティブコード側で非同期な処理を書いてコールバックで値を返すと言ったことが出来るようになります。

Example.cs
        IntPtr _instance = IntPtr.Zero;

        void Start()
        {
            _buttonCall.onClick.AddListener(() =>
            {
#if !UNITY_EDITOR && UNITY_IOS
                // ネイティブプラグインの呼び出し
                CallSampleCallback(_instance);
#else
                // それ以外のプラットフォームからの呼び出し (Editor含む)
                Debug.Log("iOS以外からの呼び出し");
#endif
            });
        }

        #region P/Invok

        // `callSampleCallback`の呼び出し
        [DllImport("__Internal", EntryPoint = "callSampleCallback")]
        static extern int CallSampleCallback(IntPtr instance);

        #endregion P/Invoke

        #region P/Invoke Callback

        // 実際にネイティブコードから呼び出されるメソッド
        // NOTE: iOS(正確に言うとAOT)の場合には「staticメソッドな上で`MonoPInvokeCallbackAttribute`を付ける必要がある」
        [AOT.MonoPInvokeCallbackAttribute(typeof(SampleCallbackDelegate))]
        static void SampleCallback(int num)
        {
            Debug.Log($"ネイティブコードから呼び出された : {num}");
        }

        #endregion P/Invoke Callback

【補足】 UnitySendMessageについて

放ったらかしにするのもアレなので補足として軽く解説しておくと、前者で挙げているUnitySendMessageは手軽な反面、以下のような制約があります。

  • シーン中に決まった名前のGameObject + 決まった名前のメソッドが存在すること
  • 呼び出すメソッドの引数に文字列しか渡せない
  • 呼び出しに1フレの遅延が発生する

やりたいことが↑の制約内で収まる範疇内ならまだしも、これだけだと厳しいときも出てくるかもしれないので、今回はもう一つの手法であるネイティブコードにdelegateを渡す手法の方を主にして解説させてもらいました。

UnitySendMessageの呼び出し方については以下の記事にて実装例が載っているので、こちらを御覧ください。

その他 補足集

別記事に分けるほどでもない幾つかの補足を纏めます。

P/Invokeについて

P/Invokeを簡単に説明すると「ネイティブコードをC#のメソッドのように呼び出すための機構」です。

どの様な場面で使うのか?

こちらを利用することで、例えば「Unity C#だけでは呼び出すことが出来ないプラットフォーム固有の機能」をC#上から呼び出せるようになります。

例を挙げると以下の記事にある「Unity上からiOS端末の発熱状態を取得する」と言った機能を実装することが出来るようになります。

実装のポイント

C#からiOSのネイティブコード(ObjC++)を呼び出す上でのポイントを幾つか挙げます。

  • P/Invokeで呼び出すメソッドにはextern修飾子を付ける
  • その上でiOS向けのネイティブプラグインの場合には[DllImport("__Internal")]属性を付ける6
  • DllImport属性のEntryPointには「ネイティブコード側で外部宣言しているメソッド名」を入れる
    • →今回で言うとprintHelloWorld

※ 呼び出す対象のネイティブコード(ObjC++)はこちら (クリックで展開)
Example.mm
// MARK:- extern "C" (Cリンケージで宣言)

#ifdef __cplusplus
extern "C" {
#endif

// NOTE: この関数が実際にUnity(C#)から呼び出される
int printHelloWorld() {

    // 上記で宣言・実装した`Example.printHelloWorld`を呼び出す。呼び出し時の構文はObjC形式となる。
    // NOTE: クラスメソッド(静的関数)として実装しているので、クラスをインスタンス化せずに直接呼び出せる
    return [Example printHelloWorld];
}

#ifdef __cplusplus
}
#endif

Example.cs
        // ObjectiveC++コードで実装した`Example`クラスのP/Invoke

        /// <summary>
        /// `printHelloWorld`の呼び出し
        /// </summary>
        /// <remarks>
        /// NOTE: Example.mmの「extern "C"」内で宣言した関数をここで呼び出す
        /// - iOSのネイティブプラグインは静的に実行ファイルにリンクされるので、`DllImport`にはライブラリ名として「__Internal」を指定する必要がある
        /// - `EntryPoint`に.mm側で宣言されている名前を渡すことでC#側のメソッド名は別名を指定可能
        /// </remarks>
        [DllImport("__Internal", EntryPoint = "printHelloWorld")]
        static extern int PrintHelloWorld();

特に3つ目のEntryPointについてはあまり実装されている記事を見受けない(?)ものの、覚えておくとネイティブコード側とC#側で命名規則を分けることが出来るので便利です。

ちなみにEntryPointを指定しない場合には、以下のようにネイティブコード側で外部宣言している関数名と合わせることで呼び出すことも可能です。(全体的にこの書き方をよく見受ける印象)

Example.cs
        // NOTE: ちなみに`EntryPoint`を指定しない場合は、以下のようにC#側のメソッド名を.mmで外部宣言している関数名と同名に合わせる必要がある
        [DllImport("__Internal")]
        static extern int printHelloWorld();

ヘッダーファイルは必要?

今回のサンプルでは意図的にヘッダーファイル(.h)に分けていません。
理由としては実装したクラスが他のネイティブコードから参照されない前提があるからと言うのが挙げられます。

これはあくまで私個人のやり方にはなりますが、基本的に作成するネイティブプラグインが機能単位で独立している上で、他のソースからも参照されない場合には.mmの1ソースだけで完結させてしまうことが多いです。(この方が管理が楽)

逆に他のソースからも参照する必要が出てきたタイミングで「ヘッダーに分離しても良いかな?」と言う感じで実装してます。

上述の通り、私個人のやり方なのでチームやプロジェクトごとにルールが決まっている場合にはそれに準拠させることをオススメします。

サンプルコード

最小構成サンプルExample.mmを分けると以下のようになります。

Example.mm (クリックで展開)
Example.mm
#import "Example.h"

// MARK:- implementation (クラスの実装部)

@implementation Example

+ (int)printHelloWorld {
    // ログ出力
    NSLog(@"Hello World");

    // 戻り値を返す
    return 2;
}

@end

Example.h (クリックで展開)
Example.h
#import <Foundation/Foundation.h>

#ifndef Example_h
#define Example_h

// MARK:- interface (クラスの宣言部)

@interface Example : NSObject

/// ログに"Hello World"と出力して2を返す。
/// NOTE: ここではクラスメソッド(静的関数)として実装
+ (int)printHelloWorld;

@end


// MARK:- extern "C" (Cリンケージで宣言)

#ifdef __cplusplus
extern "C" {
#endif

// NOTE: この関数が実際にUnity(C#)から呼び出される
int printHelloWorld() {

    // 上記で宣言・実装した`Example.printHelloWorld`を呼び出す。呼び出し時の構文はObjC形式となる。
    // NOTE: クラスメソッド(静的関数)として実装しているので、クラスをインスタンス化せずに直接呼び出せる
    return [Example printHelloWorld];
}

#ifdef __cplusplus
}
#endif


#endif /* Example_h */

.m(ObjC)だけでは実装できないのか? → .mm(ObjC++)を使わない例

「今回のサンプルのソースは.mmで実装されているが、.mだけで実装できないのか?」と言う話についてですが、こちらは出来ます。
その際には拡張子を.mにした上で、外部宣言を以下のように改修する必要があります。

Example.m
.....

// これだけでC#から呼び出せる
extern int printHelloWorld() {
    return [Example printHelloWorld];
}

「何故こうする必要があるのか?」の理由については以下の章で後述してます。

.m.mmの使い分けについて

あまり詳しくないので持論ベースにはなりますが...私個人としては以下の理由から.mmにする事が多いです。

  • C++の資産(ライブラリとか)を利用することがある
  • そもそも.mで出来ることは.mmでも出来るという認識なので、特筆して前者にする理由がない

ここらの裁量については「これが正しい」とは言い切れないかと思われるので、とりあえずは仕様を理解した上で使い分けていくと良いかもしれません。
(寧ろ敢えて.mにする利点があったら教えて頂けると幸いです。。:bow:)

ネイティブコード側のインスタンスの生成/解放時の処理についての補足

インスタンスの生成/解放処理に注目すると、対象のインスタンスに対してCFRetain/CFReleaseと言う処理を呼び出してます。

ここでやっていることをザックリと説明すると、Objective-CのARC(Automatic Reference Counting):自動参照カウントを無効化して自前でRetain/Releaseを呼び出して参照カウントを管理するようにしてます。

元に生成したインスタンスのポインタはそのままC#側に渡されて管理される想定があるので、ARCと言った機能は無効化しておく必要があります。

Example.mm
// インスタンス化
// NOTE: 戻り値のポインタをC#側でIntPtrなどで保持し、インスタンスメソッドの呼び出し時に渡して使う
Example* createExample() {
    Example* instance = [[Example alloc] init];
    CFRetain((CFTypeRef) instance);
    return instance;
}

// 解放
void releaseExample(Example* instance) {
    CFRelease((CFTypeRef) instance);
}

NSObjectについて

ObjCに於いて殆どのクラスの基底となるクラスであり、「メモリ領域の確保(alloc)」や「インスタンスの初期化(init)」と言ったオブジェクトの基本となる機能が実装されてます。

ObjCで実装するクラスについては、基本的にはNSObjectを継承したサブクラスを実装することになります。

プレフィックスのNSの意味

iOS関連のコードを追っているとNSObject以外にもNSArrayNSDataとプレフィックスが「NS」から始まるクラスが多数出てきます。

そこで「この"NS"って何やねん?」と思って軽く調べてみたところ、この頭文字はNeXTSTEPと呼ばれるmacOSやiOSの前身となるOSの略称であり、そこから来ているみたいです。
簡単な話、歴史的な経緯で付いてしまった名称かと思われるので、そこまで深く考えなくても良さそうです。
詳細については以下の記事の「Objective-Cの歴史」に詳しく纏まってます。

ObjC/ObjC++Swiftのどちらで実装すればよいか?

この内容はほぼほぼ持論と言いますか、「あくまで自分ならこうする」と言った判断基準でしかないので、それを踏まえた上での一例として捉えていただけると幸いです。

私は主に以下の基準で選定することが多いです。

ObjC/ObjC++を選定するとき

  • 簡単なネイティブプラグインを実装するとき
    • 例えば外部宣言関数だけで完結できるような簡単なAPIの呼び出しとか
    • ソースファイルが1つで済むために管理しやすい
  • 汎用的なライブラリを実装するとき
    • ライブラリ側の都合で[PostProcessBuild]をフックしてPBXProjectを書き換えたくないモチベがある (Swiftバージョンの指定とか)
      • 導入先でPBXProjectの変更内容が競合する可能性が無きにしもあらず
    • この記事にある通り、Unity2019.3前後で大きく構成が変わるので、広いバージョンの保守を踏まえるとSwiftの準拠が面倒
      • ※逆に「2019.3以降のみ対応」と割り切ったり、数年経ってUnity2019.3よりも前のバージョンが完全に過去のものになった + 今の構成から大きく変わっていないなら忘れても良いかも
  • C++のライブラリを利用したり、ポインタ操作などを行うとき
    • ※一応Swiftにもポインタ型があったりとポインタに関する操作はできるが、個人的にはこのパターンだとC++で書いた方が楽

Swiftを選定するとき

繰り返しにはなりますが、「あくまで自分ならこうする」と言った判断基準のため、実際には各々の要件や技術スタックに合わせて選定することをおすすめします。

参考/関連リンク

補足記事

サンプル

謝辞

いつも配信で私を元気付けてくれるバーチャルタレントの巻乃もなかさん7に深く感謝を申し上げます。
配信に元気付けられてこの記事を書ききることが出来ました!


  1. 正確に言うとC言語/C++も含まれてくるかと思われるが、基本的には挙げている言語をベースに解説 

  2. ひょっとしたら「あれ?ヘッダーファイルは?」と思う方も居るかもいるかもしれませんが、無いのには理由があります。詳細は後述。 

  3. 一応グローバル変数に状態を持つと言う手もある。複雑化しない程度であればこれでも良いかもしれない。 

  4. 上述した通り、そもそも外部宣言関数の呼び出しだけで完結できるならSwiftは不要となる。 

  5. これはどちらかと言うとオプション的な設定であり、必須項目では無い。ちなみにこの設定自体は.xcodeprojを開いてから手動で設定を変更することも可能。 

  6. ここらの指定はプラットフォームによって変わるので、全プラットフォームこれで行けるワケでは無いことだけ一応補足 

  7. 私の最推しのバーチャルタレント(所謂VTuber)さんです! 

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
61
Help us understand the problem. What is going on with this article?