Help us understand the problem. What is going on with this article?

[Unity] C#とObjective-Cの連携まとめ

基本

Sample.mm
#ifdef __cplusplus
extern "C" {
#endif
    // C#から呼ばれる関数
    void sampleMethod1() {
        NSLog(@"sampleMethod1 called.");
    }
#ifdef __cplusplus
}
#endif
Sample.cs
using System.Runtime.InteropServices;

public class Sample {
    // Sample.mm で定義しているCの関数を、以下のようにC#側で定義する
    [DllImport("__Internal")]
    private static extern void sampleMethod1();
}

Objective-C側ではCの関数を定義したソースを作る。
Objective-Cのクラスを使いたい場合は、Cの関数を通して呼び出す形になる。
Objective-Cのソースファイルは、Assets/Plugins/iOS 配下に置く。

C#側では定義したCノ関数と同名の extern static メソッドを定義する。
このstaticメソッドを呼ぶと、定義したCの関数が呼ばれる。

引数

プリミティブ型 (int, float, bool)、文字列 (string)

Sample.mm
#ifdef __cplusplus
extern "C" {
#endif
    void sampleMethod2(int val1, float val2, bool val3, const char *val4) {
        NSLog(@"sampleMethod2 called. val1=%d, val2=%f, val3=%d, val4=%s", val1, val2, val3, val4);
    }
#ifdef __cplusplus
}
#endif
Sample.cs
using System.Runtime.InteropServices;

public class Sample {
    [DllImport("__Internal")]
    private static extern void sampleMethod2(int val1, float val2, bool val3, string val4);
}

プリミティブはそのまま渡すだけ。
stringは const char * で受け取る。

配列

Sample.mm
#ifdef __cplusplus
extern "C" {
#endif
    void sampleMethod3(int *intArr, int intArrSize, const char **strArr, int strArrSize) {
        for (int i = 0; i < intArrSize; i++) {
            NSLog(@"arr1 %d : %d", i, intArr[i]);
        }
        for (int i = 0; i < strArrSize; i++) {
            NSLog(@"arr2 %d : %s", i, strArr[i]);
        }
    }
#ifdef __cplusplus
}
#endif
Sample.cs
using System.Runtime.InteropServices;

public class Sample {
    [DllImport("__Internal")]
    private static extern void sampleMethod3(int[] intArr, int intArrSize, string[] strArr, int strArrSize);
}

ポインタで受け取る。文字列の場合は、const char のダブルポインタになる。
配列のサイズも一緒に渡すと良いと思う。

staticメソッド

Sample.mm
#ifdef __cplusplus
extern "C" {
#endif
    // C#の関数の関数ポインタ
    typedef void (*cs_callback)(int);

    void sampleMethod4(cs_callback callback) {
        callback(9);
    }
#ifdef __cplusplus
}
#endif
Sample.cs
using System.Runtime.InteropServices;
using AOT;

public class Sample {
    delegate void callback_delegate(int val);

    [DllImport("__Internal")]
    private static extern void sampleMethod4(callback_delegate callback);

    // コールバック関数を、MonoPInvokeCallbackを付けてstaticで定義
    [MonoPInvokeCallback(typeof(callback_delegate))]
    private static void cs_callback(int val) {
        UnityEngine.Debug.Log ("cs_callback : " + val);
    }

    private static void sampleMethod4Invoker() {
        // staticメソッドを直接引数に渡せる
        sampleMethod4 (cs_callback);
    }
}

C#のstaticメソッドを、関数ポインタとしてObjective-Cに渡すことができる。
コールバック関数を渡して、処理完了後に呼び出してもらうことが可能。

ただ、Objective-CからC#のコールバックを呼び出す場合、
UnitySendMessageを使う方がシンプルだし一般的だと思う。

C#のインスタンス

Sample.mm
#ifdef __cplusplus
extern "C" {
#endif
    // C#のSampleクラスのインスタンス
    typedef void *_Mono_Sample_Instance;

    // C#の関数の関数ポインタ
    typedef void (*cs_callback)(_Mono_Sample_Instance, int);

    void sampleMethod5(_Mono_Sample_Instance instance) {
        NSLog(@"sampleMethod5 called.");
        // C#のコールバック呼び出し
        cs_callback(instance, 999);
    }
#ifdef __cplusplus
}
#endif
Sample.cs
using System.Runtime.InteropServices;

public class Sample {
    delegate void callback_delegate(IntPtr gameObjectPtr, int val);

    [DllImport("__Internal")]
    private static extern void sampleMethod5(IntPtr gameObjectPtr, callback_delegate callback);

    [MonoPInvokeCallback(typeof(callback_delegate))]
    private static void cs_callback(IntPtr gameObjectPtr, int val) {
        GCHandle handle = (GCHandle) gameObjectPtr;
        // ポインタからオブジェクトを取得する
        GameObject gameObject = handle.Target as GameObject;
        // 不要になったタイミングで、AllocしたオブジェクトはFreeする
        handle.Free ();

        UnityEngine.Debug.Log ("cs_callback : " + gameObject.name);
    }

