DLL, BPL 및 문자열 전달

7126 단어 문자열
동적 에이전트를 기반으로 하는 경량 용기를 개발하는 과정에서 외부에 동적 불러오는 고객 사용자 정의 인터페이스/클래스/구성 요소 기능은 필수적인 구성 부분이다.사용자 정의 구성 요소로 DLL을 사용해야 하는지 BPL을 사용해야 하는지 선택할 수 없습니다.반복적인 시험 과정에서 그 중의 기술적 세부 사항을 발견했는데 특히 문자열 유형을 매개 변수나 반환값으로 하는 상황에서 발견했다.
DELPHI로 DLL을 개발한 사람은 DELPHI의 DLL 마법사가 생성한 코드에서 DLL Project Source는 처음부터 긴'DLL 메모리 관리에 대한 중요한 설명'이 있었다. 그 내용은 대체적으로 DLL의 exports 함수에서string 유형을 매개 변수나 반환값으로 사용한다면 DL의 Uses 세그먼트와 응용 프로그램의 Uses 세그먼트 맨 앞에 Share Mem을 넣어야 한다. 이것은 두 사람이 BORLNDMM을 공동으로 사용하게 할 것이다.DLL은 메모리 관리를 해야만 string 형식의 메모리 분배/방출이 정확하다는 것을 보장할 수 있습니다.
이것은string 형식이 DELPHI 내부에 있기 때문에 컴파일러가 동적 메모리 분배/방출 메커니즘과 인용 계수 메커니즘을 제공하여string 형식을 일반적인 간단한 형식처럼 사용할 수 있고 C/C++에서처럼 메모리 관리와 메모리 유출을 번거롭게 고려하지 않아도 된다.그러나 이 동시에 DLL의 문제점도 있다. ShareMem을 사용하지 않으면 한 곳에서 분배된 메모리가 다른 곳에서 잘못 방출되어 결국 싫은 Access Vilation을 초래할 수 있다.
간단한 함수 호출은 DLL+ShareMem으로 할 수 있지만 인터페이스와 클래스가 관련될 때는 안 된다.
아래의 이 간단한 인터페이스와 실현을 고려해 보자.
//---------------------------------------------------------
//
{$M+}
IDemoIntf = interface
['{5F3C4D61-B885-41B6-B43B-C4725DF5D901}']
Function GetHello( nID : Integer ) : String; StdCall;
End;
{$M-}

//---------------------------------------------------------
//
type
TDemoImpl = class(TInterfacedObject, IDemoIntf)
protected
{ Protected declarations }
Function GetHello( nID : Integer ) : String; StdCall;
end;

Procedure CompRegister( aIntfReg : TMRegisterIntfEvent ); Cdecl;

implementation

Procedure CompRegister( aIntfReg : TMRegisterIntfEvent );
Begin
aIntfReg( IDemoIntf, TypeInfo( IDemoIntf ), TDemoImpl );
End;

{ TDemoImpl }

function TDemoImpl.GetHello(nID: Integer): String;
begin
Result := 'Hello ' + IntToStr( nID );
end;

먼저 DLL 버전의 구현을 살펴보겠습니다.DLL Project를 만들고 상기 두 단원을 추가하고CompRegister 함수 Exports를 내보냅니다.이 CompRegister 함수에 대한 간단한 설명은 다음과 같습니다.
이CompRegister는 등록 입구입니다. 용기는 이 DLL을 호출하여 이 등록 함수를 즉시 실행합니다. 사용자 구성 요소 패키지는 이 등록 함수를 실현하고 그 중에서 용기에 사용자가 실현한 인터페이스/클래스를 등록해야 합니다.TMRegisterIntfEvent는 컨테이너가 등록 함수를 호출할 때 매개 변수를 통해 등록 방법을 참조하여 이 등록 함수를 호출할 수 있는 방법 유형입니다.
다음은 대응하는 Dunit로 이루어진 단원 테스트 프로그램입니다.
procedure TTestCaseDLLPackageLoader.Setup;
Var
funcInit : TMFuncCompRegister;
begin
hPkg := LoadLibrary( 'demopkg.dll' );
funcInit := TMFuncCompRegister( GetProcAddress( hPkg, 'CompRegister' ) );
funcInit( GMIntfReg.RegisterIntf );
end;

procedure TTestCaseDLLPackageLoader.TearDown;
begin
GMIntfReg.UnregisterIntf( IDemoIntf );
FreeLibrary( hPkg );
end;

