[Delphi:91262] DLL でのコールバックについて

919 views
Skip to first unread message

Shinya Okano

unread,
Apr 4, 2009, 2:02:02 PM4/4/09
to Del...@ml.users.gr.jp
岡野です。

DLLとそれを呼び出すアプリケーションを書いていて、書き方がよくわからなかったので質問します。
やりたいことは、アプリ側(main)からDLLにコールバック関数を渡し、
DLL側では渡されたコールバック関数に、引数としてDLL側のコールバック関数を渡すというものです。
自分で書いてみたコードだと、コンパイルは通るのですが、実行しても「問題が発生したためプログラムを
終了します」とエラーが出てしまいます。
DLL側の実装は次の通りです。

library callable;

type
TGetCallback = procedure(PCallback: Pointer); stdcall;
PGetCallback = ^TGetCallback;

procedure Writer(A, B: Integer); stdcall;
begin
WriteLn(A, B);
end;

procedure Call(GetCallback: PGetCallback); stdcall;
begin
GetCallback^(@Writer);
end;

exports Call;

begin
end.

DLLここまで。
アプリケーション側は以下の通りです。

program main;

{$APPTYPE CONSOLE}

type
TGetCallback = procedure(PCallback: Pointer); stdcall;
PGetCallback = ^TGetCallback;
TWriter = procedure(A, B: Integer); stdcall;
PWriter = ^TWriter;

procedure Call(GetCallback: PGetCallback); stdcall; external 'callable.dll';

procedure Callback(Writer: PWriter); stdcall;
begin
Writer^(100, 200);
end;

begin
Call(@Callback);
end.

アプリケーションここまで。
実行した環境はWindowsXP SP3でDelphi2009です。

----------------------------------------
Shinya Okano
email: xxs...@yahoo.co.jp
web: http://tokibito.orz.hm/

高木太郎

unread,
Apr 5, 2009, 9:58:49 PM4/5/09
to Del...@ml.users.gr.jp
 こんにちは、イマジオムの高木です。

岡野さん:
> やりたいことは、アプリ側(main)からDLLにコールバック関数を渡し、
> DLL側では渡されたコールバック関数に、引数としてDLL側のコールバック
> 関数を渡すというものです。

 DLLで用意した関数(手続き)のポインタをコールバックで
アプリケーションに渡し、アプリケーションから呼び出したいという
ことですね。

 手続き型(関数ポインタ)を LongWord 型か何かにキャストし、
受け取った側で再び手続き型にキャストし直せば、問題なく動作すると
思いますが……

■DLL側

type TCallBackProc=procedure(DLLProcAddr:LongWord); stdcall;

procedure DLLProc; stdcall;
begin
{<====== 処理A }
end;

procedure ExportProc(CalBackProcAddr:LongWord); stdcall;
begin
TCallBackProc(CalBackProcAddr)(LongWord(@DLLProc));
end;

■アプリケーション側

type TDLLProc=procedure; stdcall;

procedure CalBackProc(DLLProcAddr:LongWord); stdcall;
begin
TDLLProc(DLLProcAddr);
end;

procedure ExportProc(CalBackProcAddr:LongWord); stdcall; external...

begin
ExportProc(LongWord(@DLLProc));
end;
――――――――――――――――――――――――――――――――――――
株式会社イマジオム 代表取締役 高木太郎
〒316-0024 茨城県 日立市 水木町 1-11-10
電話:0294-28-0147
ファクシミリ:0294-28-0148
電子メール:tarou_...@imageom.co.jp
ホームページ:http://www.imageom.co.jp/

中村拓男

unread,
Apr 5, 2009, 9:58:48 PM4/5/09
to Del...@ml.users.gr.jp
中村@ブレーンです。

ひょっとしてこんなかんじのことをやりたいのでしょうか?

DLL:
----------
library callable;

type
TCallBack = procedure (A, B: Integer); stdcall;


procedure Call(Callback: TCallBack; A,B: Integer); stdcall;
begin
CallBack(A, B);
end;

procedure Writer(A, B: Integer); stdcall;
begin
WriteLn(A, B);
end;

exports Call, Writer;

begin
end.
----------

アプリ:
----------
program main;

{$APPTYPE CONSOLE}

type
TCallBack = procedure (A, B: Integer); stdcall;

procedure Call(Callback: TCallBack; A, B: Integer); stdcall; external 'callable';
procedure Writer(A, B: Integer); stdcall; external 'callable';

