しらいとブログ

ネットで検索してもなかなか出てこないIT情報を独自にまとめています

Rust言語でDxLibを使う

DxLibはWindows用のゲームなんかを作れるライブラリです。DxLibはC++のライブラリですが、C#用のDLLバージョンも公開されており、C#以外でもFFI(Foreign function interface)が使える言語なら使えます。RustもFFIが使える言語なのでDLL版DxLibを使うことが出来ます。

ただし、DLL版はいくつかの関数が使えません。例えばprintfDxDrawFormatStringのような可変長引数を使った関数は呼び出し規約がstdcallなため使えません。ですが、普通にゲームを作るだけならあまり困ることは無いと思います。ちなみにstdcall以外を使うよう自分でDLLを作ればこのような制限はありません。

この記事ではRustでDxLibの関数を呼び出し、ウィンドウを出すところまで解説します。

1. DxLibのダウンロード

DxLibの公式サイトダウンロードページからVisualC# 用パッケージをダウンロードします。

2. DLLをRustプロジェクトにコピー

32bit版Rustならプロジェクトフォルダのbin/i686-pc-windows-gnu/DxLib.dllをコピーします。

64bit版Rustならプロジェクトフォルダのbin/x86_64-pc-windows-gnu/DxLib_x64.dllをコピーします。

たぶんそのようなフォルダは最初からは用意されていないので自分で作る必要があります。

このフォルダ名はRustコンパイラが入っているbinフォルダの中のrustlibフォルダの中身で決まるので確認してみてください。

また、64bit版で32bit版をコンパイルすることは出来無さそうです。つまり、32bit用と64bit用の両方の実行ファイルを作る場合はRustも両方インストールする必要があります。

3. RustでDxLibを宣言する

DxLibのDLLを使うための宣言は当然ながらDLLの仕様に合わせる必要があります。DxLibのDLLは関数呼び出しにstdcallという呼び出し規約を使っています。そして、普通なら関数名に付いているはずのサフィックスが付いていません。この場合、普通はGCC--enable-stdcall-fixupオプションを渡しますが、Rustでは宣言時にno_mangleを付ければいいみたいです。

DLLの宣言はこんな感じになります。

#[link(name = "DxLib")] // 64bitなら"DxLib_x64"にする
#[no_mangle]
extern "stdcall" {
	// ここにDLL内の関数を宣言する
}

DLL内の関数も一つ一つ宣言しなければなりません。DLL内の関数の一覧はDLLに付属しているDxDLL.hに載っていますが、全部は大変なので使うものだけ宣言します。見たらわかりますが、関数名の先頭にdx_が追加されています。DxLibの関数をDLLから呼び出すときはdx_を先頭に付ける必要があると覚えておいてください。ただし、それだけではありません。公式サイトのリファレンスに載っている関数とDLL内の関数は引数が違うのです。DxLibはC++の機能の一つ、デフォルト引数を使っています。リファレンスではそのデフォルト引数を省略して書いてあるため引数の数が実際の関数と違うものがあります。そして、DLLにはデフォルト引数は入っていませんし、Rustにもデフォルト引数の機能はありません。

試しにDxDLL.h内をSetGraphModeで検索してみてください。

int  __stdcall dx_SetGraphMode( int  ScreenSizeX, int  ScreenSizeY, int  ColorBitDepth, int  RefreshRate = 60);

RefreshRate = 60になっていますが、60という値はコンパイル時に削除されて普通の引数になります。この関数を呼ぶときは自分で60を渡す必要があります。

この仕組みさえ解れば後は自由に宣言するだけです。Cのintは32bitなのでRustではi32にするなど、型のサイズを揃える必要があります。型のサイズさえ合っていれば割と自由に入れ替えることが出来ます。例えばsignedunsignedi32u32)は入れ替えても問題無いですし、constを付けたり外したりすることもできます。

今回使う関数はこのようになりました。

#[link(name = "DxLib")] // 64bitなら"DxLib_x64"にする
#[no_mangle]
extern "stdcall" {
	fn dx_DxLib_Init() -> i32;
	fn dx_DxLib_End() -> i32;
	fn dx_ChangeWindowMode(Flag: i32) -> i32;
	fn dx_SetMainWindowText(WindowText: *const u8) -> i32;
	fn dx_SetGraphMode(SizeX: i32, SizeY: i32, ColorBitNum: i32, RefreshRate: i32 /* = 60 */) -> i32;
	fn dx_WaitKey() -> i32;
}

間違えやすい点としては、C/C++のポインタはRustではconst*mut*だという点と、C/C++charは8bitなのに対して、Rustのcharは32bitだという点があります。Rustの&T&mut Tはファットポインタと呼ばれ、通常のポインタ2つ分のサイズになります。文字に関してはRustは特殊で、文字と文字列のエンコードが違います。Rustの文字列(str)はUTF-8で、Rustの文字(char)はコードポイント(UTF-32)です。C/C++charはRustでは8bit整数のi8u8と見なすしかありません。Rustの文字列(str)はas_ptrを呼ぶことで*const u8で参照できるので、これをC/C++char*として渡すことは出来ます。その際、文字列の最後にNULL文字(\0)を付ける必要があります。ただし、C/C++UTF-8に対応しているかが問題となります。

