🌊

C++のライブラリを直接使おう(その1)

2020/12/31に公開

はじめに

D言語からC++のライブラリを使ってみよう、のチュートリアルその1です。

D言語からC++を使うなら dpp でいいじゃないという話もありますが、実際手で書いたらどんなもんかということで、基本的なC++の型をD言語から扱う例をご紹介します。

ソースは一式GitHubで公開しているのでそちらも参照ください。

その2は以下になります。
https://zenn.dev/lempiji/articles/ffi-dlang-cpp-part2

C++との連携とは?

多くのプログラミング言語は、C言語で書かれた関数を呼び出すなどの連携機能を持っています。これは「Foreign function interface」、通称FFIなどと呼ばれています。

これと同様に、C++で定義された名前空間、クラス、関数のオーバーロードなどを定義通りそのまま扱おう、という概念がC++連携です。

しかし、C++とのFFI、特に高度な連携機能というのはほとんど見られません。(知る限り、それなりに話が進んでいるのはDとAdaくらい?Rustなどでチャレンジはあるようですが)

この主な理由としては、以下のようなことが考えられます。

  • C++の機能を別の言語で表現することが難しい
    • 特に新しい言語では意図して切り捨てる傾向のある多重継承、関数オーバーロードや例外などをどうするか
  • C言語相当でラッパーを書き、更に操作用の仕組みを整えれば大体事足りる
    • それを前提にC++のライブラリが作られることも多いので敷居が低い
  • C++連携という概念自体が希薄であり要望として少ない/優先度が低い
  • そこまでするならやっぱりC++でいいやとなる

しかしD言語では、作者が元々C++コンパイラの作者であることもあり、C++で作られる膨大な資産を可能な限りそのまま有効活用できるよう考慮され、実際に公式サポートする方針です。
(当然ですが、多重継承はありませんし、属性で表現されるものが扱えないなどの制限も多くあります)

絶賛強化中であったり今は多少ヘッダーに相当するものを手書きする手間がありますが、ラッパーの構造に頭を悩ませたり余計なオーバーヘッドがあったりすることもなく、他の言語では難しいであろうC++連携がかなり自然に書けるのが強みです。

というわけで、以下宣伝半分にその実力や現状を感じてもらえればと思います。

D言語でできること

D言語ではC言語のFFIにおけるのような関数呼び出しを超えて以下のようなことができます。

  1. C++の標準ライブラリにあるクラス等をD言語で構築および操作する(C++から受け取り、C++へ転送も可能なレベルの互換性がある)
  2. C++側で定義した構造体やクラスの継承、仮想関数のオーバーライド
  3. C++側でスローされた例外のキャッチ

大きく2の範囲まではやらないと実用に耐えないので、全2回を予定してカバーしていきたいと思います。(例外周りは未調査のため一旦見送り)

というわけで、今回は1の部分についてです。

チュートリアル内容

既存ライブラリの活用が主目的ですので、1つ簡単なC++のライブラリを作って、それをD言語のプログラムから静的リンクする方式で利用していきます。

環境準備

環境はWindows 10で、C++側は Visual Studio 2019(以下、VS)および Microsoft Visual C++(以下、MSVC)を使います。実際に試される場合、VSはワークロードとして「C++によるデスクトップ開発」を有効にしておいてください。

D言語のコンパイラは DMD でも LDC でも構いません。DUBでプロジェクトを作って両対応でやっていきますので、最低限 dub コマンドが使えればOKです。

当然Windows以外のOSもサポートされますが、標準ライブラリが鋭意実装中だったりするため一部動かないケースがあります。
ソースを見た限り、2020年末のところでは Windows と OSX が比較的安定して動作するようです。

よく使われるオブジェクトの受け渡し編

まずは基礎編として、頻出の std::string, std::vector, std::unique_ptr のオブジェクトを相互に受け渡すサンプルです。
(他にも std::array は利用可ですが、std:shared_ptr あたりはまだ対応されていません)

大きな流れとしては、C++でVSから1つライブラリを作る(ヘッダーと実装を書いてビルド) -> D言語で1つアプリを書く(ヘッダーを翻訳する、アプリの実装を書いて動かす)、という順序です。

C++側のライブラリ準備

まずはC++側から作っていきます。

結果としてできるソリューションは以下の部分になります。

https://github.com/lempiji/example-cpp-d/tree/master/src/cpp/MyLib

