😸

C++ライブラリをiOSプロジェクトに追加する方法について

2021/05/09に公開

XCode12.4でC++ライブラリをiOSアプリで使う方法について説明します。色々試してみて、IDEの支援を受けられる等の観点から落ち着いた方法としては、Swift Package Manager(以下SPM)を使った方法です。

CPMはバイナリとしてxcframework形式をサポートするので、まずC++ライブラリをxcframeworkとしてビルドします。C++ライブラリの構造は以下のような簡単なものです。

ライブラリ構成
.
├── include
│   └── native_add.h
└── native_add.cpp
native_add.cpp
#include "include/native_add.h"

int32_t native_add(int32_t a, int32_t b) {
    return a + b;
include/native_add.h
#pragma once

#include<stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
    int32_t native_add(int32_t a, int32_t b);
#ifdef __cplusplus
}

これをiOS甩にビルドするために、buildディレクトリを作ってそこにCMakeLists.txtなどを用意します。

ディレクトリ構成
.
├── build
│   ├── bin
│   ├── build.bash
│   └── ios
│       ├── CMakeLists.txt
│       └── ios.toolchain.cmake
├── include
│   └── native_add.h
└── native_add.cpp
CMakeLists.txt
cmake_minimum_required(VERSION 3.15)
project(native_add)

set(CMAKE_CXX_STANDARD 14)

FILE(GLOB SRC ../../*.cpp)
add_library(native_add STATIC ${SRC})
build.bash
#!/bin/bash
cmake -Sios -G Xcode ./ios -B bin/ios_x86_64 -DPLATFORM=SIMULATOR64 -DCMAKE_TOOLCHAIN_FILE=ios.toolchain.cmake
cmake --build ./bin/ios_x86_64
cmake -Sios -G Xcode ./ios -B bin/ios_arm64 -DPLATFORM=OS64 -DCMAKE_TOOLCHAIN_FILE=ios.toolchain.cmake
cmake --build ./bin/ios_arm64
xcodebuild -create-xcframework \
    -library bin/ios_arm64/Debug-iphoneos/libnative_add.a -headers ../include \
    -library bin/ios_x86_64/Debug-iphonesimulator/libnative_add.a -headers ../include \
    -output libnative_add.xcframework

ios.toolchain.cmakeは次のように取得します。

curl -OL https://raw.githubusercontent.com/leetal/ios-cmake/master/ios.toolchain.cmake

この状態で次のようにbuild.bashを実行するとxcframework形式のライブラリlibnative_add.xcframeworkが出来ます。

$ cd path/to/project/build
$ ./build.bash

C++の方は準備ができたのでSPM形式のパッケージを作ります。XCodeのFile > New > Swift Package...を選択して作ります。ここでのパッケージ名はSPMSampleにします。

パッケージが出来たので、SPMSample.swiftを削除して次のような構成にします。SPMはターミナルからディレクトリを作ったり、ファイルを追加してもXCodeに反映されるようなので、ターミナルで作っていきます。

ディレクトリ構成
.
├── Frameworks
│   └── libnative_add.xcframework
├── Package.swift
├── README.md
├── Sources
│   └── SPMSample
│       ├── dummy.c
│       └── include
│           ├── module.modulemap
│           ├── native_add.h
│           └── shim.h
└── Tests
...

libnative_add.xcframeworkは先程ビルドしたもので、Sources/SPMSample/includenative_add.hはC++プロジェクトから持ってきたものです。dummy.cはSPM的に必要なだけで空ファイルです。module.modulemapはC言語(Objective-C)をモジュール化するもので、これを用意するとimport文でSwiftから取り込めるようになります。中身はこんな感じです。

module.modulemap
module SPMSample {
    umbrella header "shim.h"
    export *
    link "native_add"
}

shim.hはヘッダーファイルをまとめるものだと思います(よく分かってない)。なのでnative_add.hをインクルードしておきます。

shim.h
#ifndef shim_h
#define shim_h

#include <native_add.h>


#endif /* shim_h */

こうした状態で、Package.swiftを次のように書いておけばSPMSamplelibnative_add.aとリンクされます。

Package.swift
// swift-tools-version:5.3
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
    name: "SPMSample",
    products: [
        // Products define the executables and libraries a package produces, and make them visible to other packages.
        .library(
            name: "SPMSample",
            targets: ["SPMSample"]),
    ],
    dependencies: [
        // Dependencies declare other packages that this package depends on.
        // .package(url: /* package url */, from: "1.0.0"),
    ],
    targets: [
        // Targets are the basic building blocks of a package. A target can define a module or a test suite.
        // Targets can depend on other targets in this package, and on products in packages this package depends on.
        .binaryTarget(
            name: "libnative_add",
            path: "Frameworks/libnative_add.xcframework"),
        .target(
            name: "SPMSample",
            dependencies: ["libnative_add"],            
            // 今回のコード例では不要ですが、C++の標準ライブラリを使うときはlibc++をリンクする必要があります。
            // linkerSettings: [.linkedLibrary("c++"),
            ]),
        /*
        .testTarget(
            name: "SPMSampleTests",
            dependencies: ["SPMSample"]),

         */
    ]
)

テストコードはこのままだとエラーになるので、とりあえずコメントアウトしてます。以上で、SPMが出来ました。SPMはgitリポジトリにタグを付けておかないとアプリから使えないので、タグだけ適当に付けておきます(プロジェクトを作ったときにgitプロジェクトも作られた前提です)。

$ cd /path/to/project
$ git add .
$ git commit -m "Mod."
$ git tag 0.0.1

最後にこのライブラリをiOSアプリから読んで終わりにします。iOSアプリはXCodeからFile > New > Project...を選んでiOSのAppを選択します。今回はSwiftUIにもチェックをいれています。プロジェクトが出来たらFile > Swift Packages > Add Package Dependency...を選択して、プロジェクトまでのパスをfile://~で指定します。

追加出来たら、SPMSampleをインポートしてnative_addを呼び出しを追加します。

ContentView.swift
import SwiftUI
+ import SPMSample

struct ContentView: View {
    var body: some View {
+        Text("23 + 24 = \(SPMSample.native_add(23, 24))")
            .padding()
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

ビルドしてシミュレーター、実機で次のように表示されたら成功です。

Discussion