DLLには定数は入っていないので、これは実際の値を調べて自分で定数を作る必要があります。定数の値はDLLに付属しているDxDLL.csから探します。

今回必要な定数はTRUEだけでした。

#[link(name = "DxLib")] // 64bitなら"DxLib_x64"にする
#[no_mangle]
extern "stdcall" {
	fn dx_DxLib_Init() -> i32;
	fn dx_DxLib_End() -> i32;
	fn dx_ChangeWindowMode(Flag: i32) -> i32;
	fn dx_SetMainWindowText(WindowText: *const u8) -> i32;
	fn dx_SetGraphMode(SizeX: i32, SizeY: i32, ColorBitNum: i32, RefreshRate: i32 /* = 60 */) -> i32;
	fn dx_WaitKey() -> i32;
}

const TRUE: i32 = 1;

この他に、構造体が必要になるケースがあります。その時はstructの前に[#repr(C)]を付けます。

4. unsafeを付けて呼び出す

Rust言語ではRust以外の関数を呼ぶときにunsafeを付けなければなりません。面倒なので全部unsafeの中に入れました。

#[link(name = "DxLib")] // 64bitなら"DxLib_x64"にする
#[no_mangle]
extern "stdcall" {
	fn dx_DxLib_Init() -> i32;
	fn dx_DxLib_End() -> i32;
	fn dx_ChangeWindowMode(Flag: i32) -> i32;
	fn dx_SetMainWindowText(WindowText: *const u8) -> i32;
	fn dx_SetGraphMode(SizeX: i32, SizeY: i32, ColorBitNum: i32, RefreshRate: i32 /* = 60 */) -> i32;
	fn dx_WaitKey() -> i32;
}

const TRUE: i32 = 1;

// Shift-JISで"テスト\0"
const INIT_TITLE: &'static [u8] = &[ 0x83, 0x65, 0x83, 0x58, 0x83, 0x67, 0x00 ];

const NEXT_TITLE: &'static str = "clicked\0";

fn main() {
	unsafe {
		// DxLibはデフォルトでフルスクリーンなのでウィンドウモードに変更
		dx_ChangeWindowMode(TRUE);
		
		// ウィンドウタイトルを"テスト"に変更
		dx_SetMainWindowText(INIT_TITLE.as_ptr());
		
		// ウィンドウサイズを320x240に変更
		// (32bit, 60fpsを指定していますがウィンドウモードでは関係ないはず)
		dx_SetGraphMode(320, 240, 32, 60);
		
		// DxLibの初期化
		dx_DxLib_Init();
		
		// キー入力かマウス入力を待つ
		dx_WaitKey();
		
		// ウィンドウタイトルを"clicked"に変更
		dx_SetMainWindowText(NEXT_TITLE.as_ptr());
		
		// キー入力かマウス入力を待つ
		dx_WaitKey();
		
		// DxLibの後処理
		dx_DxLib_End();
	}
}

Rust言語の文字列はUTF-8、DxLibの文字列はShift-JIS(MS932)なので、本来なら変換しないといけないのですが、この2つは半角英数字などのASCII文字は共通なので、それだけならそのまま使えます。また、実行時に文字コードを変換するのはコストが大きいので、Shift-JISをそのまま使えるよう、バイト配列を使ったやり方もやってみました。どちらもNULL終端を忘れてはいけません。

5. ビルドする

普通のビルドだと起動時にコマンドプロンプトが表示されてしまいます。それを防ぐためには-mwindowsオプションを付ける必要があります。

rustc --release -- -C link-args=-mwindows

筆者は-staticオプションも付けてGCCのDLLを実行ファイルに埋め込んでいます。

rustc --release -- -C link-args="-static -mwindows"

NightlyバージョンのRustを使っているのであればLink argsを使うことも出来ます。

#![feature(link_args)]
#[link_args = "-static -mwindows"]
extern {}

#[link(name = "DxLib")] // 64bitなら"DxLib_x64"にする
#[no_mangle]
extern "stdcall" {
/* 以下省略 */
6. 実行してみた

実行にはDxLibのDLLが必要なのでパスを通すか実行ファイルと同じフォルダにコピーしましょう。

スクリーンショット

前にGo言語でDxLibを使ってみたことがあるのですが、Go言語がGC(ガベージコレクタ)付の言語だったために面倒な記述が必要になったりして使いづらかったです。しかし、Rustは最初からGCが無いので全く問題ありません。C/C++でDxLibを使う時とほぼ同じ感覚で使えます。これからはRust言語でゲーム開発が行われるようになるかもしれませんね。