Swiftプロジェクト内でC++を使うには

はてな今週のお題はテストです」

うるせえ! 私は書きたいことを書く。

SwiftはObjective-Cとの互換性があると聞いた。

であれば、もちろんC, C++も使えるはず、と思っていた。実際に、公式ドキュメントのAbout Swiftの項にも、

Swift is a new programming language for iOS and OS X apps that builds on the best of C and Objective-C, without the constraints of C compatibility.

と書いてある。しかし、C++との互換性があるとは書いていない。「CとObjective-Cの上に構築する」とは書いてあるものの、「C互換の制約に縛られずに」という部分も気になる。

ふと思ったのだが、Objective-Cでは、C++を使う場合、実装ファイル名を.mmにする。

Swiftの場合はどうする?


【公式】
https://developer.apple.com/library/prerelease/ios/documentation/Swift/Conceptual/BuildingCocoaApps/index.html

NOTE
You cannot import C++ code directly into Swift. Instead, create an Objective-C or C wrapper for C++ code.

Swift内にC++コードを直接importすることはできません。
代わりに、C++コードをラップするCか、Objective-Cのコードを作成してください。


なんということでしょう。

まーそうかー。考えてみれば、Swiftのコード内にObjective-CC++のコードがごちゃまぜに混在していたら、いくらなんでもカオスすぎる。

まあ、C++が使えないならSwiftなんて使わないけど、単純にObjective-Cクラスとしてラップすれば良いだけならば、まだ許容できる。じゃあいいか。


・・・


いや、信用できない!

本当にできるか試してみる。

Xcode 6.0で新規にSwiftプロジェクトを作成。
・「File」→「New」→「File ...」で「Objective-C File」を選択。
・ファイル名を Wrapper.mm とした。

すると、ダイアログが出てきた。

Would you like to configure an Objective-C bridging header?

Yes/No/Cancel

ブリッジングヘッダ? なにそれ?

ああ、うん、もちろんそいつはYesだ! (よく解っていない)

すると、ProjectName-Bridging-Header.hというファイルが自動生成された。

中身はコメントだけで空である。

少し調べてみると、このブリッジングヘッダーなるものは、Swiftコード内で使用する自作Objective-Cクラスをインポートしておくものらしい。このファイル内でimportしたObjective-Cクラスは、Swiftコード内で、Swiftクラスとして使用可能みたい。

【参考】
https://developer.apple.com/library/prerelease/ios/documentation/Swift/Conceptual/BuildingCocoaApps/MixandMatch.html

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

#import "Wrapper.h"

なるほどね。

次に、Wrapper.hにC++コードをラップするObjective-Cクラスを作るわけだが、諸事情によりvoid*ポインタを使うことにする。*1

Wrapper.h

#import <Foundation/Foundation.h>

@interface MyObjcClass : NSObject
{
    void* _cppObj;    // C++クラスのインスタンスを参照するポインタ
}

-(void)test;    // このメソッドをSwiftから呼べて、内部でC++コードが動けばOK、とする

@end

次に、.mmファイル内でC++クラスを定義し、それをラップするObjective-Cクラスを実装してみる。

ただ動作できるか確認するだけなので、内容はないよう・・・

Wrapper.mm

#import "Wrapper.h"

#include <vector>
#include <iostream>

class MyCppClass
{
public:
    MyCppClass& add(int i) {vct.push_back(i); return *this;}
    void trace() {std::for_each(vct.begin(), vct.end(), [](int i){std::cout << i << std::endl;});}
    
private:
    std::vector<int> vct;
};

@implementation MyObjcClass

-(id)init {
    self = [super init];
    
    if (self) {
        _cppObj = new MyCppClass();
        MyCppClass& obj = *static_cast<MyCppClass*>(_cppObj);
        obj.add(10).add(20).add(30);
    }
    
    return self;
}

-(void)dealloc {
    delete static_cast<MyCppClass*>(_cppObj);
}

-(void)test {
    MyCppClass& obj = *static_cast<MyCppClass*>(_cppObj);
    obj.trace();
}

@end


こうして、今、MyCppClassを内部的に使用する、MyObjcClassが完成したわけだ。

あとはSwift内から、MyObjcClassを使えれば良いということになる。

ViewController.swift

import UIKit

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // ブリッジングヘッダでimportしておくだけで、ここでは特にimportはいらないみたい。
        let obj = MyObjcClass()      // Objective-Cクラスとして作成したものをインスタンス化
        obj.test()                   // そいつのメソッドを呼んでみる
    }
}

実行してみると、きちんと動作した。

10
20
30

結論として、ある程度のモジュール化は義務付けられることになるだろうが、Swiftを主として使うプロジェクト内においても、局所的にC++を使うことはできる。その場合、C++コードをObjective-Cクラスでラップするか、あるいはCの関数でラップする。*2


まあ、ひとまず安心といったところか。

*1:Objective-Cの時代から、.h内でvectorをincludeしたり、C++クラスの前方宣言をしたり、名前空間を使ったりすると、.mm以外(.m)からimportされたときに問題が起こる。というか、ヘッダファイル内でC++構文を使うという行為は、.cpp, .mmでinclude, importされる場合にしか許容されない。私の場合は、C++を実際に使用するObjective-C++ソースだけを.mmとして局所化することにした。これは即ち、.hを直接、あるいは間接的にimportするすべてのObjective-Cソースを.mmにするようなことはせずに、.h内でC++の構文を使わないようにする、という方針である。今回もそれに従っている。

*2:ここでは記述していないが、ほぼ同様の手順で、Cの関数をブリッジングヘッダー経由で、Swiftから使用できることも確認した。即ち、Swiftソース内で自由にCの命令を書くことはできないが、Swiftから自作C関数を呼び出すことはできる。C++クラスをObjective-Cクラスでラップすると、メッセージ送信のオーバーヘッドを伴うため、場合によってはC関数でラップする方が有効だと思う。具体的なやり方についてはまた別途。