mike-neckのブログ

Java or Groovy or Swift or Golang

Swift Package Manager プロジェクト にて CPP(C++) の関数を Swift から呼び出す

f:id:mike_neck:20180609045321p:plain

一つ前のエントリーの続き。

C++ で作られた関数を Swift から呼び出す方法。 巷には C++ の関数を Swift から呼び出す方法に関する記事が溢れているが、

  • XCode 前提で Swift Package Manager プロジェクトについて触れてない
  • SPM 使っててもちょっと古い
    • Bridging-Header.h を使う方法しかない

ということで、自分で書くことにした。

なお、使っている Swift Package Manager は 4.2.1 。ここに書いた方法は macOS X Mojave と Ubuntu 16.04(Docker の swift:4.2.1 イメージ) で動作を確認した。


まず、プロジェクトの構造を作る。

.
├── Package.swift
├── Sources
│   ├── CppWrapper
│   │   ├── Cpp
│   │   │   ├── cpp_lib.cpp
│   │   │   └── cpp_lib.hpp
│   │   ├── cpp_bridge.cpp
│   │   └── include
│   │       └── cpp_bridge.hpp
│   ├── CppBridge
│   │   └── include
│   ├── CppLib
│   │   └── include
│   └── SwiftCppApp
│       ├── Bridging-Header.h
│       └── main.swift
└── Tests
    └── LinuxMain.swift

Package.swift は次のようになる。

// swift-tools-version:4.2
import PackageDescription

let package = Package(
        name: "SwiftCppApp",
        products: [
            .executable(name: "SwiftCppApp", targets: ["SwiftCppApp"]),
            .library(name: "CppWrapper", targets: ["CppWrapper"]),
        ],
        dependencies: [
        ],
        targets: [
            .target(name: "CppWrapper"),
            .target(name: "SwiftCppApp", dependencies: ["CppWrapper"]),
        ],
        cLanguageStandard: .c11,
        cxxLanguageStandard: .cxx11
)

まず、 C++ のコードを用意する。これはとある ライブラリーが C++ しか提供されていなくて、それを呼び出すために一旦 C++ で書いた場合などを想定している。 この C++ のファイルは CppWrapper の直下ではなく、 Cpp というさらに下のディレクトリーに置くところがポイント。 CppWrapper 直下に置くと、 <string> ヘッダーが見つからないというエラーが出てくる。

cpp_lib.hpp

#ifndef CPP_LIB_HPP
#define CPP_LIB_HPP

#include <string>

void show(std::string const& message);

#endif //CPP_LIB_HPP

cpp_lib.cpp

同じディレクトリーにおくので、ヘッダーは #include "cpp_lib.hpp" とクオーテーションで囲んだ形式で取り込む

#include "cpp_lib.hpp"
#include <string>
#include <iostream>

void show(std::string const& message)
{
    std::cout << message << std::endl;
}

次は C++ のラッパー部分だが、 C で読めるように翻訳する C++ で書かれたレイヤー。

cpp_bridge.hpp

#ifndef CPP_BRIDGE_HPP
#define CPP_BRIDGE_HPP

#ifdef __cplusplus
extern "C"
#endif
void show_message(char const* message);

#endif //CPP_BRIDGE_HPP

cpp_bridge.cpp

先程の cpp_lib.hppinclude ディレクトリーにないので、相対パスで取り込みをおこなう。

#include <cpp_bridge.hpp>

#include <string>
#include "Cpp/cpp_lib.hpp"

#ifdef __cplusplus
extern "C"
#endif
void show_message(char const* message) {
    std::string msg(message);
    show(msg);
}

extern "C" によって、 Swift には C のインターフェースとして見えるようになります(多分)。


後は C の関数を Swift から呼び出すコードを書けばコードは完成する。

import Foundation
import CppWrapper

let string = "hello"
var message = string

if var msg = message.cString(using: .utf8) {
show_message(&msg);
}

ビルドする際に、オプションをいくつか指定しないと失敗する。

