この記事は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のXCode
でclang
がインストールできます。
ほとんどのLinuxやBSDでは、例えばDebianやDebianベースのシステムにおいて推奨されるコマンドラインapt-get install build-essential
のようにGCCパッケージが利用できます。
詳しくはあなたのシステムのドキュメントを見てください。
これらのシステムでは、たいてい環境変数CC
にシステムコンパイラのコマンドがセットされています。
あなたのシステムに合わせて以下のCC
はgcc
かclang
のどちらかに置き換えてください。
CC -c chello.c
dmd hello.d chello.o
./hello
これはあなたのシステムの設定に合わせて32ビットまたは64ビットの出力を生成します。
あなたが64ビットのシステムに32ビットのデベロッパーツールをインストールしている場合、-m32
をCC
とdmd
の両方に渡すことで32ビットバイナリを生成できます。
long
な道のり
CとDのソースを同じバイナリにコンパイルしリンクできるようになったので、さらに一般的な落とし穴を見ていきましょう。 これを正しく理解することは、Windowsとその他のプラットフォーム両方で役に立ちます。
Dの特徴の1つとして、すべての整数型のサイズは固定されています。
short
は常に2バイトであり、int
は4バイトです。
これはシステムのアーキテクチャにかかわらず決して変わりません。
これは仕様が各整数型のサイズを相対的にしか指定せず、実装に任せているCとは異なります。
それでもモダンなコンパイラの間では幅広い合意があり、現在Dがサポートするすべてのプラットフォームにおけるほとんどの整数型のサイズはDのそれと一致します。
例外はlong
とulong
です。
Dにおいて、long
とulong
は常に8バイトです。
version(Posix)
傘下にあるほとんどの64ビットシステムでは足並みが揃っており、long
とunsigned long
も8バイトです。
しかし32ビットアーキテクチャにおいては4バイトです。
さらに、Windowsにおいては64ビットアーキテクチャであっても常に4バイトです。
今日のほとんどのCコードはこれらの違いを考慮しプリプロセッサを使うことによってカスタム整数型を定義するか、誤解の余地なく定義されるint32_t
やint64_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_long
とc_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.config
はlong double
に対応したポータブルなc_long_double
型も宣言しており、DのモジュールがバインドしなければならないCライブラリに登場するかもしれません。
この先について
この投稿で、CとDをコンパイルし同じ実行ファイルにリンクするためのセットアップをし、最初のつまづきポイントをいくつか見ていきました。 このシリーズの次の投稿ではDの配列とCの配列を協調させることについてフォーカスしていきます。 ではまた!