D言語研究室



D言語のサンプル

円周率を求める - 簡単なWin32プログラム - マイクロスレッドの実装 - D言語からSDLを呼び出す - D言語からLuaを呼び出す - zip書庫を扱う - COFFからOMTへ変換する - 新しいCOFFからOMFに変換する - 文章階層化整形ツール - glExcessをDに移植する

円周率を求める


DigitalMarsのコンパイラ付属のサンプルをOO風に書き直したものを以下に掲載。


import std.c.stdio;
import std.c.stdlib;
import std.c.time;

int main(char[][] args)
{
	int q;
	if (args.length == 2) {
		sscanf((char*)args[1],"%d",&q);
	} else {
		printf("Usage: pi [precision]\n");
		exit(55);
	}

	//	計算
	CPiCalc picalc = new CPiCalc(q);

	int startime, endtime;
	std.c.time.time(&startime);
	picalc.calc();
	std.c.time.time(&endtime);

	//	表示
	picalc.print();
	printf("%ld seconds to compute %d digits of pi\n",endtime-startime,q);

	return 0;
}

class CPiCalc {
	this(int q) // 桁数設定してチョ
	{
		q++;	//	1桁多めに保持
		p = new byte[q];
		t = new byte[q];
		this.q = q;
	}

	void calc() 	//	円周率計算
	{
		arctan(2);
		arctan(3);
		mul4();
	}

	void print()	// 円周率表示
	{
		printf("pi = %d.",p[0]);
		for(int i=1;i<q-1;++i)
			printf("%d",p[i]);
		printf("\n");
	}

	//--- 以下円周率計算のための関数

	void arctan(int s)
	{
		int n;

		t[0] = 1;
		div(s); 		/* t[] = 1/s */
		add();
		n = 1;
		do {
			mul(n);
			div(s * s);
			div(n += 2);
//			if (((n-1) / 2) % 2 == 0)
			if (((n-1) & 2) == 0)
				add();
			else
				sub();
		} while (!tiszero());
	}

	void add()
	{
		//	足し合わせは下位桁から足し合わせる
		for (int j = q-1; j >= 0; j--)
		{
			if (t[j] + p[j] > 9) {
				p[j] += t[j] - 10;
				if (j!=0) p[j-1] ++;
			} else
				p[j] += t[j];
		}
	}

	void sub()
	{
		for (int j = q-1; j >= 0; j--)
			if (p[j] < t[j]) {
				p[j] -= t[j] - 10;
				if (j!=0) p[j-1] --;
			} else
				p[j] -= t[j];
	}

	void mul(int multiplier)
	{
		int b;
		int i;
		int carry = 0, digit = 0;

		for (i = q-1; i >= 0; i--) {
			b = (t[i] * multiplier + carry);
			digit = b % 10;
			carry = b / 10;
			t[i] = digit;
		}
	}

	/* t[] /= l */

	void div(int divisor)
	{
		int quotient, remainder = 0;

		foreach(inout byte n;t){
			int b = (10 * remainder + n);
			quotient = b / divisor;
			remainder = b % divisor;
			n = quotient;
		}
	}

	void div4()
	{
		int c = 0, d = 0;
		foreach(inout byte n;p) {
			c = (10 * d + n) / 4;
			d = (10 * d + n) % 4;
			n = c;
		}
	}

	void mul4()
	{
		int i, c = 0, d = 0;

		for (i = q-1; i >= 0; i--) {
			d = (p[i] * 4 + c) % 10;
			c = (p[i] * 4 + c) / 10;
			p[i] = d;
		}
	}

	bool tiszero()
	{
		foreach(byte n;t)
			if (n != 0) return false;
		return true;
	}

private:
	byte p[];
	byte t[];
	int q;
}


簡単なWin32プログラム