procedure TTestCaseDLLPackageLoader.TestLoader;
Var
f : IDemoIntf;
begin
f := GMIntfReg.GetIntfInstance( IDemoIntf ) As IDemoIntf;
Check( f.GetHello( 10 ) = 'Hello 10' );
end;

이 테스트는 매우 간단하다. 우선 초기화 (Setup 방법[1])에서 DLL을 불러온 다음에CompRegister 등록 인터페이스와 실현 클래스를 호출한다.테스트 함수TestLoader에서 인터페이스 등록 관리자(GMintfReg, 용기에서 제공)의 GetIntfInstance 방법을 통해 인터페이스 실례를 가져옵니다.이 방법의 내부 실현 원리는 인터페이스의 GUID를 통해 대응하는 클래스 유형을 찾은 다음에 실례를 만들고 Supports 함수를 통해 인터페이스로 인용하는 것이다.인터페이스 메서드 GetHello를 호출하고 Dunit의 Check 함수를 사용하여 확인합니다.마지막으로 정리 과정(TearDown 방법[1])에서 인터페이스 등록 정보를 삭제하고 DLL을 방출한다.
그러나 결과는 불행하게도 시험에 통과하지 못했다.문제는 f.GetHello () 가 string을 되돌려주었고, 이 string이 사용하는 메모리는 DLL에서 분배되었지만, DELPHI의 컴파일이TestLoader가 되돌아오기 전에 자동으로 이 반환값string 인용을 정리합니다.디버깅을 통해 문제점을 발견할 수 있습니다. 우선 Check 함수의 검사가 통과되었음을 확신할 수 있지만, CPU 디버깅 창을 열어 추적을 하면 컴파일된 청소 코드가 실행될 때 이상이 발생한 것을 발견할 수 있습니다.
왜 ShareMem을 사용했는데도 틀릴까요?여기에서 호출된 인터페이스 방법 f.GetHello ()는 DLL의 exports 함수가 아니기 때문에 매개 변수나 되돌아오는 값에string을 사용하여shareMem에 의해 관리되지 않습니다. 오류가 발생하는 것은 당연한 일입니다.
다시 BPL 방식의 실현을 살펴보자.마찬가지로 BPL 프로젝트를 만들고 앞에 있는 인터페이스 단원과 실현 단원을 추가합니다. 하지만 DLL처럼 exports가 필요하지 않지만 Hack을 추가해야 합니다.
Procedure HackRegister;
Asm
push edx
push eax
call CompRegister
pop ecx
pop ecx
End;

왜 그러세요?BPL도 본질적으로 DLL이지만 DELPHI의 실현은 다르다.가장 큰 특징은 BPL의 내용이 현식 export가 필요하지 않고 모든 Interface 부분의 내용이 일정한 규칙에 따라 자동으로 export된다는 것이다.이 법칙은 대체적으로 다음과 같다. 한 단원의 함수의 export 이름은 단원 이름과 함수 이름에 컴파일된 추가 문자열로 구성된다. 예를 들어 위의 Hack Register의 내보내기 이름은 @이다.Unit1@HackRegister$qqrv, 그중 $qrv는 추가 문자열을 컴파일하는 것입니다.클래스 방법이라면 클래스 이름도 붙이고, 매개 변수나 반환 값이 있으면, 이 정보는 추가 문자열에 컴파일되어 오버로드 등을 구분하는 데 사용됩니다.그 CompRegister에는 복잡한 매개 변수가 있기 때문에 이렇게 복잡한 내보내기 이름이 형성됩니다: @Unit1@CompRegister$qynpqqrrx5_GUIDpx17Typinfo@TTypeInfopx17System@TMetaClass$v.이러한 번거로움을 피하기 위해 DELPHI의 표준 BPL은 파라미터가 없는 Register 함수를 사용하지만 이렇게 되면 전체 변수에 의존하여 메인 프로그램과 BPL이 공용하도록 해야 한다. 진정으로 공용을 실현하려면 이 전체 변수는 반드시 공공 BPL에 의존해야 한다. 예를 들어 표준 BPL은 VCL에 의존해야 한다.bpl 및 RTL.bpl 이 두 개의 가방[2].나는 더 이상 단독 BPL을 만들고 싶지 않기 때문에 위에 있는 파라미터가 없는 Hack 방법으로 돌려서 파라미터가 edx:eax를 통해 은밀하게 전달된다다음은 BPL 버전의 테스트 코드입니다.
procedure TTestCaseBPLPackageLoader.Setup;
Var
funcInit : TProcedure;
begin
hPkg := LoadPackage( 'demobpl.bpl' );
funcInit := TProcedure( GetProcAddress( hPkg, '@Unit1@HackRegister$qqrv' ) );
CompRegisterHack( funcInit, GMIntfReg.RegisterIntf );
end;

