일반적으로, DLL에 포함된 루틴을 호출하는 방법은 정적 로딩(Static Loading)과 동적 로딩(Dynamic Loading)의 두 가지 방법이 있습니다.
정적 로딩이란 DLL 내의 루틴을 호출하기 위해 해당 루틴의 임포트 선언을 하고 사용하는 방법이죠. 아래와 같이 임포트 선언을 하게 되면 해당 DLL은 현재 프로그램이 실행되는 즉시 프로세스로 로딩됩니다.
1 |
procedure ProcInDll; external 'DLL입니다용.dll'; |
이 방법의 좋은 점은, 코드가 아주 간단하고, 이렇게 dll의 루틴들을 모두 모아 라이브러리 목적의 유닛으로 만들어두면, 해당 유닛을 uses하기만 하면 마치 메인 프로그램 소스 내의 함수인 것처럼 편하게 호출할 수 있다는 거죠. 실제로 VCL의 Windows.pas나 ShellAPI.pas처럼 Win32를 그대로 번역한 유닛들이 이런 파일이어서, 우리는 델파이에서 Win32 API를 껌씹듯이 쉽게 호출해댈 수 있습니다.
하지만 이 정적 로딩 방법에는 중요한 단점이 하나 있는데… 프로그램이 시작될 때 해당 DLL을 로드하기 때문에, 만약 해당 DLL이 없거나 DLL 안에 해당 루틴이 없을 경우 프로그램이 아예 시작되지를 못한다는 것입니다. (이런 경우를 많이 접해보셨을 겁니다) 또, 프로그램이 시작될 때 DLL을 몽땅 다 로드해버리기 때문에 당장 쓰지도 않을(경우에 따라서는 아예 호출하지도 않을 수도 있는) 루틴들을 위해 무시할 수는 없을 정도의 메모리가 소요된다는 거죠.
DLL의 동적 로딩이란, 프로그램이 시작되는 동안이 아닌, 프로그램의 코드 내에서 LoadLibrary/FreeLibrary와 GetProcAddress 등의 함수를 이용해서 코드로 일일이 DLL을 로드하고, 그 DLL에서 해당 함수의 주소를 찾고, 그 주소를 함수 포인터로 해서 해당 함수를 호출하는 절차입니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
TProcInDll = procedure; var Handle: THandle; AProcInDll: TProcInDll; begin Handle := LoadLibrary('DLL입니다용.dll'); if Handle<>0 then begin @AProcInDll := GetProcAddress(Handle, 'ProcInDll'); if @AProcInDll<>nil then AProcInDll; end; FreeLibrary(Handle); end; |
정적 로딩 방법에 비해 동적 로딩 방법의 단점은, 위 코드에서 바로 눈에 보이시죠. 코드가 복잡하고 까다롭다는 겁니다. 대신, 해당 DLL이 없거나 DLL 안의 루틴이 없더라도 당장 프로그램이 아예 뜨지도 않는 경우는 발생하지 않는다는 거죠. 물론 기대했던 DLL이 없다면 난감하겠지만, 위의 코드에서처럼 에러 체크를 통해 적어도 뭔가 대책을 세울 수 있는 가능성이 생깁니다. 또, 필요한 경우에만 DLL을 로드하기 때문에 메모리도 적게 먹습니다.
정적 로딩과 동적 로딩 사이에 장단점이 너무나 명확하죠. 그러면… 동적 로딩과 정적 로딩 사이에서 장점만 취할 수 있는 방법은 없을까요? 정적 로딩에서처럼 간단하게 임포트 선언만 하면서도, 프로그램 실행시에 무조건 바로 DLL을 로드하지는 않고 실제로 필요할 때 DLL을 불러들이면 되겠죠.
별로 알려지지 않았지만, 델파이 2010에서 바로 이 기능이 추가되었습니다. 지연 로딩(Delayed Loading)이라는 것으로, delayed라는 지시어가 추가되어 가능해졌습니다. 위의 정적 로딩에서 임포트 선언의 뒤에 delayed만 추가하면 된다는 거죠.
1 |
procedure ProcInDll; external 'DLL입니다용.dll' delayed; |
정적 로딩 방식과 똑같은 임포트 선언에 delayed 지시어가 추가되었을 뿐입니다. 이렇게 delayed를 추가해주면, 프로그램 시작시에 바로 로딩되지 않고 실제로 해당 프로시저/함수가 호출될 때 비로소 DLL을 로드하고 루틴을 임포트합니다. 그래서 지연 로딩, delayed인 거죠.
당장 델파이 2010의 Windows.pas나 ShellAPI.pas 등등에는 2009 버전까지는 보이지 않았던 수없이 많은 함수들이 이 delayed 지시어와 함께 임포트 선언으로 추가되어 있는데요. 이 함수들은 윈도우 7, 윈도우 비스타, 윈도우 XP 등 특정 버전 이상에서 추가된 함수들로서, 그보다 낮은 윈도우 버전에서는 존재하지 않았던 함수들입니다. 그래서 API 지원이 훨씬 풍부해졌죠.
그러면, 해당 DLL이나 함수가 존재하지 않을 경우, 어떻게 대응을 해야 할까요? 해당 DLL이나 함수가 존재하지 않아 불러들일 수 없는 경우 EExternalException 예외가 발생합니다. 따라서, 동적 로딩에서 리턴 값을 체크해서 함수의 존재 여부에 따라 처리하는 것과 마찬가지로 try except 블럭으로 EExternalException 예외를 처리하면 깔끔하게 되겠죠.
그렇다고 동적 로딩을 사용하는 모든 경우를 지연 로딩으로 커버할 수는 물론 없습니다. 지연 로딩에서 프로그램 시작 이후 DLL 함수가 호출될 때까지는 DLL이 메모리에 로드되지 않지만, 일단 호출된 후에는 해당 DLL이 언로드되지 않고 계속 메모리에 머무르게 됩니다. 극단적으로 효율적인 메모리 운영이 필요하다면 언제든 DLL을 언로드할 수 있는 동적 로딩을 따라갈 수는 없죠.
또, 동적 로딩의 경우에는 DLL 이름과 함수 이름을 문자열로 취급하므로 메인 프로그램이 사전에 알지 못하는 DLL이나 함수도 호출이 가능하지만 정적 로딩과 지연 로딩에서는 컴파일 시점에 고정되는 이름만 지원할 수 있으므로, 업무 개발 프로젝트에서처럼 사전에 프로그램에서 알 수 없는 많은 DLL들을 쓰는 경우를 지원할 수 없게 됩니다.
성능 면에서는 어떨까요. delayed를 불필요하게 너무 남발하면 정적 로딩에 비해 프로그램의 성능을 떨어뜨릴 수도 있습니다. 반면 한번 호출되어 로딩된 DLL은 메모리에 계속 상주하게 되므로 이런 면에서는 매번 LoadLibrary, GetProcAddress를 호출해야 하는 동적 로딩보다는 속도가 빠르게 되죠. 결국 개발자가 정적 로딩, 지연 로딩, 동적 로딩을 적절히 잘 배합해서 사용하는 것이 좋습니다. 실제 VCL 코드에서도 이 세가지가 잘 배합되어 사용되고 있죠.
지연 로딩에 대한 좀 더 자세한 내용은, 엠바카데로의 앨런 바우어의 블로그에서 보실 수 있습니다.
http://blogs.embarcadero.com/abauer/2009/08/25/38894
http://blogs.embarcadero.com/abauer/2009/08/29/38896
http://blogs.embarcadero.com/chrishesik/2009/11/02/35056
아래 JEDI 블로그에서는 조금 더 테크니컬한 내용들을 보실 수 있습니다.
http://blog.delphi-jedi.net/2009/08/29/version-checking-for-delphi/
오~~!
이런 기능이 추가되었군요
그런데 C++Builder에서도 가능한가요?
pas유닛 하나 Builder프로젝트에 추가해서 거기에다가 함수선언하면 쉽게되려나?
오 좋네요
와우~~~ 좋은 기능인데요~~ ^^
오 좋은 개념하나 배우고 갑니다.^^