Win32用のプログラムには、まず、スタートアップコードが必要となります。
以下のものをコピーして自分なりに変更して使うと良いでしょう。
(以下のはDigitalMarsのコンパイラのサンプルコードからの改変したものです。)


/**********************************************************/
//	以下、windows用のスタートアップコード
/*
    \dmd\src\phobos\dmain2.dにある
    コンソール版のD言語のスタートアップコードと比較してみてください。

    .defファイルとして、最低限以下のことを書いたファイルを用意して
    コンパイル時に、そのdefファイルを指定してください。
        EXETYPE NT
        SUBSYSTEM WINDOWS
*/

extern (C) void gc_init();
extern (C) void gc_term();
extern (C) void _minit();
extern (C) void _moduleCtor();
extern (C) void _moduleUnitTests();

extern (Windows)
int WinMain(HINSTANCE hInstance,
	HINSTANCE hPrevInstance,
	LPSTR lpCmdLine,
	int nCmdShow)
{
	int result;

	gc_init();			// initialize garbage collector
	_minit();			// initialize module constructor table

	try
	{
	_moduleCtor();		// call module constructors
	_moduleUnitTests(); // run unit tests (optional)

	result = user_start();	// ユーザーコードはここに入れる
	}

	catch (Object o)		// catch any uncaught exceptions
	{
	MessageBoxA(null, (char *)o.toString(), "Error",
			MB_OK | MB_ICONEXCLAMATION);
	result = 0; 	// failed
	}

	gc_term();			// run finalizers; terminate garbage collector
	return result;
}


上記コードはuser_start関数を呼び出すようになっていますので、その関数を用意します。

(以下のは、DigitalMarsのDコンパイラ付属のサンプルを改造したものです。)


/*
    コンパイルは以下の方法で。
        dmd wintest gdi32.lib wintest.def
    wintest.defについては、
        EXETYPE NT
        SUBSYSTEM WINDOWS
    と書いたファイルを用意すればok。

    dmd -L/EXET:NT -L/SU:windows source.d ...
のように、リンカに直接指定しても良いようです。
*/

import std.c.windows.windows;
import std.c.stdio;

const int IDC_BTNCLICK = 101;
const int IDC_BTNDONTCLICK = 102;

extern(Windows)
int WindowProc(HWND hWnd, uint uMsg, WPARAM wParam, LPARAM lParam)
{
	switch (uMsg)
	{
		case WM_COMMAND:
		{
		switch (LOWORD(wParam))
		{
			case IDC_BTNCLICK:
			if (HIWORD(wParam) == BN_CLICKED)
				MessageBoxA(hWnd, "Hello, world!", "こんにちは",
					MB_OK | MB_ICONINFORMATION);
			break;
			case IDC_BTNDONTCLICK:
			if (HIWORD(wParam) == BN_CLICKED)
			{
				MessageBoxA(hWnd,
					"わざとメモリ保護違反を出します", "メモリ保護違反出すよー",
					MB_OK | MB_ICONEXCLAMATION);
				*(cast(int*) null) = 666;
			}
			break;
		}
		break;
		}

		case WM_PAINT:
		{
		static char[] text = "D言語はWindowsも出来る";
		PAINTSTRUCT ps;
		HDC dc = BeginPaint(hWnd, &ps); 	
		RECT r;
		GetClientRect(hWnd, &r);
		HFONT font = CreateFontA(80, 0, 0, 0, FW_EXTRABOLD, FALSE, FALSE,
			FALSE, /*ANSI_CHARSET*/ SHIFTJIS_CHARSET, OUT_DEFAULT_PRECIS,
			CLIP_DEFAULT_PRECIS,DEFAULT_QUALITY, DEFAULT_PITCH | FF_DONTCARE,
			"MS明朝");
		HGDIOBJ old = SelectObject(dc, cast(HGDIOBJ) font);
		SetTextAlign(dc, TA_CENTER | TA_BASELINE);
		TextOutA(dc, r.right / 2, r.bottom / 2, text, text.length);
		SelectObject(dc, old);
		EndPaint(hWnd, &ps);
		break;
		}
		case WM_DESTROY:
		PostQuitMessage(0);
		break;

		default:
		break;
	}
	return DefWindowProcA(hWnd, uMsg, wParam, lParam);
}

