KOTET'S PERSONAL BLOG

#dlang DとCのインターフェース:入門編【翻訳】

Created: , Last modified:
#dlang #tech #translation #d_and_c #d_blog #advent_calendar #cpplang

これは1年以上前の記事です

ここに書かれている情報、見解は現在のものとは異なっている場合があります。

この記事はD言語 Advent Calendar 2017 10日目の記事であり、 Interfacing D with C: Getting Started – The D Blog を自分用に翻訳したものを 許可を得て 公開するものである。

ソース中にコメントの形で原文を残している。 内容が理解できる程度のざっくりした翻訳であり誤字や誤訳などが多いので、気になったら Pull requestだ!


D言語の短期的な設計目標の一つにCとのインターフェース能力があります。 その目標のために、Cの標準ライブラリへのアクセスを可能にし、CやC++コンパイラが使うのと同じオブジェクトファイルフォーマットとシステムリンカを使うABI互換を提供しています。 ほとんどのDの組み込み型、構造体までもがCとの直接の互換をもち、適切なリンケージアトリビュートとともにD内で宣言されたCの関数に自由に渡すことができます。 大抵の場合、Cのコードをコピーし、それをDのモジュールにペーストして、最低限の修正をすればコンパイルできます。 反対に、適切に宣言されたDの関数はCから呼び出すことができます。

これはDがCの欠点までそのまま持っているというわけではありません。 DにはCでよく発生するエラーをなくす、あるいは簡単に回避するための機能があります。 たとえば、配列の境界チェックはデフォルトで有効であり、言語のsafeサブセットはメモリ安全をコンパイル時に強制します。 また、DはWalter BrightがCの最大の失敗と考えるCの悪いところを変更または回避しています。 ポインタと配列の混同です。 ここには知識のない人が驚かされる実装の違いが潜んでいます。

これは知識のない人に伝えるためにCとDのインタラクションを探求するシリーズ最初の投稿です。 以前私はこのトピックの基本的な部分に関してGameDev.netで記事を書いており、また詳細については私の本‘Learning D’のチャプター9全体で触れています。

このブログシリーズでは前述のコーナーケースに焦点を当てていくので、それを学ぶために本を買ったり試行錯誤したりする必要はありません。 したがって、DとC(またはC++)とのインターフェーシングをする人は私がGameDev.netの記事に残した基礎と公式ドキュメントを一緒に読むことをお勧めします。

ある振る舞いをハイライトするために私が提供するCやDのコードは読者によってコンパイルし、リンクされることを意図しています。 コードは失敗や成功を実演します。 コンパイラのエラーを知り、理解することはそれを直す事と同じくらい重要であり、実際にそれらを見ることは直すという目標のために役立ちます。 それはCとDのソースファイルのコンパイルとリンクの必須知識を伴います。 幸いなことに、それはこの投稿の次のセクションで見ていくものです。

Cのコードには、WindowsではDigital Mars C/C++コンパイラとMicrosoft C/C++コンパイラを、それ以外ではGCCとClangを使います。 Dの方は、もっぱらDのリファレンスコンパイラ、DMDを使います。 Microsoftのツールを使ったDMDのセットアップに慣れていないWindowsユーザーは、こちらの‘DMD, Windows, and C’という記事が役に立ちます。

この投稿ではコーナーケースのひとつ、DとCのインターフェースを始めてすぐ、特に既存のCのライブラリのバインディングを作るときに現れるようなものを取り扱います。

コンパイルとリンク

この記事では、Dのプログラムとリンクされるために保存されオブジェクトファイルへとコンパイルされることを意図したCのソースコードの例を示します。 オブジェクトファイルを生成するコマンドラインは、どのプラットフォームでも同じような複数の手順からなります。 まずはWindowsのやり方を見て、その後他のシステムは1つのセクションにまとめようと思います。

次の2セクションでは、以下のCとDのソースファイルを使います。 これらを同じディレクトリに(便宜上です)そのままの名前で保存してください。 2つのファイルが同じディレクトリ内で同じ名前である場合、CコンパイラやDMDで作られるオブジェクトファイルも同じ名前になり、古いオブジェクトファイルが新しいオブジェクトファイルで上書きされてしまいます。 これを避けるためのコンパイラスイッチもありますが、チュートリアルのためコマンドラインはシンプルにしておきましょう。

chello.c

#include <stdio.h>
void say_hello(void) 
{
    puts("Hello from C!");
}

hello.d

extern(C) void say_hello();
void main() 
{
    say_hello();
}

Dのコード内にあるCの関数の宣言のextern(C)という部分はリンケージ属性です。 これは上記以外のものにも関わってきますが、その潜在的なハマりどころはこのシリーズの別の機会に見ていきます。

Windows

Zipアーカイブとインストーラとしてdlang.orgで入手できる公式DMDパッケージは、Dのファイルをコンパイルする前提として他のツールをインストールする必要のないDMDのリリースバージョンです。 これらのパッケージはOMFフォーマットで32ビット実行ファイルをコンパイルするために必要な全てが含まれた状態で公開されています(詳細は‘DMD, Windows, and C’ で触れています)。

外部のオブジェクトファイルとDのプログラムをリンクする時、そのオブジェクトファイルフォーマットとアーキテクチャがDコンパイラの出力と一致していることが重要です。 前者は主にWindowsの問題ですが、後者についてはすべてのプラットフォームで注意する必要があります。

CのソースをWindowsのヴァニラDMD互換のフォーマットでコンパイルするにはthe Digital Mars C/C++ compilerが必要です。 DMDと同じようなツール群とともに公開されており、自由にダウンロードすることができます。 これはOMFフォーマットのオブジェクトファイルを出力します。 これとDMDの両方をインストール、システムパスに配置すれば、上記のソースファイルはこのようにコンパイル、リンク、実行ができます:

