Rust + FFIでiOS向けHTTPクライアントライブラリを作成する

# 概要
RustでHTTPクライアントライブラリを作成して、それをXcodeからFFIで呼び出す方法です。
[JSONPlaceholderのposts/1](https://jsonplaceholder.typicode.com/posts/1)からJSONデータを受け取って構造体にしてコールバックを返す関数を作ります。
[reqwest](https://docs.rs/reqwest/latest/reqwest/index.html)クレートを使用して実装します。
私が学習のために実装したの内容ですので、間違いが含まれているかもしれません。

# 方法

## プロジェクト作成
ライブラリとしてプロジェクトを作成します。
```shell-session
$ cargo new httpclient --lib
```
静的ライブラリとして作成するよう設定します。
```toml:Cargo.toml
[lib]
crate-type = ["staticlib"]
```

## 実装

### HTTP通信処理

#### 依存追加
reqwestを使用してHTTPクライアントを実装します。
Cargo.tomlのdependenciesにreqwestとserde追加します。
serdeは、受け取ったJSONデータを構造体にデシリアライズするのに使います。
```toml:Cargo.toml
[dependencies]
reqwest = { version = "0.11", features = ["json", "blocking"] }
serde = { version = "1.0", features = ["derive"] }
```
#### HTTP通信処理追加
受け取ったJSONデータを受け取る構造体と、reqwestを使用したHTTP通信処理を追加します。
```rs:lib.rs
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++側で使用する構造体を追加します。
```rs:lib.rs
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,
}
```

#### コールバックインタフェース追加
通信完了時にコールバックとして呼び出すインタフェースを追加します。
```rs:lib.rs
type RequestCallback = extern "C" fn(bool, *const Post);
```

#### FFI用関数追加
先ほど追加した`get_request_impl`関数を呼び出して、C++向けの構造体に変換してコールバックを呼び出す関数を追加します。
```rs:lib.rs
#[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`オプションを使うためです。

:::note info
ビルドターゲットに`aarch64-apple-ios`などがない場合は、`rustup`でツールチェインを追加します。
:::

```shell:build.sh
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`関数を呼び出して、コールバックで返ってきた情報を表示しています。
```obj-c++:ViewController.mm
#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外でも参照したい場合はコピーする必要があります。

最初にも書いた通り、上記例には間違いが含まれているかもしれません。
この記事が何かのとっかかりになったらいいなと思います。

# 参考
https://qiita.com/0yoyoyo/items/578c4ce72d01cac4f226

https://rustinpractice.org/rust-reqwest

https://blog.a-1.dev/post/2019-02-05-jsonplacehoder/

https://zenn.dev/eduidl/articles/f2fd959f220393

https://qiita.com/James0124/items/eae0c30d322e4864b319