int user_start()
{
//	ここにメインプログラムを書く

	HINSTANCE hInst = GetModuleHandleA(null);
	WNDCLASS wc;
	wc.lpszClassName = "DWndClass";
	wc.style = CS_OWNDC | CS_HREDRAW | CS_VREDRAW;
	wc.lpfnWndProc = &WindowProc;
	wc.hInstance = hInst;
	wc.hIcon = LoadIconA(cast(HINSTANCE) null, IDI_APPLICATION);
	wc.hCursor = LoadCursorA(cast(HINSTANCE) null, IDC_CROSS);
	wc.hbrBackground = cast(HBRUSH) (COLOR_WINDOW + 1);
	wc.lpszMenuName = null;
	wc.cbClsExtra = wc.cbWndExtra = 0;
	assert(RegisterClassA(&wc));
	
	HWND hWnd, btnClick, btnDontClick;
	hWnd = CreateWindowA("DWndClass", "Just a window", WS_THICKFRAME |
		WS_MAXIMIZEBOX | WS_MINIMIZEBOX | WS_SYSMENU | WS_VISIBLE,
		CW_USEDEFAULT, CW_USEDEFAULT, 400, 300, HWND_DESKTOP,
		cast(HMENU) null, hInst, null);
	assert(hWnd);
	
	btnClick = CreateWindowA("BUTTON", "Click Me", WS_CHILD | WS_VISIBLE,
		0, 0, 100, 25, hWnd, cast(HMENU) IDC_BTNCLICK, hInst, null);

	btnDontClick = CreateWindowA("BUTTON", "DON'T CLICK!", WS_CHILD | WS_VISIBLE,
		110, 0, 100, 25, hWnd, cast(HMENU) IDC_BTNDONTCLICK, hInst, null);

	MSG msg;
	while (GetMessageA(&msg, cast(HWND) null, 0, 0))
	{
		TranslateMessage(&msg);
		DispatchMessageA(&msg);
	}
	
	return 1;
}


見ての通り、おおむねWindowsのAPIが呼び出せています。CreateWindowAは、VCのincludeヘッダではCreateWindowのマクロとして定義されており、ANSIバージョンではCreateWindowA,UNICODEバージョンではCreateWindowWを呼び出すようになっていますが、このへんstd.c.windows.windowsのほうではきちんとwrapされていないようです。

今後、このへんDigitalMars社で対応される予定があるのか無いのか、あるいは自分でwrapして使えばいいのか、誰かがきっちりportingしてくれるのを待つのがいいのかはよくわかりません。少なくともD言語の仕様の外の話のようなので、過度の期待はできないでしょう。

あと、std.c.windowsのほうには、全てのAPIは登録されていないようです。

もし自分の必要なAPIが無ければ、自分で登録して使うか、あるいはDedicateD[英語]のほうからWin32APIのimport moduleをdownloadしてきて使うかです。

とりあえず、思ったより簡単にWindows用のプログラムを作れることがわかりました。

参考になりそうなリンク:

D言語でWindows用のプログラムを作成(EXE,DLL):
http://www.digitalmars.com/d/windows.html

上記の和訳:
http://www.kmonos.net/alang/d/windows.html

マイクロスレッドの実装

ゲームで欲しくなるマイクロスレッド(co-routine,fiber等の名前で呼ばれることもあり)をD言語で実装してみました。

マイクロスレッドについては、こちら。
http://bm98.yaneu.com/yaneSDK3rd/chap0124.html

原理は、別のスタックフレームを用意して、そことすり替えているだけです。