プロジェクト準備

Visual Studio 2019を使い、C++向け静的リンク用ライブラリのテンプレートを利用します。
以下のテンプレートになります。

適当なフォルダに新しいプロジェクトを1つ、MyLib という名前で作ります。
また作成できたら一度ビルドして問題ないことを確認しておきます。

以下簡単のため、ビルドモードは Release 、アーキテクチャは x64 としておいてください。
他の設定を使う場合は、D言語側でビルド時に微調整が必要になります。

ヘッダー定義

プロジェクトの「ヘッダー ファイル」というフォルダに mylib.h というファイルを追加します。

あとは適当な名前空間を作って、そこに各種オブジェクトを作って返す関数と受け取って使う関数をそれぞれ定義します。

今回は std::string, std::vector<int>, std::unique_ptr<int> の3種で、構築して返す create_* と受け取って処理する process_* を作ります。

mylib.h
#pragma once

#include <memory>
#include <string>
#include <vector>

namespace mylib {
    std::string create_string(void);
    std::vector<int> create_vector(void);
    std::unique_ptr<int> create_pointer(void);

    void process_string(std::string text);
    void process_vector(std::vector<int> vector);
    void process_pointer(std::unique_ptr<int> pointer);
}

参照や const に関してはいろいろな話があるため、今回は簡単のためすべて省いておきます。

実装

次はプロジェクトの「ソース ファイル」というフォルダに実装用の mylib.cpp ファイルを追加します。

作る方は適当にコンストラクタを呼んで返し、使う方は標準出力に出すだけにしておきます。
ここはあまり重要ではないので抜粋でサクッと書くにとどめます。

mylib.cpp
#include "mylib.h"

#include <iostream>

std::string mylib::create_string(void) {
    return std::string("MyLib String");
}

std::vector<int> mylib::create_vector(void) {
    return {10, 20, 30};
}

std::unique_ptr<int> mylib::create_pointer(void) {
    return std::make_unique<int>(10);
}

void mylib::process_string(std::string text) {
    std::cout << text << std::endl;
}

void mylib::process_vector(std::vector<int> array) {
    for (auto v : array) {
        std::cout << v << std::endl;
    }
}

void mylib::process_pointer(std::unique_ptr<int> pointer) {
    std::cout << *pointer << std::endl;
}

ビルド

VSの機能で通常通りビルドします。

結果、ソリューションファイルと同じフォルダに x64 フォルダができて、その下の Release フォルダ内に MyLib.lib というファイルが生成されていれば成功です。

この MyLib.lib ファイルはあとでコピーして使います。

利用側アプリ

続いてD言語側です。

こちらの最終的な構成は以下のURLのようになります。

https://github.com/lempiji/example-cpp-d/tree/master/src/d

プロジェクト準備

今回は普通に DUB のプロジェクトを初期化して使っていきます。

まずはコマンドプロンプトを開き、適当なフォルダで以下のコマンドを実行してプロジェクトを初期化します。

dub init example-cpp-d -f sdl

dub.sdl でビルド設定を行う

できたプロジェクトのビルド設定として、 dub.sdl に以下を書き足します。

  1. 静的リンクするLIBファイルの指示
  2. C++ランタイムのリンク指示
  3. リンク時コード生成の有効化(リンク時警告の抑制)

順番に説明しますが、ざっと結果だけ書いてしまうと以下の内容となります。

// 省略

libs "Mylib"
//dflags "-mscrtlib=libcmt" platform="dmd"  // ランタイムライブラリが「マルチスレッド(/MT)」かつDMDの場合
//dflags "--mscrtlib=libcmt" platform="ldc" // ランタイムライブラリが「マルチスレッド(/MT)」かつLDCの場合(無くても動作する)
dflags "-mscrtlib=msvcrt" platform="dmd"  // ランタイムライブラリが「マルチスレッド DLL(/MD)」かつDMDの場合
dflags "--mscrtlib=msvcrt" platform="ldc" // ランタイムライブラリが「マルチスレッド DLL(/MD)」かつLDCの場合
lflags "/LTCG"

1. リンクするライブラリの設定

以下の行で、作ったC++の lib ファイルをリンクする設定をします。

libs "Mylib"

これがないとリンク時に「シンボルが未解決」といったエラーが出ます。