procedure TTestCaseBPLPackageLoader.TearDown;
begin
GMIntfReg.UnregisterIntf( IDemoIntf );
UnloadPackage( hPkg );
end;

procedure TTestCaseBPLPackageLoader.TestLoader;
Var
f : IDemoIntf;
begin
f := GMIntfReg.GetIntfInstance( IDemoIntf ) As IDemoIntf;
Check( f.GetHello( 10 ) = 'Hello 10' );
end;

기본적으로 DLL 버전과 마찬가지로 등록 입구를 호출하는 것은CompRegisterHack을 통해 간접적으로 이루어지고 그 중에서 파라미터를 ex:eax에 저장한 후에 HackRegister 함수 바늘을 호출한다.이 외에도 DLL 버전과 동일합니다.
이론적으로 이렇게 하면 문제가 없을 것이다. 그러나 안전을 위해 나는 디버깅 방식으로 이 테스트를 실행하고 CPU 창의 코드를 추적했다. 테스트 함수가 돌아올 때까지 문제가 없을 것 같다.하지만 계속 운행하던 Dunit의 테스트 프레임워크에서 예상치 못한 이상이 발생했다.DELPHI 자체도 이렇게 BPL을 사용하고 String을 사용하는 곳도 흔하기 때문에 문제가 있다는 말을 들어본 적이 없습니다.문제의 소재를 찾기 위해, 나는 심지어 실현 클래스를 Tcomponent에서 파생하는 것으로 바꾸어 보았지만, 이상은 여전히 존재한다.
마지막으로 나는 여기에서 GetIntfInstance에서 만든 것이 TDemoImpl의 실례이지만, 되돌아온 후에 호출된 GetHello 방법은 인터페이스 형식인 IDemoIntf의 구성원이라는 것을 알아차렸다.이론적으로 인터페이스를 호출하는 방법은 본질적으로 OOP의 허함수 메커니즘을 통해 클래스 실례에 비추는 것이기 때문에 별 차이가 없을 것이다. 그러나 여기서 크로스 모드 호출(EXE와 BPL 사이에서 코드를 호출하는 것)의 문제가 언급되었는데 여기서 비추어서ShareMem이 요구하는 조건을 파괴하지 않을까?
이 추측을 증명하기 위해서, 나는 테스트 프로그램에 클래스 정의 코드를 포함하고, 인터페이스 방법 호출을 클래스 방법 호출로 바꾸었는데, 과연 성공했다.문제의 본질을 깊이 있게 이해하기 위해 나는 클래스 방법 호출과 인터페이스 방법 호출 두 가지 상황의 CPU 창을 다시 한 번 보았다.과연 인터페이스 방법이 호출된 후 허함수의 점프를 한 번 거쳤는데, 점프 후의 목표 주소는 클래스 방법이 호출한 주소와 다르다!그러나 두 가지 상황에서 서로 다른 코드 주소의 코드는 완전히 같다!
이것은 나로 하여금 속수무책으로 만들었다.나는 심지어 이를 위해 인터페이스를 기반으로 하는 String 클래스를 실현하여 String 유형을 대체했다. 비록 이것으로 파라미터를 통해 String을 전달할 수 있지만, 되돌아오는 값은 여전히 안 된다.그리고 이런 방법은 정말 보기 싫고 쓰기 어렵다.
오늘 우연히 나의 테스트 프로그램은 정적 패키지 방식으로 컴파일되었다. 즉, BPL 방식으로 실행되지 않았고, DELPHI 자체는 BPL 방식으로 컴파일되어 실행되었다는 것을 생각했다.그래서 한번 해보자는 마음으로 테스트 프로그램을 VCL로 바꿨습니다.bpl 및 RTL.bpl 방식.
역시 테스트는 통과했다.
참고 문헌[1] 채환린, [2] 왕예,.
[ Mental Studio ]맹금Apr.24-05

좋은 웹페이지 즐겨찾기