以下、switchingする関数だけご紹介。完全な実装は、yaneSDK4Dのy4d_thread/microthread.dにあります。


	void	switchThread(){
		asm {
			naked;					//	このnakedは有効

	//	naked functionならばEAXにはthisが入っていると仮定して良いらしい
	//	それなら、[EAX+8]がこのクラスで宣言されているの一つ目の変数
	//	(register_esp_)だと仮定していい

			push	EBX;
			push	EBP;
			push	ESI;
			push	EDI;

			xchg	ESP,[EAX+8];

			pop		EDI;
			pop		ESI;
			pop		EBP;
			pop		EBX;
			ret;
		}
	}


DMCコンパイラの場合、-debugをつけてビルドしても、関数呼び出しのときにスタックはほとんど食わないようで、マイクロスレッドとして小さな関数を動かすだけならば、300バイト程度の仮想スタックで十分動きます。よって、シューティングの敵一匹ごとにマイクロスレッドを使ったとしても十分実用になると思います。

マイクロスレッド動作中にGCが動いても大丈夫なのか?と思った人はこちらも参考にしてください。→D言語のGCの仕組み

D言語からSDLを呼び出す


DedicateD[英語]のほうからSDL用のportingライブラリを落としてきます。本体(libファイル,dllファイル)も含まれているので、これだけあればSDL[ライブラリ]を利用できます。yaneSDK4DにSDL一式含まれているのでそちらを利用しても良いでしょう。

プログラムの先頭に
import SDL;

と書けば、SDLを呼び出せるようになります。SDL.d等は、include pathの通ったフォルダに存在しないといけません。これは、dmdコンパイラの場合、Iオプションで指定します。また、SDL.libとリンクすることを指定してやります。

例)
dmd test.d c:\sdk\sdl.lib -Ic:\sdl

SDLを利用して書いたプログラムの実行に関してはsdl.dllを実行ファイルと同じフォルダに配置する必要があります。

D言語からLuaを呼び出す


Lua[言語]というのは、組み込み系の言語としてはかなり有名な言語で、採用実績もかなりの数にのぼります。汎用性、拡張性において、優れています。

ここでは、D言語からLua言語で書かれたプログラムを呼び出す方法について書いておきます。また、逆に、Lua言語のほうから、D言語の関数の呼び出しも出来ます。

dmdコンパイラでコンパイルするのに必要なもの一式用意しました。
これとdmdコンパイラだけあれば、Lua5.0をD言語から利用できます。
lua_project.zip
(ご協力いただいた方々、ありがとうございましたm(_ _)m)

サンプルプログラム。

import lua;
import lualib;
import lauxlib;

import std.stream;
import std.string;

extern (C) int printNum(lua_State lua) {
	int n = lua_gettop(lua);
	if (n != 1) throw new Error("dame dayo");

	double val = lua_tonumber(lua, 1);
	stdout.writeLine(std.string.toString(val));

	lua_pushnumber(lua, cast(lua_Number)(val+1));
	
	return 1;
}

int main() {
	lua_State lua = lua_open();
	lua_baselibopen(lua);
	luaL_loadfile(lua, "luatest.lua");
	lua_register(lua, "printNum", &printNum);
	lua_pcall(lua, 0, 0, 0);

	return 0;
}


何がすごいって、D言語で書かれた関数printNumを"printNum"という関数名で登録して、Lua言語側から呼び出せるようにしている点とか、注目に値すると思うのです。このようにD言語で用意した関数をLua言語から呼び出したり、D言語側の変数をLua言語側からアクセスしたり出来ます。

ゲームでフラグによる分岐とか必要になるシーンがあると思いますが、そういう部分を記述するのにLua言語で書かれたスクリプトを実行する、なんてことも出来そうです。

ノベル系のゲームで、シナリオ部分をXML等で記述して、Luaタグで囲まれた部分だけを切り出して、その部分は実行する、なんてことも出来ます。