また実際に lib ファイルを読み取って使う必要があるため、パスが通った場所に配置する等の必要があります。
ここでは簡単のため、C++側のビルド成果物である MyLib.libdub.sdl と同じフォルダにコピーしておきます。

2. ランタイムライブラリの設定

D言語側でビルドする際、MSVCで指定したランタイムライブラリに応じて変える設定が1つあります。

まず Visual Studio 側で設定を確認するため、 プロジェクトのプロパティ -> C/C++ -> コード生成 の項目を開きます。
以下の画像にある ランタイムライブラリ という箇所です。

これは通常のテンプレートであれば「マルチスレッド DLL」のほうが選択されていますが、それぞれ設定に合わせて dub.sdl ファイルに以下の設定を書き足せば良いです。

マルチスレッド
dflags "-mscrtlib=libcmt" platform="dmd"
dflags "--mscrtlib=libcmt" platform="ldc"
マルチスレッドデバッグ
dflags "-mscrtlib=libcmtd" platform="dmd"
dflags "--mscrtlib=libcmtd" platform="ldc"
マルチスレッドDLL
dflags "-mscrtlib=msvcrt" platform="dmd"
dflags "--mscrtlib=msvcrt" platform="ldc"
マルチスレッドデバッグDLL
dflags "-mscrtlib=msvcrtd" platform="dmd"
dflags "--mscrtlib=msvcrtd" platform="ldc"

今回はVSのテンプレートに従い3番目の「マルチスレッドDLL」の分を書き足しておくということです。

なおこれをやっておかないと、ビルドしたときに以下のようなエラーが発生します。

Mylib.lib(MyLib.obj) : error LNK2038: 'RuntimeLibrary' の不一致が検出されました。値 'MD_DynamicRelease' が MT_StaticRelease の値 'example-cpp-d.obj' と一致しません。
LINK : warning LNK4098: defaultlib 'MSVCRT' は他のライブラリの使用と競合しています。/NODEFAULTLIB:library を使用してください。
.dub\build\application-debug-windows-x86_64-dmd_2094-81B81716D3777BCAB9C413255CB007A0\example-cpp-d.exe : fatal error LNK1319: 1 の不一致が検出されました

意図せずこのエラーに遭遇したら、 RuntimeLibrary, MD_DynamicRelease, MT_StaticRelease, defailtlib, MSVCRT といったあたりのキーワードから頑張ってあたりをつけることになります。私はなりました。
ちなみに /NODEFAULTLIB:library を付けても解決しないので注意です。なんでやねん。

このLink Time Code Generation機能は、パフォーマンス向上のためにリンク時のコード生成を許可するというフラグです。
細かい説明は以下URLにあります。

https://docs.microsoft.com/ja-jp/cpp/build/reference/ltcg-link-time-code-generation?view=msvc-160

これはプロジェクトのプロパティから C/C++ -> 最適化 -> プログラム全体の最適化 という項目が有効になっていると必要なフラグです。
以下の設定になります。

こちら、VSのビルドモードが Release になっている場合は規定で有効になっています。裏を返せば Debug モードでは不要な設定です。

また、ここで /LTCG を書き足さない場合は以下の警告が表示されます。

Mylib.lib(MyLib.obj) : MSIL .netmodule または /GL を伴ってコンパイルされたモジュールが見つかりました。/LTCG を使用して再開始してください。リンカーのパフォーマンスを向上さ 
せるためには、コマンドラインに /LTCG を追加してください。

こちらは素直にメッセージに従い、 lflags/LTCG とつけてやれば解決します。メッセージを信じるも信じないも状況次第です。

C++ヘッダーの変換

続いて、C++のヘッダーファイルに対してD言語の定義を作っていきます。この変換作業が要所です。

書き換えパターンの細かい部分は公式サイトの以下のページに記述されていますので、不明な点があれば参照してください。

定義

まず作った dub のプロジェクトに移り、 source フォルダの直下に mylib.d というファイルを作って、ざっと以下の内容を書いていきます。

ファイル名とモジュール名(module の宣言)は元のヘッダーファイル名と合わせておくのが無難で、以下順番に解説していきます。

mylib.d
module mylib;

import core.stdcpp.memory;
import core.stdcpp.string;
import core.stdcpp.vector;

alias cppstring = basic_string!char;