begin
Call(Writer, 100, 200);
// Call(@Writer, 100, 200); でもよい。
end.
----------

岡野さんのコードの問題点

1) 手続き型 は既にポインタ型です。手続き型へのポインタ型を定義する必要はありません。
#定義してもよいですが意図するところではないと思います。
2) 手続きを Pointer型で渡す必要はありません。型チェックがなくなるのでコンパイルエラーで
問題をチェックできなくなります。
3) DLL の Writer が exports されていません。これでは呼び出すことは不可能です。
4) アプリ側の Writer には Callback が渡ってしまっています。DLL の Writer が渡されていません。
#これが根本的な誤りです。

その他:

5) 手続き型には手続きがそのまま代入できます。この際手続きの呼び出しは起こりません。
手続きへのポインタが代入されます。@付きで渡しても同じことになります。
紛らわしければ @付きで渡してもよいと思います。

Shinya Okano さんは書きました:

----------
東京都 日野市 中村拓男

Shinya Okano

unread,
Apr 5, 2009, 10:28:14 PM4/5/09
to Del...@ml.users.gr.jp
岡野です。

中村さん、高木さんありがとうございます。
以下のコードで目的の動作を実現することができました。

DLL側
library callable;

type


TWriter = procedure(A, B: Integer); stdcall;

TCallable = procedure(Writer: TWriter); stdcall;

procedure Writer(A, B: Integer); stdcall;
begin
WriteLn(A, B);
end;

procedure Call(Callback: TCallable); stdcall;
begin
Callback(Writer);
end;

exports Call;

begin
end.

アプリ側
program main;

{$APPTYPE CONSOLE}

type


TWriter = procedure(A, B: Integer); stdcall;

TCallable = procedure(Writer: TWriter); stdcall;

procedure Call(param: TCallable); stdcall; external 'callable';

procedure Callable(Writer: TWriter); stdcall;
begin
Writer(100, 200);
end;

begin
Call(Callable);
end.

実際にはDLL側はCで実装されているのですが、DelphiでDLLを作成する時に参考にさせていただきます。
ありがとうございました。

Shinya Okano

unread,
Apr 6, 2009, 10:07:57 PM4/6/09
to Del...@ml.users.gr.jp
岡野です。

以前投稿したのとは別の問題になるのですが、関連しているのでreplyで投稿します。

手続き/関数をコールバックで渡して、それをCやDelphiで書いたDLLから実行するように書くのはできるの
ですが、同様にインスタンスのメソッドをDLLに渡して実行してもらうことはできるでしょうか?

試しにDelphiで書いたDLLではコールバックを"of
object"にすることで呼び出すことができたのですが、Delphi
以外で書かれたDLL(主にC言語)の場合、こういったことは可能でしょうか。

現在とあるCで書かれたDLLのラッパーをDelphiで書いていて、DLLのコールバックとラッパークラスをきれ
いに繋げたいと思い試行錯誤しています。DLL側のコードを変更することはできないです。C#で書かれたラ
ッパーが既にあり、そちらではdeligateを使ってインスタンスメソッドを渡していました。

以下試したコードです。
DLL側
library callable;

type
TWriter = procedure(A: Integer); stdcall;
TCallable = procedure(Writer: TWriter) of object; stdcall; // ここのof objectをなくしたい

procedure Writer(A: Integer); stdcall;
begin
WriteLn(A);
end;

procedure Call(Callback: TCallable); stdcall;
begin
Callback(Writer);

end;

exports Call;

begin
end.

アプリ側
program main;

{$APPTYPE CONSOLE}

type
TWriter = procedure(A: Integer); stdcall;
TCallable = procedure(Writer: TWriter) of object; stdcall;
TMethodHolder = class
private
FValue: Integer;
public
procedure Callable(Writer: TWriter); stdcall;
property Value: Integer read FValue write FValue;
end;

procedure Call(Callback: TCallable); stdcall; external 'callable';

procedure TMethodHolder.Callable(Writer: TWriter);
begin
Writer(Self.Value);
end;

var
holder1, holder2: TMethodHolder;

begin
holder1 := TMethodHolder.Create;
holder1.Value := 100;
holder2 := TMethodHolder.Create;
holder2.Value := 200;
Call(holder1.Callable);
Call(holder2.Callable);
end.

以上です。よろしくお願いします。

高木太郎

unread,
Apr 6, 2009, 10:36:21 PM4/6/09
to Del...@ml.users.gr.jp
 こんにちは、イマジオムの高木です。

