류종택님의 ffmpeg 래퍼 모듈을 C++Builder 6에 붙일때
우선 DLL에 있는 함수를 써야 하고
별도로 제공된 델파이 소스도 활용해야 하는데, 이와 같은 경우 이를 맞추는 방법입니다.
그냥 류종택님이 공개하신 것을 그대로 예로 들어드리고 싶은데, 원저자의 허락을 미리 구해야 하는 문제가 있어
다른 명칭을 예로 삼겠습니다.
여기서는 방법만 이야기할 것이므로, 소스를 열거하지 않고 필요한 부분만 쓰겠습니다.
C++Builder에서 사용할 대상 파일은 다음과 같습니다.
libMPEG.dll ; 주요 기능 제공하는 델파이로 만들어진 DLL
libMpeg.pas ; DLL 내의 함수와 구조체 상수 등에 대한 서비스
VideoSvc.pas ; 관련 유닛, libMpeg.pas 유닛 참조.
AudioSvc.pas ; 관련 유닛
등 등..
우선 기능을 제공해주는 미리 만들어 놓은 DLL을 사용해야 하는 문제가 있는데,
여기에는 몇가지 함수가 export 되어 있다고 합시다.
이를 쓸려면 Implib 로 빌더용 .lib 파일을 우선 만들면 됩니다.
이렇게 안하면 동적으로 일일히 함수 주소를 찾아서 매핑해줘야 하는데, 함수가 여러개 되면
좀 귀잖은 관계로 그냥 .lib 파일을 만들어 프로젝트에 포함시키면 자동으로 DLL 로딩과 참조가 이루어지므로
특별한 이유가 없다면 보통 이렇게 사용하니 여기서도 이렇게 하겠습니다.
커맨드라인에서
> implib libMPEG libMPEG.dll
하면 libMPEG.lib 파일이 생성되므로 이를 프로젝트에 포함해주면 됩니다.
아, 그런데 여기서 명칭에 다소 문제가 있습니다.
이 DLL이 델파이에서 제작되었다고 하면 분명히 함수 명칭은 다음과 유사하게 되어 있을 것입니다.
function openMPEG(FileName:PAnsiChar):pointer; cdecl; external 'libMPEG.dll';
식으로 되어 있기 때문에, C++Builder에서 링크하려면 실제 라이브러리 내 명칭은
openMPEG 이 아니라 _openMPEG
즉 언더바 _ 가 선행되어야 합니다.
그래서
> implib -a libMPEG libMPEG.dll
로 라이브러리를 생성하도록 합니다. 이렇게 하면
lib 내에서는 앞에 언더바 _ 가 붙은 채로 명칭 참조를 하게됩니다.
그런데, 이렇게 하면 또 문제가 생깁니다.
이 함수를 사용하는 델파이 유닛 *.pas 파일이 있는데
이 파일이 그대로 C++Builder 프로젝트에 포함되어 있는데 이 유닛내에서 함수를 호출할때의 문제입니다.
unit MPEG_C;
interface
uses
Classes, SysUtils, Graphics;
TMPEGStream = class
private
FHandle : pointer;
public
constructor Create;
destructor Destroy; override;
function Open(AFileName:string):boolean;
procedure Close;
function ReadPacket:boolean;
end;
function openMPEG(FileName:PAnsiChar):pointer; cdecl; external 'libMPEG.dll';
implementation
function TMPEGStream.Open(AFileName: string): boolean;
begin
FHandle := openMPEG(PAnsiChar(AnsiString(AFileName)));
result := 0;
end;
이와 같이 유닛 자체는 문제가 없으나 최종 링크시
openMPEG
함수를 찾을 수 없다고 나옵니다.
그도 그럴 것이 델파이는 cdecl 규약이라 할지라도 함수명 앞에 언더바 _ 를 붙이지 않기 때문입니다.
이 유닛을 C++Builder에서
#include "libMPEG.hpp"
로 포함해서
openMPEG
함수를 사용해도 문제 없으나, 최종 링크에서 델파이 모듈 쪽에서 openMPEG 함수를 링크할 수 없다고
에러를 내는 것입니다.
그러면 어떻게 C++Builder 쪽도 만족시키고 Delphi 쪽도 만족시켜 링크시 문제가 없도록 만들수 있을까요?
그것은 위 MPEG_C 델파이 유닛의
import 되는 함수 명칭에 언더바를 붙여주면 해결됩니다.
function _openMPEG(FileName:PAnsiChar):pointer; cdecl; external 'libMPEG.dll';
이렇게 말이죠.
그러면 다 됐을까요?
아니죠.. 이렇게 하면
#include "libMPEG.hpp"
에서 _openMPEG 함수만 찾을 수 있기 때문에
C++ 소스에서 사용한 함수 명칭 openMPEG을 참조할 수 없어 컴파일 시 에러를 내게 됩니다.
그러면 이를 해결하려면
C++ 소스 파일 안에
다음과 같이 추가 선언을 해주면 됩니다.
extern "C" void * __cdecl openMPEG(char * AFileName);
그러면 모든게 잘 참조되고 링크 됩니다.
결론 :
이런 문제는 애초에 볼랜드가 RunTime 함수와의 충돌을 피하기 위해
C/C++ 소스가 컴파일 될때 언더바를 함수명 앞에 붙이는 것으로부터 출발했는데
지금에 와서 보면 더 큰 문제를 만들고 말았죠.
그래서 언더바를 붙이지 않는 stdcall 호출규약을 쓰면 위 문제가 자동 해결됩니다.
즉
function openMPEG(FileName:PAnsiChar):pointer; stdcall; external 'libMPEG.dll';
로 드러내고
extern "C" void * __stdcall openMPEG(char * AFileName);
로 받아서 참조 할수 있게 DLL 함수를 구성하면 된다는 것이죠.
그리고 DLL 내에는 C 스타일의 함수가 많이 제공되는데
제가 별도로 델파이로 만든 DLL에는 이런 식으로 함수를 export 하고 있습니다.
interface
uses
Classes, SysUtils;
type
// 바깥으로 들어내는 인터페이스
IKtsAudio = class
procedure Open(channel: Integer; bitrate: Integer); virtual; stdcall; abstract;
procedure Close(); virtual; stdcall; abstract;
function PlayData(data: Pointer; size: Integer): integer; virtual; stdcall; abstract;
procedure Delete(); virtual; stdcall; abstract;
end;
// 클래스 생성 소멸 함수를 제공한다.
function newCKtsAudio(): IKtsAudio; stdcall;
외부로 드러내는 DLL 함수는 newCKtsAudio 하나가 전부입니다.
stdcall 로 되어 있으니, 네임맹글(Name Mangle)에 언더바를 고려할 필요가 없습니다.
이 함수는 IKtsAudio라는 클래스를 리턴해주는게 다입니다.
그러면 이 DLL을 델파이, C++Builder, VC++ 등에서 받아서 그대로 사용하면 됩니다.
여기서 DLL 많이 사용해보신 분들은
엉? 표준 DLL은 그런거 안되잖아 라고 아시는 분도 많을텐데,
분명히 제가 만든 DLL은 표준 DLL 맞고
단 한개의 함수만 export 하고 있습니다.
그리고 이 함수는 class를 리턴함에도 불구하고
델파이, C++Builder, VC++ 등의 컴파일러에서 그대로 class로 받아서 사용 가능합니다.
이 기법은 제가 몇년전에 발표한바 있습니다.
http://cbuilder.borlandforum.com/impboard/impboard.dll?action=read&db=bcb_tutorial&no=143
표준 DLL 이지만, 여러 컴파일러에서 class 정확히는 interface에 가깝지만
멤버 변수까지 쓸수 있다는 점에서 class 라고 표현해도 무방한데
이를 여러 컴파일러에서 사용할 수 있으므로,
쓰는 사람이 가져다 쓰기 매우 좋습니다.
델파이, C++Builder, VC++ 에서 동일하게 동작함을 이미 여러차례 확인했고
실무에 써봐도 아무런 문제가 없는데, 아마도 GCC Dev-C 등의 컴파일러에서
가져다 써도 별 문제 없을 것으로 생각하고 있습니다.
물론 C++ 계열에서는 가져다 쓰려면 클래스를 C++ 스타일로 선언해서 써야죠.
// 바깥으로 들어내는 인터페이스
class IKtsAudio
{
public:
virtual void __stdcall Open(int channel, int bitrate) = 0;
virtual void __stdcall Close() = 0;
virtual int __stdcall PlayData(void *data, int size) = 0;
virtual void __stdcall Delete() = 0;
};
이런 식으로 말이죠.
이 클래스를 리턴 받아서
IKtsAudio *KtsAudio = newCKtsAudio();
사용하다가 사용이 끝나면 .Delete() 메소드를 호출해주면 객체가 메모리에서 최종적으로 사라지게 됩니다.
아 물론,IKtsAudio 클래스를 상속받은 내부 구현 클래스 내에서는
Delete() 메소드는 내적으로 Free() 메소드를 호출해서 객체를 메모리에서 소멸시키게 됩니다.