    private static void sampleMethod5Invoker() {
        GameObject gameObject = new GameObject ("Sample_GameObject");
        // GCに解放されないように、Allocする
        IntPtr gameObjectPtr = (IntPtr)GCHandle.Alloc (gameObject);
        sampleMethod5 (gameObjectPtr, cs_callback);
    }
}

C#のインスタンスもIntPtrで渡すことができる。
使い道としては、Objective-Cに渡したC#のインスタンスを、コールバックでまたC#に
渡してもらう形になると思う。
これを使うと、上記で記載したstaticメソッドを渡してコールバックを呼ぶ時に、
そこからインスタンスメソッドを呼ぶ事ができる。

戻り値

プリミティブ型 (int, float, bool)、文字列 (string)

Sample.mm
#ifdef __cplusplus
extern "C" {
#endif
    int sampleMethod6() {
        return 5;
    }

    float sampleMethod7() {
        return 1.1f;
    }

    bool sampleMethod8() {
        return true;
    }

    const char* sampleMethod9() {
        const char *str = "test";
        char* retStr = (char*)malloc(strlen(str) + 1);
        strcpy(retStr, str);
        retStr[strlen(str)] = '\0';
        return retStr;
    }
#ifdef __cplusplus
}
#endif
Sample.cs
using System.Runtime.InteropServices;

public class Sample {
    [DllImport("__Internal")]
    private static extern int sampleMethod6();

    [DllImport("__Internal")]
    private static extern float sampleMethod7();

    [DllImport("__Internal")]
    private static extern bool sampleMethod8();

    [DllImport("__Internal")]
    private static extern string sampleMethod9();
}

文字列はC側でmallocして返却する必要があるので注意。

配列

Sample.mm
#ifdef __cplusplus
extern "C" {
#endif
    void sampleMethod10(int **arrPtr, int *size) {
        int arr[] = {1, 2, 3};
        int *retArr = (int *)malloc(sizeof(arr));
        memcpy(retArr, arr, sizeof(arr));
        *arrPtr = retArr;
        *size = sizeof(arr) / sizeof(arr[0]);
    }
#ifdef __cplusplus
}
#endif
Sample.cs
using System;
using System.Runtime.InteropServices;

public class Sample {
    [DllImport("__Internal")]
    private static extern void sampleMethod10(out IntPtr arrPtr, out int size);

    private static int[] sampleMethod10Invoker() {
        IntPtr arrPtr = IntPtr.Zero;
        int size = 0;
        sampleMethod10 (out arrPtr, out size);
        int[] arr = new int[size];
        Marshal.Copy (arrPtr, arr, 0, size);
        return arr;
    }
}

だいぶ面倒な感じ。
単純にreturnで返却は無理っぽい(サイズも返却する必要がある)ので、
C#から変数を参照渡し(out / ref)して受け取る。配列はIntPtrで受ける。
C側ではmallocで確保した配列のポインタとそのサイズを、参照渡しされた変数にセットする。
最後にIntPtrで受け取った配列を、Marshal.CopyでC#の配列にコピーする。

Objective-Cのインスタンス

Sample.mm
@interface Sample : NSObject
- (void)test;
@end

@implementation Sample
- (void)test {
    NSLog(@"test called.");
}
@end

#ifdef __cplusplus
extern "C" {
#endif
    Sample* sample_init() {
        Sample *sample = [[Sample alloc] init];
        CFRetain((CFTypeRef)sample);
        return sample;
    }

    void sample_test(Sample *sample) {
        [sample test];
    }

    void cfRelease(id *obj) {
        CFRelease((CFTypeRef)obj);
    }
#ifdef __cplusplus
}
#endif
Sample.cs
using System;
using System.Runtime.InteropServices;

public class Sample {
    [DllImport("__Internal")]
    private static extern IntPtr sample_init();

    [DllImport("__Internal")]
    private static extern void sample_test(IntPtr samplePtr);

    [DllImport("__Internal")]
    private static extern void cfRelease(IntPtr samplePtr);

    private static void sampleInvoker() {
        IntPtr objcPtr = sample_init ();
        sample_test (objcPtr);
        cfRelease (objcPtr);
    }
}

Objective-Cのインスタンスは、C#側でIntPtrで保持する事ができる。
Objective-Cのインスタンスの生存期間をC#側で管理したい場合に便利。
Objective-C側はretainしてからオブジェクトをC#に返却しないと解放されてしまうので注意。
C#のコンストラクタでObjective-Cのインスタンスを生成、かつretainして、
デストラクタでreleaseすると良いかも。

Objective-CからC#のメソッドを呼び出す

UnitySendMessage

UnitySendMessage("GameObjectName", "MethodName", "Message to send");

http://docs.unity3d.com/ja/current/Manual/PluginsForIOS.html

UnitySendMessageは、指定したGameObjectに含まれるメソッドを呼び出す。
第3引数がパラメータになるが、文字列しか指定できない。
また、非同期となり1フレームの遅延が発生する。

C#のstaticメソッドを関数ポインタで渡す