extern (C++, mylib)
{
@nogc:
    cppstring create_string();
    vector!int create_vector();
    unique_ptr!int create_pointer();

    void process_string(cppstring text);
    void process_vector(vector!int array);
    void process_pointer(unique_ptr!int pointer);
}

標準ライブラリのimport

C++では #include <memory> などと書いていたものを、D言語では import 文に置き換えてやります。
以下の部分です。

import core.stdcpp.memory;
import core.stdcpp.string;
import core.stdcpp.vector;

ヘッダーと比較すると機械的に置き換えていることが分かると思いますが、 core.stdcpp というモジュール階層がとても重要です。
この下に使えるモジュールが一通り格納される構造になっています。

名前はC++側と同じにすることとなっているため、 memory であれば core.stdcpp.memory にするといった具合です。わかりやすいですね。

参考情報

これらのモジュールはD言語のランタイムとして提供されていますが、その中身はC++の各種コンパイラが提供する標準ライブラリと同じものをD言語で再実装したものです。

これ結構すごいことだと思われるのですが、MSVC、GCC、Clangでそれぞれ実装もあれこれ異なるところ、D言語では条件コンパイルの機能を使い、その差異を吸収する形で1つのモジュールに収めきっています。

たとえば std::unique_ptr の実装では、MSVC/Clangだと Compressed_Pair という特殊なイディオムを使った実装になっているところ、GCC用には Tuple を使った実装になっていたりします。
しかしこれを適切に切り替えないと、コンパイラ違いで異なる位置のメンバーにアクセスしてしまうなど、結局正しく動作しないことになります。

まだまだ鋭意実装中のモジュールも多いですが、それもこれもC++の資産を活用するためには必要な努力であり、それもD言語なら実現可能というわけです。

複数の方が取り組まれている壮大なテーマですが、大変素晴らしい働きであり感謝しつつこれからの動向にも期待したいと思います。

string の alias

続いて以下の部分です。ここでは std::string に相当するものを定義しています。

alias cppstring = basic_string!char;

なぜこんなことをするかというと、C++では std::string というクラスが提供されますが、利用している core.stdcpp.string では std::string が直接提供されないためです。

理由は定かではありませんが、D言語にも既定の文字列が string というキーワードで提供されているので衝突を嫌ったものかと思われます。(モジュール名には使われていますが…)

というわけで代わりに元の std::string と同様、 basic_string テンプレートを特殊化することでC++標準の std::string と同じものを定義しています。
これはD言語の文字列として char で特殊化してやれば良く、上記のような記述になります。

また、一応C++の basic_string らしく char_traits といった比較的マイナー機能?もサポートしているので、C++と同等の特殊化が可能になっていることも覚えておくと役に立つことがあるかと思います。

リンケージの指定、名前空間の記述

extern (C++) というブロックがあり、ここでC++の定義であることの表明と名前空間の指定を行っています。

以下の部分です。

extern (C++, mylib)
{
    // 省略
}

ブロック記法を使うとほぼ namespace と同じです。
また namespace が複数階層ある場合は extern (C++, mylib.detail) といった形でドット区切りになります。

その他、extern(C++): という「以降すべて」といった意味の記述もできるので、このあたりはお好みに合わせて使うと良いかと思います。

関数定義

あとは普通に関数を定義していきます。

@nogc:
    cppstring create_string();
    vector!int create_vector();
    unique_ptr!int create_pointer();

    void process_string(cppstring text);
    void process_vector(vector!int array);
    void process_pointer(unique_ptr!int pointer);

変換作業自体は、 vector!intunique_ptr!int といったD言語的なテンプレートの特殊化になっている、引数がないことを示す void を削る、というくらいです。

また @nogc を付けていますが、こちらはGCが動作しないのは明白なので付けています。
実際は付けても付けなくてもマングリングの解決などに影響しないため動作します。

あとはC++とは違ってクラスなどの名前に std:: が付かないため、 vectorunique_ptr と書く分少し見た目がすっきりします。
C++ばかり書いてるとどこかで using namespace std; をしているようで違和感が強かったですが、一旦気にしないことにして先に進みます。

メイン処理

ヘッダーさえ定義できてしまえば、あとはいつものD言語です。
というわけで、dub コマンドによって既に作られている app.d ファイルを編集して実際に使っていきます。

最初に変換した mylibimport して、create_* を試す run_createprocess_* を試す run_process をそれぞれ実装します。

app.d
import mylib;

void main()
{
    run_create();
    run_process();
}