あるいは、D言語でSDLのようなゲームライブラリをimportして関数を片っ端からLuaのほうに登録して、LuaからSDLやOpenGL etc..を呼び出せるようにすれば、お手軽にゲーム用スクリプトの完成!なんてことも可能なわけで。そういう意味では、未知の可能性のようなものを感じます。

zip書庫を扱う


phobos付属のzlibは、zip streamを扱うだけで、いわゆるzip書庫(pkzip)は解凍できないのです。未実装扱いになっていますが、これは将来的にも実装されないのかも知れません。

※ DMD0.78では実装されてるっぽい?しかし、ファイルまるごと読み込むので、ちょっと使えそうにもない。

そこで、zip書庫ファイルをシームレスに読み込むクラスを自作する必要があるのですが、pkzipのヘッダはそれほど複雑ではないので少し調べれば簡単に実装できます。

ヘッダ仕様は、
http://www.pkware.com/
の、ここ:
http://www.pkware.com/products/enterprise/white_papers/appnote.html

サンプル:
http://f-ts.bias.ne.jp/~azuco/column/emuzip.htm

ところが、pkzipファイルのフォーマット確認したら、ヘッダがインターリーブしてます。仮にzipファイルに200個のファイルが内在していれば、1つのファイルを読み出すごとに平均で約100回のランダムアクセスを行なわないといけないわけで、読み出しは当然セクタ単位、セクタ長が8kだとして、100*8k = 800kの読み込み。

1kほどの定義ファイルを読み込むのにも800k。
10kほどのインターフェースを読み込むのにも800k。

こんなものは、ゲームには到底使えません。(もちろん、pkzipがこのようになっている理由は、1パスで書き込みを完了する必要があったからでしょうけど。)

と思ったら、仕様書の「D. Central directory structure」のところ、末尾にもヘッダに相当するものがついてるという指摘を掲示板でいただきました。なるほど。じゃあ、zip書庫をシームレスに扱うクラスを用意することにします。

さくっと実装しました。→ yaneSDK4Dのy4d_aux/filesys.dのFileArchiverZipFileが、zipを扱うクラスなので詳しいことはそちらを見てください。

※ yaneSDK4Dはパスワード付きの書庫の読み込みにも対応しました。

COFFからOMTへ変換する

DMDコンパイラが出力するオブジェクトファイル形式はBCCと同じOMFです。
VC++やGCCはCOFFです。 なお、COFFにも新旧あり、DirectX8は新しいCOFFだそうです。
これを変換するにはVCのlib.exeで古いCOFFに変換しなきゃいけないそうです。

COFFからOMFに変換するのは、COFF2OMFというのを使えば出来ます。
BCC5.5などに付属しています。

http://www.borland.co.jp/cppbuilder/freecompiler/

dllからシンボルを抜き出してlibに変換するには、DigitalMarsのフリーのCコンパイラのBasic Utilitiesに含まれている、implibを使えば出来ます。こちらのほうが作業が簡単でしょう。

http://www.digitalmars.com/download/freecompiler.html

たとえば、SDL_image.dllから、libを作成するには、

implib /system SDL_image.lib SDL_image.dll

のように、/systemオプションを指定して、アンダーバー( _ )を付与してexportすればokです。

BCC付属のimplibでdllからlibを作成するには、

implib -a SDL_image.lib SDL_image.dll

のように-aオプションを指定して、アンダーバー( _ )を付与してexportすればokです。

新しいCOFFからOMFに変換する

VC++.NETのCOFFは新しい形式のCOFFなので、簡単には変換できません。

dllを経由して、OMFのlibを作成すると良いようです。ここのページが参考になります:
「dllを作成してdllからlibを作成してやる必要がある」
http://bme.t.u-tokyo.ac.jp/members/manabu/MKL_BCC/mkl_BCC.html


dumpbin /exports libguide40.dll >libguide40.temp