必要なオプションは次のコマンドの通り

swift build -Xcxx -std=c++11

これを入れることで、 C++ を使っていることが認識される。

CPP(C++) の関数を C 言語から呼び出す方法

C 言語を業務でやったことがほとんどない + CPP は完全に触ったことがないので、いろいろググりつつやってみた。

f:id:mike_neck:20180609045321p:plain

最終的に Swift から C++ で定義されているオブジェクトの関数を呼び出すところまでが目標。


まず、次のような C++ の関数が定義されているものとする。

cpp_lib.h

#ifndef CPP_LIB_H
#define CPP_LIB_H

#include <string>

void show(std::string const& message);

#endif //CPP_LIB_H

cpp_lib.cpp

#include "cpp_lib.h"
#include <string>
#include <iostream>

void show(std::string const& message)
{
  std::cout << message << std::endl;
}

これは単純に std::string を受け取って、 標準出力に出力する C++ の関数。


あってるかどうかわからないが、 C の文字列をこの関数に渡すには 一度 char の配列を std::string に変換する層が必要になる。

cpp_bridge.h

#ifndef CPP_BRIDGE_H
#define CPP_BRIDGE_H

#ifdef __cplusplus
extern "C"
#endif
void show_bridge(char const* message);

#endif // CPP_BRIDGE_H

cpp_bridge.cpp

#include "cpp_bridge.h"
#include "cpp_lib.h"

#include <string>

#ifdef __cplusplus
extern "C"
#endif
void show_bridge(char const* message)
{
  std::string msg(message);
  show(msg);
}

C++コンパイルした関数は次の記事によるとシグネチャーが異なるらしいので、 extern "C" を付ける必要がある。

d.hatena.ne.jp


最後に これを C のアプリケーションを書く。

#include "cpp_bridge.h"

int main(int argc, char *argv[])
{
  char* message = "hello";
  show_bridge(message);
}

次にコンパイルする。コンパイラーは clang-3.8 を使う。

clang++-3.8 -c -Wall -o .build/cpp_lib.o cpp_lib.cpp
clang++-3.8 -c -Wall -o .build/cpp_bridge.o cpp_bridge.cpp
clang-3.8 -c -Wall -o .build/main.o main.c
clang++-3.8 -Wall -o .build/main .build/main.o .build/cpp_bridge.o .build/cpp_lib.o

Amazon 製 Server-side Swift フレームワーク smoke-framework について

f:id:mike_neck:20180609045321p:plain

これは Qiita のアドベントカレンダー 2018 の初日のエントリーです。

特に Swift コミュニティに何の貢献もしていないのですが、空いてたのでついポチッと登録してしまいました。

qiita.com


2018 年の 10 月 4 日に Prime Video のエンジニアの Simon さんのツイートが僕のTLに流れてきました。

Amazon が Swift 製の軽量サーバーサイドフレームワーク smoke-framework を作ったとのこと。

早速ためしてみたかったのですが、閃の軌跡というゲームをやるために忙しくて、ずっと試せていませんでした。 ほぼ2ヶ月経過して日本語の紹介記事等がなさそうなので、紹介してみることにしました。


smoke-framework は Swift-NIO の上に作られた軽量 Web フレームワークです。 JSON でのリクエスト-レスポンスの機能だけを提供しているだけのシンプルなものとなっています。

github.com


ここでは例として POST されたメッセージをそのまま返す Echo HTTP アプリケーションを作ってみます。

インストール

Swift Package Manager を使います。 swift package init でプロジェクトを初期化します。

mkdir ExampleResponseApp
cd ExampleResponseApp
swift package init --type executable

この後に、 Package.swift が次の通りになるように dependencies/targets/products に追記します。

// swift-tools-version:4.2

import PackageDescription

