Q&A게시판에
BCB로 만든 DLL을 VC++에서 사용할때
DLL에서 TThread의 Synchronize함수를 쓸때의 문제점에 대해
질문에 답해주고 자세한 설명을 팁/강좌 게시판에 올린다고 했는데요..
http://cbuilder.borlandforum.com/impboard/impboard.dll?action=read&db=bcb_qna&no=60086
찾아보니 제 카페(
http://cafe.naver.com/bcbmaster/)에 이미 예전에 올려놓은 글이 있네요
http://cafe.naver.com/bcbmaster/1297
카페에 올린 글 원문을 그대로 copy-paste합니다.
//-----------------------------------------------------------------------------------------
가끔 모듈을 분리하기 위해 Dll로 만드는 경우가 있다.
Dll에서 VCL을 사용할경우 주의할점이 여러가지가 있는데...
그중에 하나 Dll내의 Thread에서 Synchronize함수 사용시
Synchronize 함수가 제대로 동작하지 않는 경우에 대해 얘기하고자 한다.
오늘 델마당에 Dll에서 CPort사용시 RxChar 이벤트가 발생하지 않는다는 질문이 올라왔었다.
참조
http://www.delmadang.com/community/bbs_view.asp?bbsNo=17&bbsCat=0&st=&keyword=&indx=415729&keyword1=&keyword2=&page=1
관련하여 원인을 찾아본 결과
원인은 Dll내의 Thread에서 Synchronize함수에 있었다.
[Synchronize 함수 동작 원리]
먼저 Synchronize함수가 어떻게 동작하는지 살펴 보게습니다.
다음은 TThread의 Synchronize 함수입니다.
procedure TThread.Synchronize(Method: TThreadMethod);
class procedure TThread.Synchronize(ASyncRec: PSynchronizeRecord);
1 . 먼저 TThreadMethod를 파라메터로 받는 Synchronize 함수에서
SynchronizeRecord 구조체게 관련 parameter를 설정한후에
2. class procedure 인 TThread.Synchronize 를 호출합니다.
class procedure TThread.Synchronize(ASyncRec: PSynchronizeRecord);
var
SyncProc: TSyncProc;
begin
if GetCurrentThreadID = MainThreadID then
ASyncRec.FMethod
else
begin
{$IFDEF MSWINDOWS}
SyncProc.Signal := CreateEvent(nil, True, False, nil);
try
{$ENDIF}
{$IFDEF LINUX}
FillChar(SyncProc, SizeOf(SyncProc), 0); // This also initializes the cond_var
{$ENDIF}
EnterCriticalSection(ThreadLock);
try
if SyncList = nil then
SyncList := TList.Create;
SyncProc.SyncRec := ASyncRec;
SyncList.Add(@SyncProc);
SignalSyncEvent;
if Assigned(WakeMainThread) then
WakeMainThread(SyncProc.SyncRec.FThread); //여기서PostMessage(Application.Handle,WM_NULL...
{$IFDEF MSWINDOWS}
LeaveCriticalSection(ThreadLock);
try
WaitForSingleObject(SyncProc.Signal, INFINITE);
finally
EnterCriticalSection(ThreadLock);
end;
{$ENDIF}
{$IFDEF LINUX}
pthread_cond_wait(SyncProc.Signal, ThreadLock);
{$ENDIF}
finally
LeaveCriticalSection(ThreadLock);
end;
{$IFDEF MSWINDOWS}
finally
CloseHandle(SyncProc.Signal);
end;
{$ENDIF}
if Assigned(ASyncRec.FSynchronizeException) then raise ASyncRec.FSynchronizeException;
end;
end;
procedure TThread.Synchronize(Method: TThreadMethod);
begin
FSynchronize.FThread := Self;
FSynchronize.FSynchronizeException := nil;
FSynchronize.FMethod := Method;
Synchronize(@FSynchronize);
end;
3. 위 코드에 보면 WakeMainThread 함수에서
Application.Handle에 PostMessage로 WM_NULL을 보낸다.
WakeMainThread(SyncProc.SyncRec.FThread); //여기서PostMessage(Application.Handle,WM_NULL...
4. Application의 WindowProc에서 WM_NULL메세지를 받는 부분을 살펴보면...
아래는 TApplication.WndProc 함수 코드이다.
대분의 코드를 생략하고 WM_NULL 메세지가 올경우 CheckSynchronize 라는 함수를 호출하였다.
procedure TApplication.WndProc(var Message: TMessage);
....
begin
try
with Message do
case Msg of
.....
WM_NULL:
CheckSynchronize;
else
Default;
end;
except
HandleException(Self);
end;
end;
5. 다음은 CheckSynchronize 함수이다.
LocalSyncList 리스트에 있는 Synchronize 함수 정보를 하나씩 가져와서 실행하고 잇다.
실행하고 난뒤에 SetEvent(SyncProc.signal); 로 Synchronize를 호출한 Thread를 wake시켜 준다.
function CheckSynchronize(Timeout: Integer = 0): Boolean;
var
SyncProc: PSyncProc;
LocalSyncList: TList;
begin
if GetCurrentThreadID <> MainThreadID then
raise EThread.CreateResFmt(@SCheckSynchronizeError, [GetCurrentThreadID]);
if Timeout > 0 then
WaitForSyncEvent(Timeout)
else
ResetSyncEvent;
LocalSyncList := nil;
EnterCriticalSection(ThreadLock);
try
Integer(LocalSyncList) := InterlockedExchange(Integer(SyncList), Integer(LocalSyncList));
try
Result := (LocalSyncList <> nil) and (LocalSyncList.Count > 0);
if Result then
begin
while LocalSyncList.Count > 0 do
begin
SyncProc := LocalSyncList[0];
LocalSyncList.Delete(0);
LeaveCriticalSection(ThreadLock);
try
try
SyncProc.SyncRec.FMethod; //실제 Synchronize함수 실행
except
SyncProc.SyncRec.FSynchronizeException := AcquireExceptionObject;
end;
finally
EnterCriticalSection(ThreadLock);
end;
{$IFDEF MSWINDOWS}
SetEvent(SyncProc.signal);
{$ENDIF}
{$IFDEF LINUX}
pthread_cond_signal(SyncProc.Signal);
{$ENDIF}
end;
end;
finally
LocalSyncList.Free;
end;
finally
LeaveCriticalSection(ThreadLock);
end;
end;
-------------------------------------------------------------------------------------------------
[Dll에 쓰레드에서 Synchronize함수 사용의 문제 되는 경우는?]
dll에 있는 쓰레드에서 Synchronize함수 사용에 문제가 되는 경우가 어떤경우 일까?
그것은 다름아닌
첫째 . Delphi 또는 CBuilder로 만든 Dll을 같은 버젼의 Delphi나 CBuilder로 만든 exe에서 사용하지 않는경우
둘째 . Dll이나 Exe모두 같은버젼의 Delphi나 CBuilder로 만든 경우라 하더라도
컴파일시 Runtime-Package를 사용하지 않은 경우 등이 있다.
실례)
1) 델마당의 질문에서와 같이
CPort같은 serial통신 수신 이벤트에 문제가 생긴다.
2) 또는 delphi/cbuilder 의 기본 tcp/ip socket인
TServerSocket/TClientsocket의 수신이벤트에도 문제가 생긴다.
TServerSocket/TClientsocket 은 non-blocking소켓으로 data수신시 Synchronize 함수로
메인쓰레드에서 이벤트를 발생시키기 때문이다.
3) 기타 위와 비슷한경우는 이루 수도없이 많을것이다.
[문제의 원인]
이제 문제의 원인을 살펴보자
앞에 Synchronize의 동작원리를 자세히 설명한 이유도 여기에 있다.
위에 Synchronize의 동작원리를 보면
WakeMainThread함수에서 다음과 같이 Application.Handle로 WM_NULL 메세지를 PostMessage로 보낸다.
procedure TApplication.WakeMainThread(Sender: TObject);
begin
PostMessage(Handle, WM_NULL, 0, 0);
end;
그런데 DLL의 경우에 Application.Handle이 NULL 또는 nil 이라는데 있다.
다음은 TApplication클래스의 생성자 함수이다.
constructor TApplication.Create(AOwner: TComponent);
... 생략
begin
inherited Create(AOwner);
.... 생략
if not IsLibrary then CreateHandle;
... 생략
end;
위 코드르 보면 not IsLibrary 즉 DLL이 아닌 EXE인 경우에만 CreateHandle을 호출한다.
즉 Dll에서는 CreateHandle 함수가 호출되지 않고 Application.Handle이 NULL이다.
Application.Handle이 NULL이니 아무리 PostMessage로 보내도 메세지를 받은 Handle이 없으니
Synchronize 가 동작하지 않는 것이다.
[해결방법]
첫째 . dll 및 exe둘다 Runtime-Package 를 사용한다.
즉 Dll 및 exe둘다 runtime-package를 사용한다는 것은?
runtime-package를 사용하지 않으면
exe에서 사용하는 Application객체와 dll에서 사용하는 Application객체가 다른 놈이 된다.
exe/dll둘다 runtime-package를 사용한다는 뜻은
VCLxx.bpl 을 동적으로 LoadPakage하여서 dll과 exe에서 같은 vclxx.bpl을 사용하게 된다.
하지만 만약 둘중에 한쪽이라도 runtime-package를 사용하지 않게되면
둘중 한쪽은 static-link된 vcl -lib를 사용하게 되게 된다.
이경우 dll에 있는 쓰레드에서 Synchronize 문제 뿐만 아니라 매우 많은 문제를 야기 시킨다.
둘째 . Dll에서 Application->CreateHandle(); 을 명시적으로 호출해준다.
위에 설명했듯이 Application->Handle은 Application->CreateHandle(); 함수에서 생성된다.
명시적으로 Application->CreateHandle(); 을 호출해줄경우 PostMessage로 메세지를 보내고 받을
윈도우 Handle이 생성되므로 문제가 해결되는 것이다.
셋째. delphi나 CBuilder로만 project를 할경우 DLL 대시 BPL을 사용하는 방법도 있다.
BPL은 delphi에서 사용하는 dll로써 bpl로 모듈을 만들면 Runtim-package및 static-link 시에 알아서
링크해준다. 그래서 exe나 bpl에서 같은 vclxx.bpl 을 사용하게 되믄로 문제가 해결된다.
만약 Dll이 같은 버젼의 delphi나 CBuilder가 아닌 프로그램으로 만든 exe에서 사용할 목적이라면
두번째 방법 Application->CreateHandle();을 사용하길 권한다.
그럼...