上で記載した方法。
実装が面倒なので、問題なければ素直にUnitySendMessage使った方が良いと思う。

ビルド時の諸々対応

iOS標準のFrameworkをリンクに追加

Unityエディタ上で、ネイティブのリソース(.h, .m .mm .a .framework等)を選択し、
Inspectorから該当のフレームワークにチェックを入れておけばリンクされる。

iOS標準のライブラリをリンクに追加 (.dylib, .tbd)

PostProcessBuildで対応する。Xcode Manipulation APIを使う。

Assets/Editor/PostBuildProcess.cs
using UnityEngine;
using UnityEditor;
using UnityEditor.Callbacks;
using UnityEditor.iOS.Xcode;
using System.IO;

public class PostBuildProcess {

    [PostProcessBuild]
    public static void OnPostProcessBuild (BuildTarget buildTarget, string path) {
        string projPath = Path.Combine (path, "Unity-iPhone.xcodeproj/project.pbxproj");

        PBXProject proj = new PBXProject ();
        proj.ReadFromString (File.ReadAllText (projPath));

        string target = proj.TargetGuidByName ("Unity-iPhone");
        proj.AddFileToBuild(target, proj.AddFile("usr/lib/libsqlite3.tbd", "Frameworks/libsqlite3.tbd", PBXSourceTree.Sdk));
        proj.AddFileToBuild(target, proj.AddFile("usr/lib/libz.tbd", "Frameworks/libz.tbd", PBXSourceTree.Sdk));

        File.WriteAllText (projPath, proj.WriteToString ());
    }
}

なお、Xcode Manipulation APIが、まだ.tbdには対応できていないようなので、
対応方法は以下を参照。(たぶん近いうちに以下は不要になる)
[Unity] PostProcessBuildで*.tbdを追加する

※Unity5.3.2p1から対応されました

サードパーティのFramework/ライブラリをリンクに追加

Assets/Plugins/iOS 配下に入れておくだけでリンクされる。

info.plistに設定追加する

PostProcessBuildで対応する。

Assets/Editor/PostBuildProcess.cs
using UnityEngine;
using UnityEditor;
using UnityEditor.Callbacks;
using UnityEditor.iOS.Xcode;
using System.IO;

public class PostBuildProcess {

    [PostProcessBuild]
    public static void OnPostProcessBuild (BuildTarget buildTarget, string path) {
        string plistPath = Path.Combine(path, "Info.plist");
        PlistDocument plist = new PlistDocument();
        plist.ReadFromFile(plistPath);

        PlistElementDict rootDict = plist.root;

        PlistElementArray urlTypesArray = rootDict.CreateArray ("CFBundleURLTypes");
        PlistElementDict dict = urlTypesArray.AddDict ();
        dict.SetString ("CFBundleURLName", "xxxxx");
        PlistElementArray schemesArray = dict.CreateArray ("CFBundleURLSchemes");
        schemesArray.AddString ("xxxxx");

        File.WriteAllText(plistPath, plist.WriteToString());
    }
}

なお、ATS対策で、

<key>NSAppTransportSecurity</key>
<dict>
    <key>NSAllowsArbitraryLoads</key>
    <true/>
</dict>

をやりたいだけであれば、iOSのPlayerSettingsを開いて
「Allow downloads over HTTP」をONにするだけで良い(デフォルトON)

ビルド時に各種フラグとか追加

PostProcessBuildで。

Assets/Editor/PostBuildProcess.cs
using UnityEngine;
using UnityEditor;
using UnityEditor.Callbacks;
using UnityEditor.iOS.Xcode;
using System.IO;

public class PostBuildProcess {

    [PostProcessBuild]
    public static void OnPostProcessBuild (BuildTarget buildTarget, string path) {
        string projPath = Path.Combine (path, "Unity-iPhone.xcodeproj/project.pbxproj");

        PBXProject proj = new PBXProject ();
        proj.ReadFromString (File.ReadAllText (projPath));

        string target = proj.TargetGuidByName ("Unity-iPhone");
        // Enabled Modules = YES
        proj.SetBuildProperty (target, "CLANG_ENABLE_MODULES", "YES");
        // Enable Objective-C Exceptions = YES
        proj.SetBuildProperty (target, "GCC_ENABLE_OBJC_EXCEPTIONS", "YES");
        // "Other Linker Flags" に "-ObjC" を追加
        proj.AddBuildProperty (target, "OTHER_LDFLAGS", "-ObjC");

        File.WriteAllText (projPath, proj.WriteToString ());
    }
}

設定するキー名がわからない場合は、.xcodeproj の中の project.pbxproj を
テキストで開いて中を見るのが手取り早いと思う。

pbxprojとinfo.plistの編集については、基本 PostProcessBuild でできるはず。
PBXProjectクラスでうまく設定できなくても、最悪読み込んだ文字列を置換すればいける。

なお、ソースに対して "-fno-objc-arc" フラグを付けたい場合は、Unityエディタ上で
対象のソースを選択し、Inspectorで 「Compile flags」に設定すれば良い。

以上。

Why do not you register as a user and use Qiita more conveniently?
  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
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  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