let package = Package(
    name: "ExampleResponseApp",
    products: [
        .executable(name: "ExampleResponseApp", targets: ["ExampleResponseApp"]),
    ],
    dependencies: [
        .package(url: "https://github.com/amzn/smoke-framework.git", .upToNextMajor(from: "0.6.0")),
    ],
    targets: [
        .target(name: "ExampleResponseApp", dependencies: ["SmokeOperationsHTTP1"]),
        .testTarget(name: "ExampleResponseAppTests", dependencies: ["ExampleResponseApp"]),
    ]
)

アプリケーション

このフレームワークでアプリケーションを作る場合、以下の3ステップで作れます。

  1. オペレーション関数を記述する
  2. パスとオペレーション関数を結びつけたハンドラーセレクターを作る
  3. ハンドラーセレクターをサーバーにわたす

以下のプログラムは次のモジュールをインポートしておきます。なお、 LoggerAPI は smoke-framework が依存している IBM-Swift の LoggerAPI です。

import SmokeHTTP1
import SmokeOperationsHTTP1
import SmokeOperations
import NIOHTTP1
import LoggerAPI

オペレーション関数

オペレーション関数として次の型を満たす関数を作ります

(InputType, ContextType) throws -> OutputType

オペレーション関数に挙げられている型は特定のプロトコルを満たしている必要があります。README には書かれていますがチュートリアルの部分には書いていないため、最初戸惑いました。

ContextType

ContextType はアプリケーションを実行する際の様々な情報を持つデータで、ユーザーは任意の型を作れます。

とりあえずは、このような struct を作ってみます

struct MyAppContext {}
InputTypeOutputType

次に、 InputTypeOutputType ですが、こちらは ValidatableCodable プロトコルを満たす必要があります。 ValidatableCodable プロトコルの定義は次のようになっています。

public typealias ValidatableCodable = Validatable & Codable

public protocol Validatable {
  func validate() throws
}

これを満たすように InputTypeOutputType を作ります。なお、ここにある ErrorResponse 型については次で述べます。

struct Message {
  let text: String
}

extension Message: ValidatableCodable {
  func validate() throws {
    if self.text.isEmpty {
      throw ErrorResponse.badRequest
    }
  }
}
エラーの型

投げるエラーの型は ErrorIdentifiableByDescription プロトコルを満たす必要があります。これの定義は次のとおりです。

typealias ErrorIdentifiableByDescription = Swift.Error & CustomStringConvertible

これを満たすように ErrorResponse を書いてみます。

enum ErrorResponse {
  case badRequest
}

extension ErrorResponse: ErrorIdentifiableByDescription {
  var description: String {
    return "bad request"
  }

  var statusCode: Int {
    return 400
  }
}
オペレーション関数を組み立て

上記の条件を満たした後、オペレーション関数を組み立てます。届いたメッセージをそのまま返す単純な関数です。

func echo(message: Message, context: MyAppContext) throws -> Message {
  Log.info("message: \(message.text)")
  return message
}

ハンドラーセレクターを作る

作成したオペレーション関数とパスのマッピングを作ります。まずは次のような型エイリアスをつくります。

public typealias HandlerSelector =
    StandardSmokeHTTP1HandlerSelector<MyAppContext, JSONPayloadHTTP1OperationDelegate>

StandardSmokeHTTP1HandlerSelector には addHandlerForUri(_ uri: httpMethod: operation: allowedErrors: operationDelegate:) という関数が生えているので、この関数にパスとオペレーション関数を渡します。

func handlerSelector() -> HandlerSelector {
  var selector = HandlerSelector()
  selector.addHandlerForUri(
      "/example",
      httpMethod: .POST,
      operation: echo,
      allowedErrors: [(ErrorResponse.badRequest, ErrorResponse.badRequest.statusCode)],
      operationDelegate: nil)
  return selector
}

アプリケーション組み立て

アプリケーションを組み立てます。先程準備したコンテキストオブジェクトとハンドラーセレクターを SmokeHTTP1Server の class 関数 startAsOperationServer に渡します。

do {
  try SmokeHTTP1Server.startAsOperationServer(
      withHandlerSelector: handlerSelector(),
      andContext: MyAppContext(),
      defaultOperationDelegate: JSONPayloadHTTP1OperationDelegate())
} catch {
  Log.error("failed to start server: \(error)")
}