ここで生成されたtempファイルをdefファイルに加工。VC7のlib.exeを使って、

lib /DEF:libguide40.def

(mspdb70.dllが無いと言われるので、検索でVC7のフォルダ
C:\Program Files\Microsoft Visual Studio .NET
を探すと出てくるのでlib.exeと同じフォルダにコピー)


この、defファイルへの加工手順は、ダンプ内容:


Microsoft (R) COFF/PE Dumper Version 7.00.9466
Copyright (C) Microsoft Corporation.  All rights reserved.


Dump of file libguide40.dll

File Type: DLL

  Section contains the following exports for libguide40.dll

	00000000 characteristics
	3E9F0C51 time date stamp Fri Apr 18 05:19:29 2003
		0.00 version
		   1 ordinal base
		1745 number of functions
		 273 number of names

	ordinal hint RVA	  name

	   1725    0 0000B680 KMP_CALLOC
	   1726    1 0000B940 KMP_FREE

  Summary

	  228000 .data
		3000 .data1
		5000 .rdata
		4000 .reloc
		1000 .rsrc
	   1B000 .text


を、


LIBRARY libguide40
DESCRIPTION 'libguide40 DLL'
EXPORTS
	_KMP_CALLOC
	_KMP_FREE


のようにしてやる必要があります。このEXPORTSの部分、書き換えるのが面倒くさいので、エディタを使って正規表現置換すると良いでしょう。シンボルの先頭に '_'を付与しないといけないことにも注意。

また、Windows環境でのextern (Windows) ,Linux環境でのextern (C)は、呼び出し先でスタックを調整して帰ります。いわゆるstdcallです。そこで、シンボル名のところに調整するスタックの数を書く必要があります。

たとえば、
_glutInit@8
のように、です。実際に

extern (Windows) {
void glutInit(int,int);
}
と書いてコンパイルを行ない、リンクエラーを出せば、内部的にどんな名前のシンボルに変換されているかがわかります。

その他資料。
詳しいことは、こちら:
http://www.microsoft.com/japan/developer/library/vccore/_core_export_from_a_dll_using_..def_files.htm

VC++.NETでコンパイルするときにリンカのコマンドオプションとして /MAPを指定してもいいでしょう。
http://www.microsoft.com/japan/developer/library/vccore/_core_.2f.map.htm

こんな方法も:
http://cmeerw.org/prog/dm/ms_sdk.html

文章階層化整形ツール


このD言語研究室のページのために使っている文章整形ツールoutliner
http://www.cybercom.co.jp/~mikio/

ところが、このツールは、ソースコードを正確に埋め込むことが出来ません。

そこで、元文章の <D_CODE> 〜 </D_CODE> で囲った領域だけをHTMLコードに整形するツールが必要となりました。DMDのサンプルのD2HTMLを1時間ほどいじって、そのあとD言語のキーワードを登録して作ったのが、これです。

codecov.zip(ソース・バイナリつき)

D2HTMLのほうにあった、16進数が書いてあるとハングするバグも修正してあります。ソースは汚くなってますんで参考にはならないと思いますが。

使いかたは、ファイル「元原稿.txt」というのを入力として「元原稿2.txt」というのを出力します。この出力ファイルをoutlinerの元ソースとして指定して使います。

glExcessをDに移植する


D言語からopenGLを呼び出せるわけで、openGLのソースを少しいじればそのまま動かせるはずです。試しにglExcessを移植してみました。

glExcessは(ひと昔前の言葉で言うところの)メガデモの一種です。
http://www.glexcess.com/

移植したものはこれです(ソース付き):

glExcess into D (no sound)
http://yaneurao.dyndns.org/yaneSDK4D/glexcess004.zip

※ news groupに投稿したところ、公式のnewsトピックのところで取り上げてもらいました。


Last Updated : 2004-4-19

written by yaneurao
http://bm98.yaneu.com/dlang/