[Swift] Objective-C++に移行した際のマングリングの問題

written: kengo

Swiftを使うにあたり、これまでのObjCのコード資産をうまく使うため、ObjCをなるべくC言語にラップしてSwiftで使うようにしていました。 しかしC++も一緒に使いたくなったので(主にOpenCV3.0的な意味合いで。)、Objective-C++へ移行することを検討しました。 そこで問題になったことをメモとして残しておきます。

ObjCからObjC++へ

今回の移行はiOSとOSXと両方を対象に考えています。

だいぶ前にトンチンカンなこと書いてましたが、ObjCのコードをObjC++でコンパイルするのは本来簡単なはずで、今回あらためて試してみたところ、実際簡単でした。全ての.mファイルを.mmに置き換えるだけでObjC++に変わります。いくつかのAPIがコンパイル通らないように思っていましたが、フレームワークあたりをいじっていたらiOSもOSXもコンパイルは通りました。これによってiOS/OSX向けにC++のコードが書けるようになりました。

C++を直接使えないSwiftとC関数経由で連携する

SwiftはC++を直接ブリッジできません。C言語かObjCクラスでラップして間接アクセスさせる必要があります。今回はC++のクラスをC言語でラップするパターンを考えます。KNGCppというC++のクラスをSwiftで呼び出してみます。XCodeのプロジェクトでOSX向けコマンドラインツールを選択し、以下のコードを作成しました。

KNGCpp.h

#ifndef KNGCpp_h
#define KNGCpp_h

#include <stdio.h>

// C言語の関数
void KNGCppTestFunc();

#endif
KNGCpp.hは後述するSwiftとのブリッジファイルで共有するヘッダです。 SwiftとのブリッジするヘッダはCの関数のみにしておきます。C++の記述がエラーになるためです。

KNGCpp.mm

#include "KNGCpp.h"

// C++のクラス
class KNGCpp
{
public:
	KNGCpp() {}
	void testFunc();
};

// C++のテスト関数
void KNGCpp::testFunc()
{
	printf("C++のコードを呼び出しましたよ");
}

// C関数でC++のクラスを作って呼ぶ
void KNGCppTestFunc()
{
	KNGCpp cpp;
	cpp.testFunc();
}
C++の記述はSwiftと直接ブリッジしないヘッダか実装ファイルに閉じ込めておきます。 今回はKNGCpp.mm内で完結するようにしました。C言語実装のKNGCppTestFunc()の中で、 C++のKNGCppクラスを実体化しtestFunc()を呼び出しています。

KNGCpp-Bridging-Header.h

//
//  Use this file to import your target's public headers that you would like to expose to Swift.
//

#include "KNGCpp.h"
Swift用のブリッジファイルです。KNGCpp.hをインクルードしておきます。

main.swift

import Foundation

// C言語関数を呼び出す
KNGCppTestFunc()
SwiftではC言語実装のKNGCppTestFunc()関数を呼び出すのみです。

リンクでエラーが発生する

上記のコードはコンパイルまでは問題なく通りますが、リンカに怒られます。

Undefined symbols for architecture x86_64:
  "_KNGCppTestFunc", referenced from:
      _main in main.o

_KNGCppTestFuncが定義されてないよ、とのこと。コンパイラが通ってるんだから大丈夫だと思いきや落とし穴でした。最初なんのことかよくわかっていなかったのですが、CコンパイラとC++コンパイラの関数の解釈の違いから起きるものであるとわかりました。

ObjCコンパイラとObjC++コンパイラのマングリング

詳しくは参考にしましたサイトさまをご覧いただくと良いと思います。こちらのサイトではCとC++コンパイラの違いになっていますが、SwiftコンパイラとObjC++コンパイラも同様でした。

かいつまむとKNGCppTestFunc()をコンパイルしたとき、CコンパイラとC++コンパイラが解釈するシンボル名が異なることが問題でした(マングリング)。

SwiftコンパイラはKNGCppTestFunc()をC言語として解釈しようとしたが、ObjC++コンパイラはKNGCppTestFunc()をC++としてコンパイルしているので、関数名が一致しなくなっていたということです。

マングリングの回避 extern “C”

Swiftでもマングリングが発生するということが分かったので、ヘッダにextern “C”を入れることで回避します。 extern “C”のスコープ範囲内にある記述は、C++コンパイラでもC言語的に解釈させます。

KNGCpp.h(改良)

#ifndef KNGCpp_h
#define KNGCpp_h

#include <stdio.h>

// C++でもC言語の関数として解釈するようにする
#ifdef __cplusplus
extern "C" {
#endif

// C言語の関数
void KNGCppTestFunc();

// extern "C"の閉じカッコ
#ifdef __cplusplus
}
#endif

#endif

extern “C”はCコンパイラ系では認識しないためC++のときのみ効くようにする必要があります。 C++コンパイラでは__cplusplusが定義されるので、C++のときのみextern “C”が使われるようにマクロを記述します。

これでSwiftとのリンクの問題も解決でき、無事C++のコードを動作させることができました。

References