In the process of developing a lightweight container Based on Dynamic proxy, it is necessary to dynamically load external custom interfaces, classes, and component functions. You cannot determine whether to use DLL or BPL as the implementation method of custom components. Some technical details were found during repeated tests, especially when the string type was used as a parameter or return value.
Anyone who has developed DLL using Delphi knows that in the code generated by the Delphi DLL wizard, there is a long "important description about dll Memory Management" at the beginning of the DLL project source ", in general, if the string type is used as the parameter or return value in the DLL exports function, you must add sharemem at the beginning of the uses segment of the DLL and the uses segment of your application so that the two can use borlndmm together. DLL for memory management, to ensure that the string type of memory allocation/release is correct.
This is because the string type is in Delphi and the compiler provides it with a dynamic memory allocation/release mechanism and a reference counting mechanism, so that the string type can be used as a simple type, instead of worrying about memory management and preventing Memory leakage in C/C ++. But this also brings a problem like the DLL: If sharemem is not used, it is possible that the memory allocated in one location is mistakenly released in another location, this eventually leads to a nasty access violation.
For simple function calls, dll + sharemem can be used, but it won't work if it involves interfaces and classes.
Consider the following simple interface and implementation.
//---------------------------------------------------------
// Defined in the interface unit
{$ M +}
Idemointf = interface
['{5f3c4d61-b885-41b6-b43b-c4725df5d901}']
Function gethello (NID: integer): string; stdcall;
End;
{$ M -}
//---------------------------------------------------------
// Defined in the Implementation Unit
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;
First, let's look at the implementation of the dll version. Create a DLL project, add the above two units, and run the compregister function exports. The compregister function is described as follows:
This compregister is a registration entry. The Container calls the DLL and runs the registration function immediately. The user component package must implement this registration function, register the user-Implemented interfaces/classes with the container. Tmregisterintfevent is a method type. When a container calls the registration function, it uses a parameter to reference its registration method and passes it to the registration function for calling.
The following is a unit test program implemented by 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;
This test is simple: first load the DLL in the initialization (Setup method [1]), and then call the compregister registration interface and implementation class. In testloader, the interface instance is obtained through the getintfinstance method of the interface registration Manager (gmintfreg, provided by the container. The internal implementation principle of this method is to find the corresponding class type through the interface guid, create an instance, and then convert it to interface reference through the supports function. Call the interface method gethello and check with dunit's check function. Finally, delete the interface Registration Information and release the DLL in the cleanup process (teardown method [1.
However, the test failed. The problem is that F. gethello () returns a string, and the memory used by this string is allocated in the DLL, but because Delphi compilation will automatically clear such a returned string reference before testloader returns. After debugging, you can find the problem here. First of all, you can be sure that the check function is passed, however, when you open the CPU debugging window tracking, you can find that an exception occurs when the compiled cleaning code is executed.
Why does an error still occur when sharemem is clearly used? Because the interface method F called here. gethello () is not a DLL's exports function. Therefore, strings used in its parameters or return values are not managed by sharemem. An error is certainly a problem.
Let's take a look at the implementation of the BPL method. Create a BPL project and add the previous interface unit and Implementation Unit. However, you do not need to add exports like DLL, but you need to add an hack:
Procedure HackRegister;
Asm
push edx
push eax
call CompRegister
pop ecx
pop ecx
End;
- Why? Although BPL is essentially a DLL, Delphi implements it differently. One of the biggest features is that the content in BPL does not need to be explicitly export, but the content in all interfaces is automatically export according to certain rules. This rule is generally like this: the export name of a function in a unit is composed of its unit name and function name plus the compilation additional string, for example, the export name of the above hackregister is @ unit1 @ hackregister $ qqrv, where $ qqrv is the compilation of additional strings. If it is a class method, the class name is added. If there is a parameter or return value, the information will also be compiled into the additional string for distinguishing overload and so on. Because the compregister has a complex parameter, a complicated export name like this will be formed: @ unit1 @ compregister $ qynpqqrrx5_guidpx17typinfo @ ttypeinfopx17system @ tmetaclass $ v. To avoid this problem, the standard BPL of Delphi uses a non-parameter register function, but in this case, it needs to rely on a global variable for the master program to share with BPL, to truly achieve sharing, this global variable must be in a public BPL. For example, the standard BPL depends on VCL. BPL and RTL. BPL two packages [2]. I don't want to create another separate BPL, so use the preceding hack method without parameters to transfer it implicitly through edX: eax.
The following is the test code of 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;
Basically the same as the dll version, but the call registration entry is indirectly implemented through compregisterhack, where the parameter is saved to edX: eax and then the hackregister function pointer is called. In addition, it is the same as the dll version.
In theory, this should be okay, but for the sake of security, I ran the test in debug mode and tracked the code in the CPU window until the test function was returned. However, an unexpected exception occurred in the dunit test framework. This leaves me puzzled for several days, Because delphi uses BPL as well. It is also common to use string, and I have never heard of any problems. To locate the problem, I even tried to change the implementation class to derived from tcomponent, but the exception still exists.
Finally, I noticed that although the tdemoimpl instance is created in getintfinstance, The gethello method called after the return is a member of the interface type idemointf. Theoretically, calling an interface method is essentially mapped to a class instance through the virtual function mechanism of OOP. There should be no difference, but this involves cross-module calling (calling code between EXE and BPL) in this case, will the conditions required by sharemem be damaged after the ing?
To confirm this guess, I included the class definition code in the test program and changed the interface method call to class method call. The test was successful. To gain a deeper understanding of the nature of the problem, I checked the CPU window in the case of class method call and interface method call. Sure enough, the interface method is called after a virtual function jump, and the destination address after the jump is actually different from the class method call address! In both cases, the code at different code addresses is exactly the same!
This makes me unable to do anything. I even implemented an interface-based string class to replace the string type. Although this can be used to pass the string through parameters, the return value still does not work. This method is really ugly and difficult to use.
Today, I happened to think that my test program is compiled in static package mode, that is, it runs without BPL, while Delphi itself is compiled to run in BPL mode. So I tried to change the test program to VCL. BPL and RTL. BPL.
Sure enough, the test passed.
References
[1] "dunit Delphi's zookeeper compaction Tools"
[2] Wang Rui's in-depth exploration of dynamic loading and Dynamic Registration Technologies
[Mental Studio] apr.24-05