void run_create()
{
    import std.stdio;

    auto text = create_string();
    auto array = create_vector();
    auto pointer = create_pointer();

    writeln(text);
    writeln(array[]);
    writeln(*pointer.get());
}

void run_process()
{
    import core.lifetime;
    import core.stdcpp.memory;
    import core.stdcpp.vector;

    auto text = cppstring("Hello, C++!");
    auto array = vector!int([10, 20, 30]);
    auto pointer = make_unique!int(100);

    process_string(text);
    process_vector(array);
    process_pointer(move(pointer));
}

関数呼び出し

create_stringprocess_vector など、普通に呼び出せば利用できます。
このあたりは特に言うことがありません。。

vectorの利用

vector をそのまま writeln に渡すと構造体の既定の処理でメンバーが出力されます。結果、文字列っぽい見た目になりません。

これを文字列らしくするには全体スライスを取れば良いです。要素毎の配列になるので、結果としてD言語の文字列になります。

unique_ptrのアクセス

C++の間接参照等に関する演算子オーバーロードが未実装のため、基本的にリソースを保持する用途でのみ使うのが吉です。
T opUnary(string op: "*")opDispatch が実装されればいけるはず?)

今のところ、どうしても値を利用したい場合は .get() で一度生ポインタを取り出し、それに間接参照をかける必要があり、結果以下のような記述になります。

writeln(*pointer.get());

ちなみにDでは間接参照でもドットアクセスなので、何かクラス等を保持していても ptr.get().hoge() といったメソッドチェーンのように書くことができます。

C++クラスの構築

D言語側でC++の構造体やクラスも普通に構築することができます。
以下のあたりです。

auto text = cppstring("Hello, C++!");
auto array = vector!int([10, 20, 30]);
auto pointer = make_unique!int(100);

cppstringvector は特に奇妙に見えるかもしれませんが、それぞれD言語に合わせて動的配列を受け取るコンストラクタが存在している、というのが1つポイントです。
これはオブジェクトの実体およびC++との受け渡しに影響しないので、言ってしまえばD言語側の定義で好き放題実装しているためです。(vtableどうなってるのか気になりますが)

たとえば string に関して言えば、以下のドキュメントに使えるコンストラクタが記載されています。

https://dlang.org/phobos/core_stdcpp_string.html#.basic_string.this.3

当然ですが、vector であれば push_back などのメソッドも提供されます。

unique_ptr に対する move

unique_ptr はC++と同じで、所有権の移動に際して以下のように move が必要です。

process_pointer(move(pointer));

これはD言語における unique_ptr の定義で、 @disable this(this) の記述によりコピーが無効化されているためです。
ほかにも代入などの演算子オーバーロードが実装されており、C++とほぼ同じように使うことができるよう配慮されています。

C++との連携となると、単に関数が呼び出せるだけでなくこういった機能のカバーもポイントになってきますね。

ちなみに move の定義は core.lifetime というモジュールにあるものを使えばOKです。

実行

さて、ここまで設定や実装が済んでしまえば、あとは dub コマンドで実行するだけです。

dub run

結果は以下のような出力になります。無事にC++と相互に連携することができました!

MyLib String
[10, 20, 30]
10
Hello, C++!
10
20
30
10

補足

コンパイラの連携パターン制限について

2020年末時点では、DとC++の連携では環境ごとに対応するC++のコンパイラが制限されています。

具体的には、Windows環境ではMSVC、Linux環境ではGCC、OSX環境ではClang、といった具合です。

たとえばWindows環境でClangを使ってビルドしたものをLDCで使おう、みたいなことをするためにはLDCに手を入れる必要があるので注意が必要です。

ちなみに実装は主にこのあたりのフラグ切り替えの話になってきますので、気になる方は見てみてください。

おわりに

毎回リリースノートの日本語訳でまとめを書いていて、C++連携が強化されました!と言っている割にあまり露出のない機能だなと思われるのでひとつ記事にしてみました。

extern(C++) のあたりさえ押さえてしまえば、D言語とC++の連携について一通りの流れは掴めるかと思います。

とはいえ今回は非常に簡単な範囲なので、近いうちに構造体やクラス、仮想関数などを使った実践的な部分でその2を書きたいと思います。

というわけで書きました!(2年越し)

https://zenn.dev/lempiji/articles/ffi-dlang-cpp-part2

Discussion