dmc -c chello.c
dmd hello.d chello.obj
hello

-cオプションはDMCにリンクをさせないようにするもので、その結果Cのソースはコンパイルのみが行われオブジェクトファイルchello.objが出力されます。

Windowsで64ビットの出力を得るときには、DMCは使いません。 この場合、DMDはWindowsのMicrosoft build toolsを要求します。 MS build toolsをインストールしたら、設定済みx64ネイティブツールコマンドプロンプトを開き、以下のコマンドを実行します(Microsoft build toolsの入手と設定済コマンドプロンプトの開き方についてはこのブログの‘D, Windows, and C’を見てください。依存するVisual StudioやMS build toolsのバージョンがちょっと違うかもしれません):

cl /c chello.c
dmd -m64 hello.d chello.obj
hello

/cオプションはやはりコンパイラにリンクをさせないためのものです。 MSコンパイラで32ビットの出力を生成するためには、設定済x86ネイティブツールコマンドプロンプトを開きこれらのコマンドを実行してください:

cl /c hello.c
dmd -m32mscoff hello.c chello.obj
hello

WindowsにおいてDMDは-m32スイッチを認識しますが、それは(デフォルトでは)Microsoftのリンカと互換性のない32ビットOMFの出力を生成するので、かわりに-m32mscoffを使う必要があります。

その他のプラットフォーム

Dをサポートしている他のプラットフォームでは、GCCやClangのようなシステムCコンパイラはdmdが動作するなら既にインストールされています。 Mac OSでは、App StoreのXCodeclangがインストールできます。 ほとんどのLinuxやBSDでは、例えばDebianやDebianベースのシステムにおいて推奨されるコマンドラインapt-get install build-essentialのようにGCCパッケージが利用できます。 詳しくはあなたのシステムのドキュメントを見てください。

これらのシステムでは、たいてい環境変数CCにシステムコンパイラのコマンドがセットされています。 あなたのシステムに合わせて以下のCCgccclangのどちらかに置き換えてください。

CC -c chello.c
dmd hello.d chello.o
./hello

これはあなたのシステムの設定に合わせて32ビットまたは64ビットの出力を生成します。 あなたが64ビットのシステムに32ビットのデベロッパーツールをインストールしている場合、-m32CCdmdの両方に渡すことで32ビットバイナリを生成できます。

longな道のり

CとDのソースを同じバイナリにコンパイルしリンクできるようになったので、さらに一般的な落とし穴を見ていきましょう。 これを正しく理解することは、Windowsとその他のプラットフォーム両方で役に立ちます。

Dの特徴の1つとして、すべての整数型のサイズは固定されています。 shortは常に2バイトであり、intは4バイトです。 これはシステムのアーキテクチャにかかわらず決して変わりません。 これは仕様が各整数型のサイズを相対的にしか指定せず、実装に任せているCとは異なります。 それでもモダンなコンパイラの間では幅広い合意があり、現在Dがサポートするすべてのプラットフォームにおけるほとんどの整数型のサイズはDのそれと一致します。 例外はlongulongです。

Dにおいて、longulongは常に8バイトです。 version(Posix)傘下にあるほとんどの64ビットシステムでは足並みが揃っており、longunsigned longも8バイトです。 しかし32ビットアーキテクチャにおいては4バイトです。 さらに、Windowsにおいては64ビットアーキテクチャであっても常に4バイトです。

今日のほとんどのCコードはこれらの違いを考慮しプリプロセッサを使うことによってカスタム整数型を定義するか、誤解の余地なく定義されるint32_tint64_tのような型のあるC99 stdint.hを使うようにしています。 しかしいまだにlongをそのまま使っているCライブラリに遭遇することがあります。

以下のようなCの関数を考えてみましょう:

maxval.c

#include <limits.h>
unsigned long max_val(void)
{
    return ULONG_MAX;
}

素朴なDの実装はこのようになります:

showmax1.d

extern(C) ulong max_val();
void main()
{
    import std.stdio : writeln;
    writeln(max_val());
}

結果はCコンパイラとアーキテクチャに依存します。 例えば、Windowsのdmcでは7316910580432895、x86 clでは59663353508790271、x64 clでは4294967295になります。 C側のunsigned longのサイズは最後のものも他の2つと変わらず4バイトですが、この最後の値が本当の正しい値です。 おそらくx64 ABIが返り値を8バイトのRAXレジスタに保存し、それがD側の8バイトであるulongとして問題なく読めてしまうからだと思います。 ここで重要なポイントは、D側が32ビットのレジスタに64ビットの返り値を期待しており、実際の値より大きな値を呼んでいるため、x86のコードの2つの値は役に立たないと言うことです。

幸いにも、DRuntimeはcore.c.configでこの問題の回避策を提供しており、c_longc_ulongがあります。 これら2つはCのランタイム実装とアーキテクチャの構成に合うよう、コンパイル時に調整されます。 これにより、Dのモジュール内のmax_valの宣言をこのように変更する必要があります:

showmax2.d

import core.stdc.config : c_ulong;
extern(C) c_ulong max_val();

void main()
{
    import std.stdio : writeln;
    writeln(max_val());
}

コンパイルし実行するとどこであっても正しい結果が得られます。 Windowsにおいては、一律で4294967295になります。

遭遇することは少ないですが、core.stdc.configlong doubleに対応したポータブルなc_long_double型も宣言しており、DのモジュールがバインドしなければならないCライブラリに登場するかもしれません。

この先について

この投稿で、CとDをコンパイルし同じ実行ファイルにリンクするためのセットアップをし、最初のつまづきポイントをいくつか見ていきました。 このシリーズの次の投稿ではDの配列とCの配列を協調させることについてフォーカスしていきます。 ではまた!