これをそのまま実行してもよいのですが、デフォルトの LoggerAPI はログを出力しません。そこで、次のような Logger をつくって、 Logger に登録します。

struct StandardOutLogger: Logger {
  func log(_ type: LoggerMessageType, msg: String, functionName: String, lineNum: Int, fileName: String) {
    if isLogging(type) {
      print("\(type) (\(fileName) - \(functionName) -\(lineNum)) - \(msg)")
    }
  }
  func isLogging(_ level: LoggerMessageType) -> Bool {
    switch level {
    case .info, .error, .warning: return true
    default: return false
    }
  }
}

アプリケーションを実行

これでアプリケーションの完成です。次のコマンドでアプリケーションを実行してみます。

swift run ExampleResponseApp

次のようなログが出てきて、ポート 8080 でサーバーが起動していることがわかります。ポートの選択の仕方が Java っぽいです。

INFO (/Users/user/ExampleResponseApp/.build/checkouts/smoke-framework.git-5627467287507921261/Sources/SmokeOperationsHTTP1/SmokeHTTP1Server+startAsOperationServer.swift - startAsOperationServer(withHandlerSelector:andContext:defaultOperationDelegate:andPort:invocationStrategy:) -49) - Server starting on port 8080...
INFO (/Users/user/ExampleResponseApp/.build/checkouts/smoke-framework.git-5627467287507921261/Sources/SmokeOperationsHTTP1/SmokeHTTP1Server+startAsOperationServer.swift - startAsOperationServer(withHandlerSelector:andContext:defaultOperationDelegate:andPort:invocationStrategy:) -53) - Server started on port 8080...

curl で動作確認してみます。このようなレスポンスが返ってくるのではないかと思います。

$ curl -i http://localhost:8080/example -d '{"text":"hello, smoke-framework!"}'
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 40

{
  "text" : "hello, smoke-framework!"
}

比較的簡単に Swift で web アプリケーションを書けるので、普段は iOS を触っている方も、モックの HTTP サーバーを書くのに使ってみてはいかがでしょうか?


smoke-frmaework シリーズ

現在 GitHub の amzn のレポジトリーに smoke を冠したレポジトリーは smoke-framework を含めて 4 つほどあります。

  • smoke-framework
  • smoke-http
    • Swift-NIO ベースの http client
  • smoke-aws
    • smoke-http ベースの aws sdk
  • smoke-aws-credentials
    • ECS で使うことが前提っぽい短時間の aws クレデンシャル
  • smoke-dynamoDB
    • Swift で dynamoDB を扱いやすくするためのライブラリー

これらのレポジトリーが 10 月中旬ころからできており、また smoke-aws のコミットログを見るとモデルクラスの生成を aws-sdk-go から行っているようです。割とすごい速さで充実していってるような印象を持ちました。


というわけで、 Amazon 製の smoke-framework の紹介でした。

明日は @fumiyasac さんが UIについて書いてくれるそうです。






余談話

2018/11/30 に開催された 中央線Meetup 用にデモとして、 Azure で smoke-framework アプリケーションをビルドしてみたのですが、このようなログが出てきました。

