C++のライブラリをC言語でくるんでRustから使うまで

この記事はAmusementCreatorsAdventCalendar(通称ACAC)の20日目の記事です.

www.adventar.org

にゃん

以前下のような記事を書いたのですが,その時に色々学ぶことがあったので纏めておこうと思います. akitsu-sanae.hatenablog.com

この記事は大まかに分けてC++のコードをCでくるむ部分とそれをRustから使う部分の2つに分かれます.
前半はRustに興味のない人にも役に立つのではと思います.

C++の環境はVisual Studio 2015(v140)
Rustの環境は

  • rustup 0.6.5 (88ef618 2016-11-04)
  • rustc 1.15.0-nightly (2217bd771 2016-11-25) nightly-i686-pc-windows-msvc
  • cargo 0.16.0-nightly (b26d672 2016-11-25)

です.

説明のためライブラリのヘッダファイルはnyan.hpp,ライブラリのバイナリはnyan.libとします.

何か不備などありましたら遠慮なくコメントください.

Visual C++のコードをC言語でくるむ

取り敢えずラップする用のVisual C++プロジェクトを作ります.
ラッパーのcppファイルをwrapper.cppとでもしておきます.
wrapper.cppは基本的に下の様になります.

#include "nyan.hpp"

extern "C" {
    // ここにラップするコードを書いていく
}

フリー関数についてはそのままラップすればよくて特筆すべきことはありません.

例えば下のようなクラスをラップすることを考えます

namespace nyan {

struct Inu {
    explicit Inu() { ... }
    ~Inu() { ... }

    void greet(const char* name) {
        std::cout << "こんにちは!" << name << "さん!wanwan!!" << std::endl;
    }

    ...
};

}

この場合ラップする関数は

  • nyan::Inu::Inu()
  • nyan::Inu::~Inu()
  • nyan::Inu::greet(const char*)

の三つです.
wrapper.cppは下の様になります.

#include "nyan.hpp"

extern "C" {
    typedef struct {
        Inu impl;
    } InuImpl;

    // nyan::Inu::Inu
    InuImpl* nyan_Inu_create() {
        return new InuImpl{};
    }

    // nyan::Inu::~Inu
    void nyan_Inu_delete(InuImpl* inu) {
        delete inu;
    }

    // nyan::Inu::greet
    void nyan_Inu_greet(InuImpl* inu, const char* name) {
        inu->greet(name);
    }
}

まぁつまりC++のクラスであるInuをCのstruct InuImplでラップして,それを経由してコンストラクタやデストラクタ,メンバ関数をラップします.
Inuはほとんどの場合std::shared_ptr<Inu>の形で使われるようならInuImplが持つのはstd::shared_ptr<Inu>でもよいかもしれません.
見てもらえば分かるように,コンストラクタは単純にInuImpl*を返すフリー関数として,デストラクタはInuImpl*を引数に取ってvoidを返すフリー関数としてラップできます.

メンバ関数の扱いなのですが,これはそのクラスのポインタが第一引数になるようなフリー関数としてラップできます.このポインタはC++メンバ関数では暗黙的に渡されているthisポインタのことですね.

こんな感じでバリバリラッパーを書いていきます.

で,当然ですがスタティックリンクライブラリとしてビルドします.
この時,ビルド後イベントにlib /out:lib/nyan-wrapper.lib nyan.lib Release/(プロジェクトの名前).libを付け加えるとwrapper.cppをビルドしたものとnyan.libをくっつけたものをlib/nyan-wrapper.libに吐き出してくれます.
libはLinux環境でのarみたいなものだと思ってくれればよいです.

これでnyan.libをラップしてC言語のインターフェースにしたnyan-wrapper.libが得られました.

C言語インターフェースの静的ライブラリをRustから使う

まずcargo new nyan-rs --binなどとしてRustのプロジェクトをつくります.
取り敢えずsys部分を作っていきます.sysでは静的ライブラリの中にどんな関数があるのかを列挙します.
今回だとsys部分であるnyan-rs/src/sys.rsは下のようになります.

use std::os::raw::c_char;
use std::os::raw::c_int;

pub enum InuImpl {}

#[link(name = "nyan-wrapper")]
extern {
    pub fn nyan_Inu_create() -> *mut InuImpl;
    pub fn nyan_Inu_delete(inu: *mut InuImpl);
    pub fn nyan_Inu_greet(inu: *mut InuImpl, name: *const c_char);
}

今回はstd::os::rawc_charc_intがあったのでそっちを使いましたがlibcを利用した方が良いかもです. InuImplは4行目のようにダミーのenumで宣言してやります.

他にも例えばuser32.libをリンクしたければ

#[link(name = "user32")]
extern {}

などと付け加えます.

これでsys部分は出来ました.
次はsys部分をRustらしくラップしていきます.

src/nyan.rsにこう書きましょう

use std::ffi::CString;

pub struct Inu {
    raw: *mut sys::InuImpl
}

impl Inu {
    pub fn new() -> Self {
        unsafe {
            Inu {
                raw: sys::nyan_Inu_create()
            }
        }
    }

    pub fn greet(&mut self, name: &str) {
        unsafe {
            sys::nyan_Inu_greet(self.raw, CString::new(name).unwrap())
        }
    }
}

impl Drop for Inu {
    fn drop(&mut self) {
        unsafe {
            sys::nyan_Inu_delete(self.raw)
        }
    }
}

これで次からRustのコードでnyan::InuC++nyan::Inuと同じ気持ちで使えるようになります.
やったね!!

参考にしたもの

Foreign Function Interface

Rust と C言語 をコールバックで行き来する(Cブリッジが必要なVer) | d.sunnyone.org