./index.html ../index.html

Adaでdllを作る&使うぞ やめときゃいいのに…

作る

コンソールアプリは普通にgnatmake、GUIアプリはpragma Linker_Options("-mwindows")、では、dllは作れるのか?という挑戦です。

最初は、単純に、pragma Linker_Options("-mdll")だけでできると思っていました。 しかし、そうやってgnatmakeを繰り返しても、いくらやっても壊れた?dllしかできない様子なので方針変更。

test_dll.oは、dllとして公開する関数を書いたpackageをコンパイルしたものです。 (一応説明しておくと、.oは、gcc版.objファイル)

...>gcc -mdll -o xxx.dll test_dll.o

...>impdef a.def test_dll.dll

Borland Impdef Version 3.0.22 Copyright (c) 1991, 2000 Inprise Corporation
Warning test_dll.dll: no exports

ちゃんとしたdllっぽいファイルはできたのですが、pragma Exportは明記しているのに、関数をエクスポートしてくれない…。

...>gcc -shared -o xxx.dll test_dll.o

おお、今度は何かエクスポートされてるぞ。

...>impdef a.def test_dll.dll

Borland Impdef Version 3.0.22 Copyright (c) 1991, 2000 Inprise Corporation

...>type a.def
LIBRARY     TEST_DLL.DLL

EXPORTS
    aaa                           = aaa@0
    test_dll_E                     @2   ; test_dll_E

次に、pragma Exportpragma Export_Functionかで結構な時間悩んだのですが…。
前者だと、「関数名@数字」が避けられないみたいだけれど、後者だと呼びだし規約が変えられない…。 pragma Conventionで別途規約を指定すると、後者でも「関数名@数字」になってしまう…。

…なんて悩んだのも実は馬鹿らしい話で、-sharedというのはシェアードライブラリを作るオプション。 シェアードライブラリというのはよくわからないのですが、.libの.dll版みたいなもの(Delphiのパッケージみたいなものですか?)で、明示しようがしまいが、ソース上の全関数がエクスポートされてしまうのでした。
呼びだし.exe書いて動作確認までしちゃったじゃないか…。

やはり、-mdllで、何とかして動作させるしかないですね。

…ということで、.defファイルを書く、という古典的解決方法になりました。

LIBRARY TEST_DLL.DLL
EXPORTS
  aaa = aaa
...>gcc -mdll -o test_dll.dll test_dll.o test_dll.def
Warning: resolving _aaa by linking to _aaa@0
Use --enable-stdcall-fixup to disable these warnings
Use --disable-stdcall-fixup to disable these fixups

...>impdef a.def test_dll.dll

Borland Impdef Version 3.0.22 Copyright (c) 1991, 2000 Inprise Corporation

...>type a.def
LIBRARY     TEST_DLL.DLL

EXPORTS
    aaa                            @1   ; aaa

警告はよくわからないので無視するとして(←おい)、数字でエクスポートされてるみたいではありますが、大丈夫かな?
やってみよ。

program Caller;

{$APPTYPE CONSOLE}

uses
  SysUtils;

  procedure exec; stdcall; external 'test_dll.dll' name 'aaa';

begin
  exec
end.

呼びだし側もAdaを期待してた人はごめんなさい。 Delphiだとインポートライブラリ不要なので楽ですもの。
で、無事、エラーも何も起きない事を確認。(aaaの中は空なので)

やったか!?やったー。

よし、調子に乗って、.dll中で何かやってみましょう。

package body test_dll is

  procedure aaa is
  begin
    Ada.Text_IO.Put("delodelo");
  end aaa;

end test_dll;
...>gcc -c test_dll.adb

