C++Builder Programming Forum
C++Builder  |  Delphi  |  FireMonkey  |  C/C++  |  Free Pascal  |  Firebird
볼랜드포럼 BorlandForum
 경고! 게시물 작성자의 사전 허락없는 메일주소 추출행위 절대 금지
C++빌더 포럼
Q & A
FAQ
팁&트릭
강좌/문서
자료실
컴포넌트/라이브러리
메신저 프로젝트
볼랜드포럼 홈
헤드라인 뉴스
IT 뉴스
공지사항
자유게시판
해피 브레이크
공동 프로젝트
구인/구직
회원 장터
건의사항
운영진 게시판
회원 메뉴
북마크
볼랜드포럼 광고 모집

C++빌더 강좌/문서
C++Builder Programming Tutorial&Docments
[232] 이미 만들어진 델파이 DLL과 C++Builder와 Delphi 유닛을 혼합해서 쓸때 방법(보강)
김태선 [cppbuilder] 39981 읽음    2013-01-20 08:47
류종택님의 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() 메소드를 호출해서 객체를 메모리에서 소멸시키게 됩니다.
미수타윤 [mryyd]   2013-06-11 15:51 X
역시나...이글도많은도움이되었습니다...감사합니다(__)
박영호 [amesianx]   2013-07-11 20:02 X
말씀하신 C++ 클래스를 패키징한 DLL 이 표준인가 아닌가에 대해서 제가 설명해드리면, 표준이지만 표준이 아닌 일종의 꽁수라고 보시면 맞을것 같습니다. 이게 왜 그렇게 되냐면 위에서는 클래스를 export 한게 아니라 클래스를 생성하는 함수를 익스포트한 거니까요.. 그래서 사용법을 보시면 IKtsAudio 에 대한 인스턴스 포인터 변수를 만든 뒤에 new 를 쓰지 않고 newCKtsAudio 라는 new 역할을 해주는 가짜 클래스 생성자를 호출해서 받지요.. 그래서 이게 실제로 클래스를 노출한 것도 아니고 실제 클래스를 불러서 쓴 것도 아닙니다.. 그저 stdcall 함수를 호출해서 C++  클래스의 포인터를 넘겨받은것에 불과한 것이지요.. 원래 C++ 이라는 놈은 특정한 지정이 없는 한 멤버변수들은 모두 thiscall 이라는 함수호출 규약을 따르도록 되어있습니다. 아마 멤버함수에 별도의 호출규약 지정을 하면 에러를 낼거 같은데요.. 안해봤는데 아마 문법에서부터 에러날걸요.. 여튼.. C++ 이라는 놈은 thiscall 호출규약을 따르도록 되어잇는데요.. 이 thiscall C++ 호출규약이라는 놈이 stdcall 과 동일합니다.. 단, 멤버함수들은 자신의 모체가되는 놈인 클래스 본체의 this 포인터를 ecx 레지스터를 통해서 넘겨받는다는 암묵적인 동의를 합니다.(MSVC 기준으로 ecx 레지스터를 사용합니다.) 그니까, __asm  mov ecx, pThis(디스포인터 값을 담은 변수); Open(channel, bitrate); 처럼 호출하면 IKtsAudio 클래스의 멤버함수를 그냥 호출할 수 있습니다.. 일반적인 경우는 아니구요.. 해커들이 게임해킹할때 사용하는 기법인데요.. 이런겁니다.. 예를들어 어떤 개발자가 싱글톤으로 개발을 했고 DLL 에다가 넣어놨다고 쳐보자구요.. 위에 경우가 딱 그런경우거든요.. 싱글톤이라는게 클래스의 대가리(예를들어 GetInstance() 함수이름이 일반적임)를 넘겨주는건 다들 아시죠? 대표적인 함수이름이 GetInstance() 입니다.. 이놈은 대부분이 static 으로 선언해놓는 다는건 아시죠? 클래스함수는 static 입니다..(C++ 기본이죠..) GetInstance() 이 함수는 클래스를 반환해주는 싱글톤 함수이고 static 인데 DLL 파일안에 박혀있게되는거죠.. 위의 경우와 똑같습니다.. 똑똑한 해커들은 이 GetInstance() 함수를 DLL 에서 로드라이브러리+GetProcAddress 로 정상적인 방법으로 함수를 뽑아내죠.. 그리고 GetInstance() 싱글톤함수로 생성한 값을(this 포인터 값) ecx 레지스터에 담습니다.. __asm mov ecx, pThisGetInstance; 정도로 해두죠 pThisGetInstance 변수에는 DLL 에서 임포트한 GetInstance 함수의 리턴값을 넣어두는거죠.. 그리고 바로 Open 함수나 Close 함수 같은 멤버함수를 그냥 호출합니다.. 물론, 이때 Open 함수나 Close 함수는 오리지날이라면 thiscall 규약을 원래 따라야 합니다..(문법상 따라줘야 한다는 건데요..) 그런데, 후킹같은짓을 한 경우에는 위에 글에서 나온 것처럼 친절하게 헤더가 없거든요..? 그래서 해커들은 stdcall 규약으로 바꿔서 호출해버리죠.. 리버싱이라는 기술을 통해서 인자가 뭐가 넘어가는지 대충 알아낸 뒤에 Open 이라는 멤버함수를 __stdcall 이라는 지시어를 준 함수포인터를 만듭니다.. 그리고 Open 함수의 주소를 직접 함수포인터에 넣어서 진짜 함수 호출하듯이 사용해서 호출합니다.. 이렇게 되면 thiscall 규약이 무색해지게되죠.. ecx 에 this 포인터 값을 넣었으니 아무 문제가 없죠.. Open 함수는 그냥 stdcall 로 호출시켰죠.. 그러니 C++ 호출이 가능해지는겁니다.. 지금 이 같은 기술은 해커들이 주로 게임해킹할때 많이 쓰는 기술이구요.. 싱글톤기법을 DLL 에 넣어둔 경우에 아주 유용한 게임해킹기술이 되거든요.. 근데 재밌게도 위에 글에 써주신 분이 설명하신 기법이 제가 지금 설명해드린 그 기법의 기초이론입니다.. 위에서는 굳이 ecx 에 값을 넣어주지 않죠? 그 이유는 위에 글에서는 개발자입장이고 해커입장이 아닌지라 헤더파일이 있잖아요.. 헤더파일의 내용을 알고 있기 때문에 ecx 레지스터를 다룰 필요가 없이 IKtsAudio * 라는 방식을 사용할 수 있게된다는 것이죠.. 즉, 위에서 DLL 파일에 C++ 클래스를 익스포트한게 아니라 실제로는 stdcall 함수 하나를 익스포트한 것이구요.. 받는 쪽에서는 인스턴스 포인터만 받아서 해킹할때 ecx 레지스터에 넣어서 강제호출하듯이 이미 헤더를 알고 있으니까 IKtsAudio * 를 ecx 레지스터에 값을 밀어넣는 형식으로 사용한 기술이다라고 아시고 계시면 되는겁니다.. 즉, C++ thiscall 호출 규약의 형태를 무너뜨린 export 방식을 다시 받을때 thiscall 규약처럼 속여서 다시 받아내서 사용하는 기술이다라는 것으로 보시면됩니다.. 즉, 정상적인 표준 DLL 이기도 하지만 일종에 트릭을 사용해서 C++ 포인터를 받아서 이용한다는 것이니 변칙적 방식이죠.. 변칙적인 방식이지만 표준방식을 응용했기 때문에 기술적으로는 표준, 방식적으로는 꽁수 뭐 그런거죠..
뭐 더 이해가 안되시는 부분이 있으시면 이메일로 질문하세요.. 기술적으로 제가 풀어서 설명드렸음..
amesianx@gmail.com
김태선 [cppbuilder]   2013-08-26 11:44 X
박영호/
어떤 면에서 꼼수라도 표현할 수도 있겠으나
이 방법은 C++이 외부 인터페이스를 사용하는 표준을 응용한 것이므로
앞으로도 규칙이 변하지 않아 사용상의 위험이 없이
호환이 보장되는 방법입니다.

특히나 C++ 델파이 등에서 공통적으로 이용할수 있으므로
그 가치는 매우 크다 하겠습니다.

이 방법은 COM이 성립되기 위한 표준을 응용하여 이용하는 것이라 보는게 타당한데
(제가 이 방법을 찾을때는 COM과는 무관하게 수백차례 디스어셈블 결과로 찾은 거지만)
뭔 트릭이나 해커의 방법과는 차이가 있습니다.

+ -

관련 글 리스트
232 이미 만들어진 델파이 DLL과 C++Builder와 Delphi 유닛을 혼합해서 쓸때 방법(보강) 김태선 39981 2013/01/20
(링크)     Delphi 강좌/문서자료 > 이미 만들어진 델파이 DLL과 C++Builder와 Delphi 유닛을 혼합해서 쓸때 방법(보강)
Google
Copyright © 1999-2015, borlandforum.com. All right reserved.