岡野さん:
> インスタンスのメソッドをDLLに渡して実行してもらうことは
> できるでしょうか?

 オブジェクトのメソッドをコールバックで実行させたいということですね?

 同じプロセスを構成するのであれば(1)エントリアドレス、(2)呼び
出し規約、(3)引数リストがわかれば、どのモジュール(アプリケーション/
DLL)からでも相互に呼び出しが可能です。 メソッドの場合、TMethod
型でメソッドをキャストすれば(1)エントリアドレスは取れますし、それを
DLLに渡すこともできます。 (2)呼び出し規約と(3)引数リストを
DLLに合わせることもできるでしょうから、理屈の上ではできるはずです。

 しかしコードが複雑になりがちですから、一般的にはグローバルなコール
バック関数/手続きを一つ用意し、それからオブジェクトのメソッドを呼び
出した方がスッキリしそうな気がします。

中村拓男

unread,
Apr 7, 2009, 4:44:05 AM4/7/09
to Del...@ml.users.gr.jp
中村@ブレーンです。

Shinya Okano さんは書きました:


>以前投稿したのとは別の問題になるのですが、関連しているのでreplyで投稿します。
>
>手続き/関数をコールバックで渡して、それをCやDelphiで書いたDLLから実行するように書くのはできるの
>ですが、同様にインスタンスのメソッドをDLLに渡して実行してもらうことはできるでしょうか?

いろいろと手段はあると思いますが...

1) コールバックを呼び出す手続きがポインタ引数をひとつ余分に受け取れるならば

type
TSimpleMethod = procedure of Object; stdcall;
PSimpleMethod = ^TSimpleMethod;

procedure CallMethod(pMethod: PSimpleMethod); stdcall;
begin
pMethod^();
end;

なんてのを作っておいて

var
Method: TSimpleMethod;
pMethod: PSimpleMethod;
  :
:
Method := インスタンス.メソッド;
pMethod := @@Method;

  として、CallMethod と pMethod を渡せば呼び出せるはずです。

#普通コールバック関数はポインタを一個受け取るように作ると思います。

2) コールバックを呼び出す手続きがあくまで procedure 型(引数無し)しか受け取らないならば、サンクを作ればなんとか
なりますが、大変なのでお勧めしません(^^;

こーんなユニットを書いておいて(引数無し、戻り値無しで stdcallのメソッド専用 (^^; )

----------
unit ThunkU;

interface


function CreateThunk(Method: TMethod): Pointer;
procedure ReleaseThunk(Thunk: Pointer);


implementation

uses Windows;

type
TThunkBlock = packed record
MovEAXCode: Byte;
EAXValue: Pointer;
PushEAXCode:Byte;
CALLCode: Byte;
CALLADDR: LongInt;
RETCode: Byte;
end;

PThunkBlock = ^TThunkBlock;

function CreateThunk(Method: TMethod): Pointer;
begin
Result := VirtualAlloc(nil, 4096, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
PThunkBlock(Result).MovEAXCode := $B8; // mov eax, 即値
PThunkBlock(Result).EAXValue := Method.Data; // Address of Object
PThunkBlock(Result).PushEAXCode := $50; // push EAX
PThunkBlock(Result).CALLCode := $E8; // call
PThunkBlock(Result).CALLADDR :=
LongInt(Method.Code) - LongInt(@PThunkBlock(Result).CALLADDR) - 4;
PThunkBlock(Result).RetCode := $C3; // ret
end;

procedure ReleaseThunk(Thunk: Pointer);
begin
VirtualFree(Thunk, 4096, MEM_DECOMMIT or MEM_RELEASE);
end;
----------

サンクの使い方
type
TSimpleMethodPointer = procedure of Object; stdcall;
var
MethodPointer: TSimpleMethodPointer;
MethodInfo: TMethod;
Thunk: Pointer;
begin
MethodPointer := インスタンス.メソッド;
MethodInfo := TMethod(MethodPointer);
Thunk := CreateThunk(MethodInfo); // これを手続きポインタとして渡す。

:
:

// 使い終わったら
ReleaseThunk(Thunk);

----------
東京都 日野市 中村拓男

Shinya Okano

unread,
Apr 7, 2009, 9:46:24 PM4/7/09
to Del...@ml.users.gr.jp
岡野です。

中村さん、高木さんありがとうございます。

予想はしていましたが、やっぱり難しいのですね。
サンクを使う方法も厳しいので、今回は別の方法を取ろうと思います。
ありがとうございました。

Reply all
Reply to author
Forward
0 new messages