2018-11-25T12:30:56.2632921Z ==============================================================================
2018-11-25T12:30:56.2633017Z Task         : Command Line
2018-11-25T12:30:56.2633076Z Description  : Run a command line script using cmd.exe on Windows and bash on macOS and Linux.
2018-11-25T12:30:56.2633167Z Version      : 2.142.2
2018-11-25T12:30:56.2633216Z Author       : Microsoft Corporation
2018-11-25T12:30:56.2633314Z Help         : [More Information](https://go.microsoft.com/fwlink/?LinkID=613735)
2018-11-25T12:30:56.2633377Z ==============================================================================
2018-11-25T12:30:57.1610232Z Generating script.
2018-11-25T12:30:57.1634662Z Script contents:
2018-11-25T12:30:57.1635377Z swift build --configuration release --product ExampleResponseApp
2018-11-25T12:30:57.1666791Z [command]/bin/bash --noprofile --norc /__w/_temp/5c9a12a6-7144-410f-b5ff-96bb137424b9.sh
2018-11-25T12:30:57.5146866Z Fetching https://github.com/apple/swift-nio.git
2018-11-25T12:30:57.5159040Z Fetching https://github.com/IBM-Swift/LoggerAPI.git
2018-11-25T12:30:57.5173837Z Fetching https://github.com/apple/swift-nio-zlib-support.git
2018-11-25T12:30:58.5142300Z Fetching https://github.com/amzn/smoke-framework.git
2018-11-25T12:31:03.6976063Z Completed resolution in 6.19s

おわかりいただけただろうか一つのログの中に

の巨大な4つの会社の名前が出ているのである。

Azure 上の Ubuntu で Swift 4.2.1 を動かす

f:id:mike_neck:20180828214340p:plain

この3連休で試したのでメモ。

Azure で立てた Linux VM(Ubuntu16.04) にて、 swift の docker イメージにある通りの手順で Swift4.2.1 をインストールしたところ、エラーが出てしまって Swift アプリケーションを動かせなかった。

具体的には

  • libdispatch-dev
  • libcurl4-openssl-dev
  • libicu-dev
  • libssl-dev
  • pkg-config

をインストールしてサンプルのアプリケーションに対してどの shared ライブラリーが使われるか確認したところ次のようなエラーが出てきた。

$ ldd ExampleApp 
./ExampleResponseApp: /usr/lib/x86_64-linux-gnu/libcurl.so.4: version `CURL_OPENSSL_3' not found (required by /usr/lib/swift/linux/libFoundation.so)
    linux-vdso.so.1 (0x00007ffe2db2a000)
    libFoundation.so => /usr/lib/swift/linux/libFoundation.so (0x00007fb764794000)
    libswiftCore.so => /usr/lib/swift/linux/libswiftCore.so (0x00007fb764279000)
    libswiftGlibc.so => /usr/lib/swift/linux/libswiftGlibc.so (0x00007fb76508b000)
    libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007fb76405a000)
    libutil.so.1 => /lib/x86_64-linux-gnu/libutil.so.1 (0x00007fb763e57000)
# 以下略

調べてみると、 GitHub にある curl の issue が見つかった。

github.com

問題そのものは Ubuntu だか Debian だか、 Swift の問題っぽい。なので、 curl ではどうにも対処できない問題の様子だが、最後のワークアラウンドを適用すると動かすことができた。


以下、 Swift4.2.1 を Azure の Ubuntu で動かすために必要なスクリプト

#!/usr/bin/env bash

sudo apt-get -q update
sudo apt-get -y install libdispatch-dev libcurl4-openssl-dev libicu-dev libssl-dev pkg-config libgconf-2-4
sudo echo "deb http://security.ubuntu.com/ubuntu xenial-security main" >> /etc/apt/sources.list
sudo apt-get -y install libcurl3

curl -fSsL http://security.ubuntu.com/ubuntu/pool/main/i/icu/libicu55_55.1-7_amd64.deb -o libicu55.deb
sudo dpkg -i libicu55.deb
rm libicu55.deb

export SWIFT_VERSION=swift-4.2.1-RELEASE
export SWIFT_BRANCH=swift-4.2.1-release
export SWIFT_PLATFORM=ubuntu16.04

export SWIFT_URL=https://swift.org/builds/${SWIFT_BRANCH}/$(echo "$SWIFT_PLATFORM" | tr -d .)/${SWIFT_VERSION}/${SWIFT_VERSION}-${SWIFT_PLATFORM}.tar.gz

curl -fSsL ${SWIFT_URL} -o swift.tgz

sudo tar -xzf swift.tgz --directory / --strip-components=1
rm swift.tgz

sudo chmod -R o+r /usr/lib/swift

今読み返したけど、 Azure ほとんど関係なかった