...>gcc -mdll -o test_dll.dll test_dll.o test_dll.def
Warning: resolving _aaa by linking to _aaa@0
Use --enable-stdcall-fixup to disable these warnings
Use --disable-stdcall-fixup to disable these fixups
test_dll.o(.text+0x26):test_dll.adb: undefined reference to `ada__text_io__put$4
'

…標準ライブラリが見つからないみたい。
ならば無理やり教えてやるまで。

...>gcc -mdll -o test_dll.dll test_dll.o test_dll.def ...\MinGW\lib\gcc-lib\ming
w32\3.2\adalib\libgnat.a
Warning: resolving _aaa by linking to _aaa@0
Use --enable-stdcall-fixup to disable these warnings
Use --disable-stdcall-fixup to disable these fixups

よしGO!

...>caller
Runtime error 216 at 0023BFDB

あれー?{$APPTYPE CONSOLE}は書いたし…(←最初マジ忘れてました)
例外処理を有効にして、詳細なエラー報告をもらう事にしましょう。 というわけでuses SysUtilsを追加。

...>caller
EAccessViolation がモジュール test_dll.dll の 0000BFDB で発生しました。
モジュール 'test_dll.dll' のアドレス 0023BFDB でアドレス 00000000 に対する読み込み違
反がおきました。.

…。

中身が空のときは動いたので、dllが壊れているとかはなさそうですし、引数も戻り値も無いので、スタックのやりとりが変という事もないでしょうし… 考えられるとして、ランタイムのスタートアップコードが走って無い、ぐらいですか。

これはネットに頼るしか無いですね…というわけで、検索したら、あっさり発見

有償版のObjectAdaならdllも変わりなく作れるらしいです。 (実はObjectAdaの試用版は、数年前Ada初挑戦時に使ったことがあるのですが、IDEが使いにくかった&よく落ちた記憶が…金出すなら入力支援&デバッガ付きのIDEは当然ですよね?)

GNUツールのバグがどうたらで、Win9xでは動かないみたいなことが書かれてますが、Win9xなんてのは、そういえば昔、よく落ちるOSがあったなーってな認識なので、何一つ問題はありません。(こんな事書いてるとまた画伯に何か言われそうだ)

サンプルを拾ってきて、make.batを実行しました。

...>make
Build the Ada dll
`-mpentium' is deprecated. Use `-march=pentium' or `-mcpu=pentium' instead.
...\b_adadll.c が見つかりませんでした。
...\b_adadll.o が見つかりませんでした。
Build the import library
'lib' は、内部コマンドまたは外部コマンド、
操作可能なプログラムまたはバッチ ファイルとして認識されていません。
...\adadll.exp が見つかりませんでした。
build the client program
Microsoft(R) 32-bit C/C++ Standard Compiler Version 13.00.9466 for 80x86
Copyright (C) Microsoft Corporation 1984-2001. All rights reserved.

cl : コマンド ライン warning D4029 : 標準の編集コンパイラでは最適化は使用できま
せん。

ファイルが見つからない、というのは、GNATのバージョン違いのせいか、make.bat中で消そうとしている中間ファイルと、実際に生成されている中間ファイルの名前が違うせいです。
libコマンドが無いのは、VC++を持って無いからに他なりません。clが一応起動しているのは、.NET Framework SDKのせいです。 (どっかで見かけたMLコンパイラもそうですが、VC++がインストールされている事が前提になっている場面が多い気がする…そんなにBorland使いって冷遇されてるワケ…?って当のMSすらDirectX8からSDKにBorland用インポートライブラリ付けなくなったぐらいなので、そうなのかも、というより、僕がWin9xなんて忘れたと言ってるのと同じようなものかも)

dllそのものはできたっぽいので、Borland C++でGO! (gcc使えと言われそうな気もするが…)

...>implib -a a.lib adadll.dll

Borland Implib Version 3.0.22 Copyright (c) 1991, 2000 Inprise Corporation

...>bcc32 client.cpp a.lib
Borland C++ 5.5.1 for Win32 Copyright (c) 1993, 2000 Borland
client.cpp:
Turbo Incremental Link 5.00 Copyright (c) 1997, 2000 Borland

...>client
Calling 'Junk' in Ada Dll
Excuting procedure 'Junk' from Dll
Calling 'Junk_2' in Ada Dll, should return 42
Now excuting function 'Junk_2' from Dll
Junk_2 returned 42
Calling 'Junk_3' in Ada Dll with 50, should return 100
function 'Junk_3' in Dll recieved the value: 50
Junk_3 returned 100

おー、動いてます動いてます。

しかし、同じ手順でもってtest_dll.dllを作り直しても、動かなかったので、理由を知るためadadll.adsを覗いてみると…。

何の事は無く、DllMainが書かれていてDLL_PROCESS_ATTACHでAdaInit、DLL_PROCESS_DETACHでAdaFinalが呼ばれていました。
というわけでその辺のコードをごっそりコピー。
転載したらまずいかな。先のリンクから持ってきて頂戴。 要約だけ書いちゃうと、

  function DllMain(hInstance: HINSTANCE; Reason: ULONG; Reserved: LPVOID) return BOOL;
  pragma Export(stdcall, DllMain, "DllMain");
  procedure AdaInit;
  pragma Import(C, AdaInit, "adainit");
  procedure AdaFinal;
  pragma Import(C, AdaFinal, "adafinal");
  function DllMain(hInstance: HINSTANCE; Reason: ULONG; Reserved: LPVOID) return BOOL is
  begin
    case Reason is
    when DLL_PROCESS_ATTACH => AdaInit;
    when DLL_PROCESS_DETACH => AdaFinal;
    end case;
    return 1; --TRUE
  end DllMain;

make.batの先頭の実際にdllを作っている部分もコピーして、adadllをtest_dllに置換して実行。

gnatbind -n -x xx.ali
gnatlink -g -mdll -s -o xx.dll xx.ali -Xlinker --base-file=xx.base
dlltool --dllname xx.dll --base-file xx.base --output-exp xx.exp --def xx.def
gnatlink -g -mdll -s -o xx.dll xx.ali -Xlinker --base-file=xx.base xx.exp
dlltool --dllname xx.dll --base-file xx.base --output-exp xx.exp --def xx.def
gnatlink -g -mdll -s -o xx.dll xx.ali -Xlinker xx.exp
...>test_make
Warning: resolving _aaa by linking to _aaa@0
Use --enable-stdcall-fixup to disable these warnings
Use --disable-stdcall-fixup to disable these fixups
Warning: resolving _aaa by linking to _aaa@0
Use --enable-stdcall-fixup to disable these warnings
Use --disable-stdcall-fixup to disable these fixups
...>caller
delodelo

動いたあ…。結局最後まで警告は消えてくれませんでしたけどね。 stdcallに関係があるのかな、と予想はしてますけど。

おまけ。普通にビルドできないか試してみました。結局ダメでしたけどね。

...>gcc -mdll -o test_dll.dll test_dll.o test_dll.def ...\MinGW\lib\gcc-lib\ming
w32\3.2\adalib\libgnat.a
Warning: resolving _aaa by linking to _aaa@0
Use --enable-stdcall-fixup to disable these warnings
Use --disable-stdcall-fixup to disable these fixups
test_dll.o(.text+0x15):test_dll.adb: undefined reference to `adainit'
test_dll.o(.text+0x1c):test_dll.adb: undefined reference to `adafinal'

adainitですが、BGREPしても見つかりませんでした。 コンパイル時に初めて生成されているようですねえ。
gnatbindが作る中間ファイルの中に登場するようなので、それも渡す事にして、挑戦。

D:\Programming\Tests\Ada\dll>gcc -mdll -o test_dll.dll b~test_dll.o test_dll.o c
:\progra~1\mingw\lib\gcc-lib\mingw32\3.2\adalib\libgnat.a

ビルドは成功しましたが…結果を書きますと、caller.exeを起動した時点で、アプリケーションを正しく初期化できませんでしたというダイアログボックスが(コンソールアプリの筈なのに何故かダイアログボックスが)出て、おしまいです。

結局、make.batが最終回答という事ですか。

なんか、Adaそのものよりも、gcc特有の、それも末端の要らない知識ばかり身についてきた気がします…。




む?gnatdll?



何だろう?検索でもしてみるか



うぉおー
これが吠えずにいられるか!
俺の苦労を返せー!!

...>gnatdll -d test_dll.dll test_dll.ali
Building relocatable DLL...
make test_dll.dll and libtest_dll.a
Warning: resolving _aaa by linking to _aaa@0
Use --enable-stdcall-fixup to disable these warnings
Use --disable-stdcall-fixup to disable these fixups
Warning: resolving _aaa by linking to _aaa@0
Use --enable-stdcall-fixup to disable these warnings
Use --disable-stdcall-fixup to disable these fixups

上記手順を自動的にやってくれる便利ツールです。こんなものがあったんです。
この例では出力.dll名とソースとなる.aliファイルしか渡してませんが、同名の.defファイルも自動的に使われているようです。

なお、gnatdllなんて名前であるからには、adainit/adafinalを呼ぶDllMainを自動で吐いてくれるかな、と思って試しましたが、ダメでした。

使う

.dllをgnatで使うには、gcc形式のインポートライブラリが必要です。

勿論gcc用のGNUツールを用いてもいいのでしょうが、上記gnatdllでもできました。
.defファイルと.dllからlibxx.aを吐いてくれます。

...>gnatdll -e test_dll.def -d test_dll.dll
Building import library...
make libtest_dll.a to use dynamic library test_dll.dll

.dll→.defは先のBorland C++ Compilerのimpdefで可能ですから、必要なものは全部揃っている、ということになります。

後はヘッダー(.ads)を書くだけです。 普通に関数宣言して、直後にpragma Import(stdcall, Func_Name, "External_Name")、それだけ。Delphiでexternal~を書くのとほぼ一緒です。

後は、コンパイル/リンク時に(pragma Linker_Optionsでも使って)、リンカに、libxx.aのファイル名を伝えてやればOKです。

…と思ったら、後ろに @4 とか付いているC++な形式じゃないと受け付けてくれない…。

impdefが生成してくれた.defファイルに手動で@なんちゃらを書き足し、それだけではdllの側と食い違うので、さらにgnatdllに-kを渡すと上手くいくようです。