概要
RustでHTTPクライアントライブラリを作成して、それをXcodeからFFIで呼び出す方法です。
JSONPlaceholderのposts/1からJSONデータを受け取って構造体にしてコールバックを返す関数を作ります。
reqwestクレートを使用して実装します。
私が学習のために実装したの内容ですので、間違いが含まれているかもしれません。
方法
プロジェクト作成
ライブラリとしてプロジェクトを作成します。
$ cargo new httpclient --lib
静的ライブラリとして作成するよう設定します。
[lib]
crate-type = ["staticlib"]
実装
HTTP通信処理
依存追加
reqwestを使用してHTTPクライアントを実装します。
Cargo.tomlのdependenciesにreqwestとserde追加します。
serdeは、受け取ったJSONデータを構造体にデシリアライズするのに使います。
[dependencies]
reqwest = { version = "0.11", features = ["json", "blocking"] }
serde = { version = "1.0", features = ["derive"] }
HTTP通信処理追加
受け取ったJSONデータを受け取る構造体と、reqwestを使用したHTTP通信処理を追加します。
use serde::Deserialize;
#[derive(Debug, Deserialize)]
struct PostInternal {
id: u32,
#[serde(rename = "userId")]
user_id: u32,
title: String,
body: String,
}
fn get_request_impl() -> Result<PostInternal, Box<dyn std::error::Error>>{
let post = reqwest::blocking::get("https://jsonplaceholder.typicode.com/posts/1")?
.json::<PostInternal>()?;
Ok(post)
}
serde(rename = "userId")
属性は、JSONデータのキー名と構造体のメンバ名が違う時に対応づけるために追加します。
Rustは構造体のメンバ名をsnake_caseにするよう推奨しているので、このようにしています。
FFI
データ構造体追加
C++側で使用する構造体を追加します。
use std::ffi::{c_char, c_uint, CString};
#[repr(C)]
pub struct Post {
id: c_uint,
user_id: c_uint,
title: *const c_char,
body: *const c_char,
}
コールバックインタフェース追加
通信完了時にコールバックとして呼び出すインタフェースを追加します。
type RequestCallback = extern "C" fn(bool, *const Post);
FFI用関数追加
先ほど追加したget_request_impl
関数を呼び出して、C++向けの構造体に変換してコールバックを呼び出す関数を追加します。
#[no_mangle]
pub extern fn get_request(callback: RequestCallback) {
thread::spawn(move || {
let post_internal = get_request_impl();
match post_internal {
Ok(p) => {
let title = CString::new(p.title).unwrap();
let body = CString::new(p.body).unwrap();
let post = Post{
id: p.id,
user_id: p.user_id,
title: title.as_ptr(),
body: body.as_ptr(),
};
callback(true, &post as *const Post);
}
Err(e) => {
println!("get_request error:{}", e);
callback(false, std::ptr::null_mut());
}
}
});
}
xcframework作成
cbindgen
を使用してのヘッダーファイル作成と、ios向け.aファイルを作成するスクリプトを追加します。
unstable-options
を追加しているのは--out-dir
オプションを使うためです。
ビルドターゲットにaarch64-apple-ios
などがない場合は、rustup
でツールチェインを追加します。
rm -rfv output/
# create header
cbindgen --crate httpclient --output output/include/httpclient.h
# create .a files
cargo build --release --target aarch64-apple-ios --out-dir output/aarch64-apple-ios -Z unstable-options
cargo build --release --target aarch64-apple-ios-sim --out-dir output/aarch64-apple-ios-sim -Z unstable-options
# create xcframework
xcodebuild -create-xcframework \
-library output/aarch64-apple-ios/libhttpclient.a -headers output/include \
-library output/aarch64-apple-ios-sim/libhttpclient.a -headers output/include \
-output output/httpclient.xcframework
上記のスクリプトを実行すると、outputディレクトリ以下にhttpclient.xcframeworkが出力されます。
Xcodeから呼び出し
出力されたxcframeworkを追加して呼び出します。
作成したライブラリで提供しているget_request
関数を呼び出して、コールバックで返ってきた情報を表示しています。
#import <httpclient.h>
- (IBAction)tapButton:(id)sender {
get_request( [](bool is_success, const Post* post){
NSLog(@"request success: %d", is_success ? 1 : 0);
NSLog(@"title: %s", post->title);
} );
}
まとめ
以上でRustで作成したiOS向けライブラリをXcodeから呼び出せたと思います。
FFIはメモリ関連で特に注意が必要です。例えばPost構造体はcallbackが呼ばれた後に解放されます。なので、callback外でも参照したい場合はコピーする必要があります。
最初にも書いた通り、上記例には間違いが含まれているかもしれません。
この記事が何かのとっかかりになったらいいなと思います。
参考