forked from dlang/dlang.org
-
Notifications
You must be signed in to change notification settings - Fork 4
/
dll.dd
631 lines (513 loc) · 18.6 KB
/
dll.dd
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
Ddoc
$(D_S D で作る Win32 DLL,
$(P DLL (Dynamic Link Libraries) は、
Windows
のシステムプログラミングの基礎技術の一つです。
D言語では、様々な種類のDLLの作成が可能です。
)
$(P DLLとは何でありどのように動くのか、といった背景知識に関しては
Jeffrey Richter の本
$(LINK2 http://www.amazon.co.jp/dp/4756138055,
Advanced Windows) の第11章が必読です。
)
$(P この文書では、Dで様々な種類のDLLを作る方法を紹介します。)
$(UL
$(LI <a href="#Cinterface">C のインターフェイスを持つ DLL</a>)
$(LI <a href="#com">COMサーバDLL</a>)
$(LI <a href="#Dcode">DLL内のDのコードを呼ぶDのコード</a>)
)
<h2><a name="Cinterface">C のインターフェイスを持つ DLL</a></h2>
$(P C言語インターフェイスを提供するDLLは、
DLL内のC関数を呼ぶ機能に対応した他の言語と連携できます。
)
$(P DLL は
D でも C とだいたい同じ方法で作れます。
次のような $(D DllMain()) を書きます:
)
--------------------------------
import std.c.windows.windows;
import core.sys.windows.dll;
__gshared HINSTANCE g_hInst;
extern (Windows)
BOOL DllMain(HINSTANCE hInstance, ULONG ulReason, LPVOID pvReserved)
{
switch (ulReason)
{
case DLL_PROCESS_ATTACH:
g_hInst = hInstance;
dll_process_attach( hInstance, true );
break;
case DLL_PROCESS_DETACH:
dll_process_detach( hInstance, true );
break;
case DLL_THREAD_ATTACH:
dll_thread_attach( true, true );
break;
case DLL_THREAD_DETACH:
dll_thread_detach( true, true );
break;
}
return true;
}
-------------------------------
$(P 注:)
$(UL
$(LI DllMain では、適切なヘルパ関数への単純な転送のみを行います。
これによってランタイムが正しく設定され、
GCやスレッドローカル記憶域の処理に関連するスレッドオブジェクトが生成されます。)
$(LI DLL は他のDLLとランタイムやメモリを共有しません。)
$(LI ヘルパ関数への第一引数のbool値は、
全スレッドがGCにコントロールされるべきかを示すフラグです。
もし、GC中も停止したくないスレッドがあれば、手動制御が必要です。
その場合、自動制御を切るためにfalseを渡して下さい。)
$(LI $(D DllMain())
の存在はコンパイラが認識し、
$(LINK2 http://www.digitalmars.com/ctg/acrtused.html, __acrtused_dll)
と $(TT phobos.lib) ランタイムライブラリへの参照を自動的に追加します。)
)
ビルド時には、次のような
($(LINK2 http://www.digitalmars.com/ctg/ctgDefFiles.html, モジュール定義ファイル))
をリンクします:
$(MODDEFFILE
LIBRARY MYDLL
DESCRIPTION 'My DLL written in D'
EXETYPE NT
CODE PRELOAD DISCARDABLE
DATA WRITE
EXPORTS
DllGetClassObject @2
DllCanUnloadNow @3
DllRegisterServer @4
DllUnregisterServer @5
)
$(P EXPORTS の中に並べた関数名は一例です。
実際にMYDLLからexportしたい関数の名前に置き換えてください。
あるいは、
$(LINK2 http://www.digitalmars.com/ctg/implib.html, implib).
を使います。以下に、文字列を出力する print() 関数を備える、
簡単なDLLの例を示します:
)
<h4>mydll.d:</h4>
-------------------------------
module mydll;
import std.c.stdio;
export void dllprint() { printf("hello dll world\n"); }
-------------------------------
$(P 注: 例を可能な限り簡単にするために、
ここでは $(CODE printf) を
$(CODE writefln)
の代わりに使用しています。)
<h4>mydll.def:</h4>
$(MODDEFFILE
LIBRARY "mydll.dll"
EXETYPE NT
SUBSYSTEM WINDOWS
CODE SHARED EXECUTE
DATA WRITE
)
$(P $(CODE DllMain()) を含む上記のコードを
$(TT dll.d) というファイルに書いたとします。
コンパイルとリンクは以下のようなコマンドで行います:
)
$(CONSOLE
C:>dmd -ofmydll.dll -L/IMPLIB mydll.d dll.d mydll.def
C:>
)
$(P これで、mydll.dll と mydll.lib が作られます。
次に、このDLLを使うプログラム、test.d です:
)
<h4>test.d:</h4>
-------------------------------
import mydll;
int main()
{
mydll.dllprint();
return 0;
}
-------------------------------
$(P 関数定義を消した、宣言だけのインターフェイスファイルを作ります:)
<h4>mydll.di:</h4>
-------------------------------
export void dllprint();
-------------------------------
そして以下でコンパイル・リンクし:
$(CONSOLE
C:>dmd test.d mydll.lib
C:>
)
実行します:
$(CONSOLE
C:>test
hello dll world
C:>
)
<h3>メモリ割り当て</h3>
$(P DのDLLはメモリ管理にガベージコレクタを使います。問題は、
割り当てられたメモリを指すポインタがDLLの外で使われるとどうなるか、という点です。
DLLがCインターフェイスを提供するならば、他言語で書かれたコードから
呼び出されることを想定しなければなりません。
それら他の言語達は、Dのメモリ管理について一切タッチしていません。
つまり、Cインターフェイスを提供する以上、DLL内部のメモリ管理について
DLLの呼び出し元が一切知る必要がないように工夫する必要があります。
)
$(P この問題に対しては沢山のアプローチがあります:)
$(UL
$(LI Dのgcが割り当てたメモリは、DLLの呼び出し元へ返さない。代わりに、
呼び出し元の割り当てたバッファを受け取って、そこをDLLで埋める
インターフェイスにする。)
$(LI そのポインタをDのDLL内に保持して、GCによって回収されないようにする。
そして、呼び出し元がデータが不要になったことをDLLに伝えるプロトコルを
用意しておく。)
$(LI GC.addRange を呼び出すことで、GCに、
外部からの参照の存在を伝える。)
$(LI DLLの外とやりとりするメモリについては、例えば VirtualAlloc() のような
OSの機能を直接使って割り当てる。)
$(LI U呼び出し元へ返されるメモリの割り当てには std.c.stdlib.malloc()
(またはその他のGC外のアロケータ)を使う。
呼び出し元がデータを解放するための関数も export しておく。)
)
<h2><a name="com">COM プログラミング</a></h2>
Windows API の多くは、COM (Common Object Model) オブジェクト (OLE や ActiveX
オブジェクトとも呼ばれる) に基づいています。COM オブジェクトとは、第一フィールドが
vtbl[] へのポインタで、その最初の三つのエントリが QueryInterface(), AddRef(),
Release() であるオブジェクトのことです。
<p>
COM の理解には、Kraig Brockshmidt の
$(LINK2 http://www.amazon.co.jp/dp/4756116183, Inside OLE)
が必読書です。
<p>
COMオブジェクトとDのinterfaceの間には類似性があります。COMオブジェクトは
皆Dのinterfaceとして表現できますし、interface Xを実装したDのオブジェクトは、
COMオブジェクトX として export できます。
これはつまり、Dは他の言語で実装された
COMオブジェクトとの相互運用ができるということです。
<p>
絶対必要なわけではありませんが、Phobosライブラリは、DのCOMオブジェクト
の基底クラスとして便利な、ComObjectクラスを提供しています。
ComObjectは QueryInterface(), AddRef(), Release()
の標準的な実装を備えています。
<p>
Windows COM オブジェクトは、Dのデフォルトとは違う、
Windows呼び出し規約に従います。
このため、属性 extern (Windows) が必要です。
結論として、COMオブジェクトを書くには次のようになります:
-------------------------------
import std.c.windows.com;
class MyCOMobject : ComObject
{
extern (Windows):
...
}
-------------------------------
サンプルディレクトリの中に、COMのクライアントとサーバDLLの例があります。
<h2><a name="Dcode">DLL内のDのコードを呼ぶDのコード</a></h2>
DのコードをDLL内に含めて、
静的リンクの場合と全く同様に扱えるようにする機能はもちろん必要です。
それによって、コードを別のDLLとして独立に開発し、
しかもアプリケーション間での共有が可能になります。
<p>
潜在的に問題となるのは、ガベージコレクション(GC)の取り扱いです。
EXE と DLL のそれぞれがGCのインスタンスを保持しています。
これらのGC同士はお互いに影響せずに共存することも可能ですが、
複数のGCが走っているというのは、無駄で非効率的です。
そこで、GC一つを決めて、他のDLLのGCはそのGCへとリダイレクトするという
案を考えました。一つに決めるGCは、
ここでは EXE ファイルのものを使うこととしました。GC のために特別の
DLL を一つ用意しておくという方法もあります。
<p>
以下は、DLLを静的にロードする方法と、
動的にロード/アンロードする方法の両方の例になっています。
<p>
DLLのソースコード mydll.d から見ていきましょう:
-------------------------------
/*
* MyDll D言語DLLの書き方デモ
*/
import core.runtime;
import std.c.stdio;
import std.c.stdlib;
import std.string;
import std.c.windows.windows;
HINSTANCE g_hInst;
extern (C)
{
void gc_setProxy(void* p);
void gc_clrProxy();
}
extern (Windows)
BOOL $(B DllMain)(HINSTANCE hInstance, ULONG ulReason, LPVOID pvReserved)
{
switch (ulReason)
{
case DLL_PROCESS_ATTACH:
printf("DLL_PROCESS_ATTACH\n");
Runtime.initialize();
break;
case DLL_PROCESS_DETACH:
printf("DLL_PROCESS_DETACH\n");
Runtime.terminate();
break;
case DLL_THREAD_ATTACH:
printf("DLL_THREAD_ATTACH\n");
return false;
case DLL_THREAD_DETACH:
printf("DLL_THREAD_DETACH\n");
return false;
}
g_hInst = hInstance;
return true;
}
export void $(B MyDLL_Initialize)(void* gc)
{
printf("MyDLL_Initialize()\n");
gc_setProxy(gc);
}
export void $(B MyDLL_Terminate)()
{
printf("MyDLL_Terminate()\n");
gc_clrProxy();
}
$(B static this)()
{
printf("static this for mydll\n");
}
$(B static ~this)()
{
printf("static ~this for mydll\n");
}
/* --------------------------------------------------------- */
class $(B MyClass)
{
char[] $(B concat)(char[] a, char[] b)
{
return a ~ " " ~ b;
}
void $(B free)(char[] s)
{
delete s;
}
}
export MyClass $(B getMyClass)()
{
return new MyClass();
}
-------------------------------
<dl>
<dt>$(B DllMain)
<dd>全てのD言語DLLのエントリーポイントはこの関数です。
Cのスタートアップコードから呼び出されます。
(DMC++でいうと、ソースは $(TT \dm\src\win32\dllstart.c) です)。
$(B printf)
の表示結果で、
どのように呼び出されるかがわかります。
古い DllMain のサンプルコードにあった初期化と終了処理のコードは、
このバージョンでも存在しています。
これは、同じDLLがCプログラムからでもDプログラムからでも使えるようにするためで、
同じ初期化プロセスがどちらの場合でも正しく動作するように作られています。
<p>
<dt>$(B MyDLL_Initialize)
<dd>
DLL が $(B Runtime.loadLibrary)() で動的にリンクされた場合、
ランタイムによって、Dプログラムに必要な初期化ステップがライブラリロード後に
実行されることが保証されます。
ライブラリが静的にリンクされた場合は、
プログラムからこの処理は呼び出されないため、
DLLを正しく動作させるためにいくつかの処理を自分で行う必要があります。
そして、静的リンクされているので、
DLLに固有の初期化処理関数が必要になります。
この関数は、呼び出し元のGCのハンドルを引数として取ります。
このハンドルを得る方法は後で説明します。
このハンドルをランタイムに渡してDLL組み込みのGCを上書きするには、
$(B gc_setProxy)() を呼び出します。
この関数は $(B export) されて、
DLLの外側から呼び出せるようになっています。
<p>
<dt>$(B MyDLL_Terminate)
<dd>対応して、この関数はアンロードの前に呼び出され、
DLLの終了処理を担当します。
具体的な処理はひとつだけで、DLLが呼び出し側のGCを使うことはもうないと
$(B gc_clrProxy)() で通知します。
このステップは重要です。この後DLLがメモリからマップ解除された場合、
仮にGCがDLLのメモリ領域をスキャンしようとすると、
セグメント違反が発生してしまいます。
<p>
<dt>$(B static this, static ~this)
<dd>モジュールの
静的コンストラクタと静的デストラクタの例です。
実行と実行タイミングの確認のために、
文字列を表示します。
<p>
<dt>$(B MyClass)
<dd>DLLからエクスポートされ、
呼び出し側から使えるようにするクラスの例です。$(B concat)
メンバ関数はGCによるメモリ割り当てを行い、$(B free)
はGCメモリの解放を行います。
<p>
<dt>$(B getMyClass)
<dd>$(B MyClass)のインスタンスを割り当て参照を返す factory
関数をエクスポートしています。
<p>
</dl>
$(TT mydll.dll) DLL をビルドするには:
$(OL
$(LI$(B $(TT dmd -c mydll -g))
<br>$(D mydll.d) を $(TT mydll.obj) へコンパイル。
$(B -g) でデバッグ情報を生成します。
)
$(LI $(B $(TT dmd mydll.obj mydll.def -g -L/map))
<br> $(TT mydll.obj) を $(TT mydll.dll) という名前のDLLへとリンクします。
$(TT mydll.def) は
$(LINK2 http://www.digitalmars.com/ctg/ctgDefFiles.html, モジュール定義ファイル)
で、以下のような内容を記述しておきます:
$(MODDEFFILE
LIBRARY MYDLL
DESCRIPTION 'MyDll demonstration DLL'
EXETYPE NT
CODE PRELOAD DISCARDABLE
DATA PRELOAD MULTIPLE
)
$(B -g) でデバッグ情報を生成し、
$(B -L/map) でマップファイル $(TT mydll.map) を生成します。
)
$(LI $(B $(TT implib /noi /system mydll.lib mydll.dll))
<br>静的に $(TT mydll.dll)
をロードするアプリケーションとのリンクに使う
$(LINK2 http://www.digitalmars.com/ctg/implib.html, インポートライブラリ)
$(TT mydll.lib)
を作ります。
)
)
$(P 以下に、$(TT mydll.dll) を使うサンプルアプリケーション $(TT test.d)
の例を示します。静的にDLLとリンクするバージョンと、
動的にロードするバージョンの2つが含まれています。
)
-------------------------------
import core.runtime;
import std.stdio;
import std.gc;
import mydll;
//version=DYNAMIC_LOAD;
version (DYNAMIC_LOAD)
{
import std.c.windows.windows;
alias MyClass function() getMyClass_fp;
int main()
{ HMODULE h;
FARPROC fp;
getMyClass_fp getMyClass;
MyClass c;
printf("Start Dynamic Link...\n");
h = cast(HMODULE) Runtime.loadLibrary("mydll.dll");
if (h is null)
{
printf("error loading mydll.dll\n");
return 1;
}
fp = GetProcAddress(h, "D5mydll10getMyClassFZC5mydll7MyClass");
if (fp is null)
{ printf("error loading symbol getMyClass()\n");
return 1;
}
getMyClass = cast(getMyClass_fp) fp;
c = (*getMyClass)();
foo(c);
if (!Runtime.unloadLibrary(h))
{ printf("error freeing mydll.dll\n");
return 1;
}
printf("End...\n");
return 0;
}
}
else
{ // DLLは静的リンク
extern (C)
{
void* gc_getProxy();
}
int main()
{
printf("Start Static Link...\n");
MyDLL_Initialize(gc_getProxy());
foo(getMyClass());
MyDLL_Terminate();
printf("End...\n");
return 0;
}
}
void foo(MyClass c)
{
char[] s;
s = c.concat("Hello", "world!");
writefln(s);
c.free(s);
delete c;
}
-------------------------------
$(P まず簡単な方の、静的リンクするバージョンから見ていきましょう。
次のようなコマンドでコンパイルとリンクを行います:
)
$(CONSOLE
C:>dmd test mydll.lib -g
)
$(P $(TT mydll.dll) のインポートライブラリ $(TT mydll.lib)
とリンクしています。
コードは簡単です。$(TT test.exe) のGCのハンドルを渡して
$(B MyDLL_Initialize)() を呼び出し、
$(TT mydll.lib) を初期化しています。
その後はDLL内の関数を、
$(TT test.exe) の中にあるのと全く同様に使うことが可能です。$(B foo)()
では、GC によるメモリの割り当てと解放が $(TT test.exe) と $(TT mydll.dll) の双方で行われています。
DLLを使い終わった後は、
$(B MyDLL_Terminate)() で終了します。
)
$(P 実行結果は次のようになります:)
$(CONSOLE
C:>test
DLL_PROCESS_ATTACH
Start Static Link...
MyDLL_Initialize()
static this for mydll
Hello world!
MyDLL_Terminate()
static ~this for mydll
End...
C:>
)
$(P 動的リンクの方は準備が少し複雑です。
次のようなコマンドでコンパイルとリンクを行います:
)
$(CONSOLE
C:>dmd test -version=DYNAMIC_LOAD -g
)
$(P インポートライブラリ $(TT mydll.lib) は不要です。
DLLは、
$(B Runtime.loadLibrary)(),
の呼び出しでロードし、
エクスポートされた関数それぞれは、
$(B GetProcAddress)() の呼び出しで取得します。
$(B GetProcAddress)() に渡すための修飾名を簡単に得るには、
生成された $(TT mydll.map) ファイルの
$(B Export) 以下の部分からコピー&貼り付けという方法があります。
一度この作業が終わると、DLL内クラスのメンバ関数も、
$(TT test.exe) 内のものと同様に使うことができます。
終了時には、DLLを
$(B Runtime.unloadLibrary)() で解放します。
)
$(P 実行結果は次のようになります:)
$(CONSOLE
C:>test
Start Dynamic Link...
DLL_PROCESS_ATTACH
static this for mydll
Hello world!
static ~this for mydll
DLL_PROCESS_DETACH
End...
C:>
)
)
Macros:
TITLE=D で作る Win32 DLL
WIKI=DLLs
CATEGORY_HOWTOS=$0