本編「【Unity】iOSネイティブプラグイン開発を完全に理解する」の追記記事です。
記事中での用語や略称についてはそのまま本編に倣う形とし、記事の内容自体もある程度は本編を読み進めているのを前提に記載していきます。
先日辺りに @fuziki さんが書かれた以下の記事を試す機会があり、実際に自分がObjC++で実装したiOSネイティブプラグインを書き直してみた所、 「ほんまにSwiftだけで完結できるやんけ...」 となったので、遅ればせながらも本編にある各章のサンプルをSwiftオンリーの実装に書き換える形で補足して行ければと思います。
特にSwiftでのポインタの扱いや、ARC(自動参照カウント)から手動での参照カウントへの切り替えと言ったメモリに関する操作周りはObjC++には無い新しい要素が出てくるので、そこらも適宜補足しつつ解説して行ければと思います。1
ここからは本編にある以下の章の内容をSwiftオンリーの実装に置き換える形で解説していきます。
他にも本編を除いて以下の付録記事でもObjC++でサンプルを実装してますが、基本的にはこの記事の内容を把握出来ていれば自前で置きかえ自体は可能かと思われるので、以下については割愛します。
「最小構成から見るネイティブプラグイン基礎」を置き換える
こちらは本編で実装しているExample.mmの内容がそのままSwiftでの実装(Example.swift
)に置き換わるイメージになります。
プロジェクト及び置き換えたExample.swift
のコード全体はこちら。
Exampleクラスを置き換える
ObjC++で実装されているExampleクラスをSwiftでの実装に置き換えると以下のようになります。
今回の例だとSwift化に伴い、NSObject
の継承は不要となっているので消してます。
import Foundation
public class Example {
/// ログに"Hello World"と出力して2を返す。
/// NOTE: ここではクラスメソッド(静的関数)として実装
///
/// - Returns: 2固定
public static func printHelloWorld() -> Int32 {
// ログ出力
print("Hello World")
// 戻り値を返す
return 2
}
}
そもそも「Swiftでクラスを実装する際にNSObject
を継承すべきか?」の話については、パフォーマンスなどの観点から基本的には必要な場合を除いて不要になっているという認識です。
extern "C"
で外部宣言している関数を置き換える
こちらは今回の参考記事にもある通り、@_cdecl
を用いてSwift上からCの関数として定義するようにします。
// `@_cdecl("[メソッド名]")`を使うことでCの関数として定義することが可能
// NOTE: この関数が実際にUnity(C#)から呼び出される
@_cdecl("printHelloWorld")
public func printHelloWorld() -> Int32 {
// ↑で実装している`Example.printHelloWorld`を呼び出すだけ。
// NOTE: クラスメソッド(静的関数)として実装しているので、クラスをインスタンス化せずに直接呼び出せる
return Example.printHelloWorld()
}
あとは通常通りにC#から呼び出すだけです。
// Swiftで実装した`Example`クラスのP/Invoke
/// <summary>
/// `printHelloWorld`の呼び出し
/// </summary>
/// <remarks>
/// NOTE: Example.swiftの`@_cdecl`で定義した関数をここで呼び出す
/// - iOSのプラグインは静的に実行ファイルにリンクされるので、`DllImport`にはライブラリ名として「__Internal」を指定する必要がある
/// - `EntryPoint`にSwift側で定義されている名前を渡すことでC#側のメソッド名は別名を指定可能
/// </remarks>
[DllImport("__Internal", EntryPoint = "printHelloWorld")]
static extern Int32 PrintHelloWorld();
「ネイティブコード側でインスタンス化したオブジェクトの管理」を置き換える
こちらも本編で実装しているExample.mmの内容がそのままSwiftでの実装(Example.swift
)に置き換わるイメージになります。
プロジェクト及び置き換えたExample.swift
のコード全体はこちら。
Exampleクラスを置き換える
先ずは元のObjC++で実装されているExampleクラスをSwiftでの実装に置き換えます。
元がシンプルなので、そのまますんなりと置き換えられるかと思います。
import Foundation
public class Example {
// メンバ変数
private var member: Int32 = 0
/// メンバ変数に値を設定
func setMember(with value: Int32) {
member = value
}
/// ログに"Hello World"と出力してメンバ変数(`self.member`)に設定された値を返す
func printHelloWorldWithMember() -> Int32 {
// ログ出力
print("Hello World : [\(member)]")
// 戻り値を返す
return member
}
}
extern "C"
で外部宣言している関数を置き換える
ここからはSwift上でポインタを扱う必要が出てくる都合上、若干複雑になってくる(と言うよりかは新しい知識が出てくる)ので順に解説していきます。
▼ インスタンスの生成
ここで行っていることを解説すると以下のようになります。
- インスタンスの生成
-
Unmanagedを利用して、生成したインスタンスをARC(自動参照カウント)の管理下から外す
- その上でpassRetainedを経由することで手動で参照カウンタをインクリメント(retain)しつつ、インスタンスに対応する
Unmanaged
型を取得
- その上でpassRetainedを経由することで手動で参照カウンタをインクリメント(retain)しつつ、インスタンスに対応する
- 更にUnmanaged.toOpaqueでインスタンスの生ポインタ(
UnsafeMutableRawPointer
)を取得- 取得できるポインタはUnsafeMutableRawPointerと言うミュータブルな型で返ってくるので、こちらをUnsafeRawPointerと言うイミュータブルな型に変換して返す2 3
// インスタンス化
// NOTE: 戻り値のポインタをC#側でIntPtrなどで保持し、インスタンスメソッドの呼び出し時に渡して使う
@_cdecl("createExample")
public func createExample() -> UnsafeRawPointer {
let instance = Example()
// `Unmanaged`を利用することで参照型をARC(自動参照カウント)の管理下から外すことが可能
// → 以下の処理は自前で参照カウンタをインクリメント(retain)しつつ、インスタンスに対応する`Unmanaged`型を取得している
let unmanaged = Unmanaged<Example>.passRetained(instance)
// `Unmanaged.toOpaque`でインスタンスの生ポインタを取得可能
// ただし、型が`UnsafeMutableRawPointer`なので、一応は意図を明示的にするために`UnsafeRawPointer`に変換している。
// (P/Invokeに於いてはあまり意味は無いかもだが..)
return UnsafeRawPointer(unmanaged.toOpaque())
}
Unmanaged
やUnsafeMutableRawPointer
なりと色々出てきてますが、順に解説していきます。
★ Unmanaged
型について
SwiftにはUnmanaged
(正確にはUnmanaged<T>
)と言う型があり、こちらを使うことで以下のようなメモリに関するアンマネージドな操作を行うことが可能となります。
- 参照型のインスタンスをARC(自動参照カウント)の管理下から外し、手動での管理4に切り替える
- 管理下から外した参照型のインスタンスの生ポインタを取得
- 逆に生ポインタを
Unmanaged
型へと変換し、参照型のインスタンスを取得することも可能
- 逆に生ポインタを
こちらの詳細については以下の記事が参考になります。
この性質からUnmanaged
を用いた上で誤ったメモリ操作を行うと、メモリリークやクラッシュと言った事故を引き起こしてしまうので、取り扱いには注意する必要があります。
→ e.g. 参照カウンタをインクリメント(retain)したままデクリメント(release)を行わない、解放済みのインスタンスに対するメモリアクセスなど
★ Swiftでのポインタ型の扱いについて
Swiftではコード上でポインタ型を表すためにはUnsafePointer
型及びそれらの派生系を利用する必要があります。
こちらについては以下の記事が参考になります。
ちなみに、Unmanaged.toOpaqueでポインタを取得した場合には「型情報を持たないUnsafeMutableRawPointer
と言う型」が返ってきます。
その上でもし値を書き換える想定が無い場合にはイミュータブル版であるUnsafeRawPointer
に型変換しておくのが安全かもしれません。
★ Swift側で返したUnsafeRawPointer
型は、C#上でIntPtr
型として受け取れる
閑話休題。
Swift側にて@_cdecl
で定義したfunc createExample() -> UnsafeRawPointer
は、通常通りにC#側でIntPtrとして受け取ることが出来ます。
// ObjectiveC++コードで実装した`Example`クラスのP/Invoke
// ネイティブコード側にあるExampleクラスのインスタンス化
// NOTE: 戻り値はインスタンスのポインタ
[DllImport("__Internal", EntryPoint = "createExample")]
static extern IntPtr CreateExample();
▼ インスタンスの解放
インスタンスの解放は以下のようになります。
C#から渡された生ポインタをUnmanaged
型へと戻し、releaseを叩くことで参照カウンタをデクリメント(release)して解放します。
// 解放
@_cdecl("releaseExample")
public func releaseExample(_ instancePtr: UnsafeRawPointer) {
// 生ポインタ(UnsafeRawPointer)は`fromOpaque`に渡すことでUnmanaged型に変換可能
let unmanaged = Unmanaged<Example>.fromOpaque(instancePtr)
// `createExample()`でインクリメント(retain)した参照カウンタをデクリメント(release)することで解放
unmanaged.release()
}
▼ インスタンスメソッドの呼び出し。
インスタンスメソッドの呼び出し手順について、先ずは先述した通りにC#から渡された生ポインタをUnmanaged
型へと戻し、更にここからtakeUnretainedValueを叩くことで 参照カウンタをインクリメント(retain)せずにExample
クラスのインスタンスを取得します。
後はインスタンスまで取得できたらメソッドを呼び出すだけです。
// 以下はインスタンスメソッドの呼び出し
// NOTE: 第一引数にはインスタンス化した際に保持しているポインタを渡す
@_cdecl("setMember")
public func setMember(_ instancePtr: UnsafeRawPointer, _ value: Int32) {
// `Unmanaged<T>.takeUnretainedValue`でUnmanaged型をインスタンスに戻すことが可能
// ここではメソッドを呼び出したいだけであり、参照カウンタはそのままで居て欲しいので`takeUnretainedValue`を利用している
// NOTE: 逆にインスタンスに戻す際に参照カウンタをインクリメント(retain)したい場合には`takeRetainedValue`が使える
let instance = Unmanaged<Example>.fromOpaque(instancePtr).takeUnretainedValue()
instance.setMember(with: value)
}
@_cdecl("printHelloWorldWithMember")
public func printHelloWorldWithMember(_ instancePtr: UnsafeRawPointer, _ value: Int32) -> Int32 {
// `setMember`と同上
let instance = Unmanaged<Example>.fromOpaque(instancePtr).takeUnretainedValue()
return instance.printHelloWorldWithMember()
}
ちなみにコメントにも記載してますが、もしインスタンスに戻す際に参照カウンタをインクリメント(retain)したい場合にはtakeRetainedValueを叩くことで操作することが可能です。
「ネイティブコードからC#のメソッドを呼び出す」を置き換える
最後に表題の章の実装を置きかえます。
とは言え、こちらも本編で実装しているExample.mmの内容がそのままSwiftでの実装(Example.swift
)に置き換わるイメージになります。
C#側のコードは大幅には変わっていないものの、以下の記事を参考にすると、ちゃんとUnmanagedFunctionPointer
やMarshalAs
と言った各種Attributeを付けたほうが良さそう...と思ったので、こちらを付け加える改修だけ加えてます。5
プロジェクト及び置き換えたExample.swift
のコード全体はこちら。
-
swift/callbackExample
- Example.swift
- Example.cs (※ロジックはそのままで、P/Invokeに関するAttributeだけ付け加えてます)
関数ポインタを定義
Swift型では以下のように@convention(c)
を付けることでC言語の関数ポインタ形式で定義することが出来ます。
(参考: Swiftのattributeまとめ[Swift4対応] - @convention)
// NOTE: C#側で定義している以下のデリゲート型に対応する関数ポインタ
// > delegate void SampleCallbackDelegate(Int32 num);
//
// ちなみに`@convention(c)`はC言語の関数ポインタ形式であることを指す
public typealias SampleCallbackDelegate = @convention(c) (Int32) -> Void
こちらはC#のコード上では以下のデリゲート型及びコールバックに対応します。
// 登録するメソッド(ここで言う`SampleCallback`)と同じフォーマットのデリゲート
// NOTE: ネイティブコード側で定義している以下の関数ポインタに対応する
// > typedef void (* sampleCallbackDelegate)(int32_t);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
delegate void SampleCallbackDelegate([MarshalAs(UnmanagedType.I4)] Int32 num);
// 実際にネイティブコードから呼び出されるメソッド
// NOTE: iOS(正確に言うとAOT)の場合には「staticメソッドな上で`MonoPInvokeCallbackAttribute`を付ける必要がある」
[AOT.MonoPInvokeCallbackAttribute(typeof(SampleCallbackDelegate))]
static void SampleCallback(Int32 num)
{
Debug.Log($"ネイティブコードから呼び出された : {num}");
}
Exampleクラスを置き換える
こちらも特に特筆すべき項目はなく、そのまま置き換えられるかと思います。
public class Example {
// メンバ変数
// NOTE: `registerSampleCallback`から渡されるC#のコールバックを持つ
private var sampleCallbackDelegate: SampleCallbackDelegate? = nil
/// コールバックの登録
///
/// NOTE:
/// メンバ変数`sampleCallbackDelegate`にC#から渡されるコールバックを登録
/// (C#風に言い換えるとデリゲート型のフィールドにメソッドを渡すイメージ)
func registerSampleCallback(_ delegate: @escaping SampleCallbackDelegate) {
sampleCallbackDelegate = delegate
}
/// `sampleCallbackDelegate`に登録してあるコールバックを呼び出す
func callSampleCallback() {
sampleCallbackDelegate?(2);
}
}
extern "C"
で外部宣言している関数を置き換える
▼ インスタンスの生成と解放
こちらは前の章と同じです。(なので詳細については割愛)
// インスタンスの生成・解放
// インスタンス化
// NOTE: 戻り値のポインタをC#側でIntPtrなどで保持し、インスタンスメソッドの呼び出し時に渡して使う
@_cdecl("createExample")
public func createExample() -> UnsafeRawPointer {
let instance = Example()
let unmanaged = Unmanaged<Example>.passRetained(instance)
return UnsafeRawPointer(unmanaged.toOpaque())
}
// 解放
@_cdecl("releaseExample")
public func releaseExample(_ instancePtr: UnsafeRawPointer) {
let unmanaged = Unmanaged<Example>.fromOpaque(instancePtr)
unmanaged.release()
}
▼ コールバックの登録と呼び出し
前の章の内容さえ把握出来ていれば、こちらもほぼ直感的に書くことが出来るかと思います。
ちなみに、この例だとコールバック自体はスコープ外でも保持されるので@escaping
を付ける必要がります。
// C#から渡されるコールバックをインスタンスに登録
@_cdecl("registerSampleCallback")
public func registerSampleCallback(_ instancePtr: UnsafeRawPointer, _ delegate: @escaping SampleCallbackDelegate) {
// NOTE: こういう感じに直接呼び出すこともできる
//delegate(1)
// インスタンスに登録
let instance = Unmanaged<Example>.fromOpaque(instancePtr).takeUnretainedValue()
instance.registerSampleCallback(delegate)
}
// インスタンスに登録したコールバックを呼び出し
@_cdecl("callSampleCallback")
public func callSampleCallback(_ instancePtr: UnsafeRawPointer) {
let instance = Unmanaged<Example>.fromOpaque(instancePtr).takeUnretainedValue()
instance.callSampleCallback()
}
-
ObjC++元いObjC自体はC言語の完全上位互換なので、ポインタ自体の扱いは言語仕様的に楽と言えば楽だった ↩
-
コメントでも記載しているが、P/Invokeと言う性質を踏まえると処理自体には特に意味は無いかもだが、「コード上で書き換える意図は無い」と言うのを自明にするために敢えて型変換を行っている ↩
-
なので、ここは実際には型変換を行わずとも
UnsafeMutableRawPointer
型のまま返しても処理的には問題はない ↩ -
所謂MRC(Manual Reference Counting)と言うやつ?(
Unmanaged
を利用した手動管理もMRCと呼んで良いのかは不明) ↩ -
無くても動くと言えば動くが...有りと無しの挙動の違いを理解しきれていない...調査する機会があったら理解でき次第に追記予定 ↩