Author: Ivan S Zapreev
summary
This article will introduce the backward compatibility problem of the DLL, which is the famous "DLL Hell" issue. First of all, I will list my research results, including other researchers' results. At the end of this article, I will also give a solution for the "DLL Hell" issue.
Introduction
I have accepted a task to solve a problem with a DLL version update. Class (direct use or derived new subclasses) to continue their C program development. Users are not available in a very detailed instructions when using these DLLs (such as what restrictions such as those exported in these DLLs, etc.). When these DLL updates are new versions, they find that they developed these DLL-based applications often crash (their application is derived from the SDK's export class). To solve this problem, users must recompile their applications to reconnect new versions of SDK DLL.
I will give my findings on this issue, and I also collect information from other places. Finally, I will solve this "DLL Hell" issue in the future.
Research result
As far as I personally understand, this problem is caused by the base class changes exported in the SDK DLL. I have viewed some articles that I found that the DLL backward compatibility problem has been raised. But as a real researcher, I decided to do some experiments. As a result, I found the following question:
1. Add a new virtual function in the DLL export class will result in the following questions: (1) If this class has a virtual function B, it adds a new virtual function A before it. In this way, we changed the virtual function table of the class. Thus, the first function in the table points to the function a (not the original B). At this point, the client program (assuming that you don't recompile, connection, connection after the new version of the DLL), the call function B will produce an exception. Since the call function B is actually turned to the call function A, if the parameter type of the function A and the function B, the problem of return value is very different! (2) If this class has no virtual function (its parent class has no virtual function), add a new virtual function to this class (or add a virtual function in its parent class) will result in new additive members. This member is a pointer type, pointing to virtual functions. Thus, the size of this class will be changed (because a member variable is added). In this case, if the client creates the instance of this class, there will be problems when you need to direct or indirectly modify the value of the class member. Because the pointer to the virtual function table is joined as the first member of the class, that is, members of this class definition generates an address offset because of the addition of the virtual function table pointer. The customer has an abnormality in the operation of the original member. (3) If this class has a virtual function (or as long as its parent class has a virtual function), and this class is exported, it is used by the client as a parent class. So, we don't add virtual functions to this class! Not only cannot be added in the beginning of class, even if it is at the end. Because the addition of virtual functions can cause the function map in the virtual function table to generate offset; even if you add virtual functions to the end of the class declaration, the virtual function table of this class's derived class will result in offset.
2. Add a new member variable in the DLL export class will result in the following questions: (1) adding a member variable to a class will result in a change in type size (add a virtual function to the original virtual function table) Change the size of the class). Suppose this member increases in the final declaration. If the customer program is less assigned less memory for the instance of this class, it is possible to cause memory offline when accessing this member. (2) If a new member is added to the middle of the original class member, the situation will be worse. Because this will result in an offset of the address of the original class member. The client operation is an error address table, which is especially the next member of the new member (they have caused changes in the offset in the class because of the addition of new members). (Note: The above customer programs refer to applications using SDK DLL.)
In addition to these reasons, other operations can cause backward compatibility problems of the DLL. The method of solving (most) these issues is listed below.
DLL coding convention
Here is all the solution I got, some of which are from the online article, some are obtained with different developers.
The following conventions are primarily developed for DLL, and is to solve the DLL backward compatibility problem:
1. Coding convention: (1) Each export class (or its parent class) of the DLL contains at least one virtual function. This way, this class will always save a pointer member to the virtual function table. This can be used to easily join the new virtual function. (2) If you want to add a virtual function to a class, then add it behind all other virtual functions. This will not change the order of address mappings in the original function in the virtual function table. (3) If you plan to give a class expansion class member, now reserve a pointer to a data structure. In this way, adding a member to modify directly in this data structure instead of modifying in the class. Thus, the addition of new members does not result in changes in class size. Of course, in order to access new members, you need to define several operation functions to this class. In this case, the DLL must be connected by the client-implicity (IMPLICITLY). (4) In order to solve the problem, you can design a pure interface class for all export classes, but at this time, the client will not be able to continue derived from these export classes, and the Hierarchic organization of the DLL export class will not be maintained. (5) Post two versions of DLL and LIB files (Debug version and Release version). Because if only release release version, developers will not be able to debug their programs, because the Release version uses a different pile (HEAP) manager with the Debug version, so as the Debug version of the client releases the release version DLL application, Resulting in runtime error (runtime failure). There is a way to solve this problem, that is, the DLL provides a function of the application and release of the memory for client programs; the DLL also guarantees that the content of the client application is not released. Usually obeying this agreement is not that simple! (6) When compiling, do not change the default parameters of the DLL export class function, if these parameters will be passed to the client program. (7) Pay attention to the changes in the inline function. (8) Check all enumerations without the default element value. Because when an addition / delete a new enumeration member, you may move the value of the old enumeration member. This is why each member should have a unique identifier value. If the enumeration can be expanded, it should also be documentded. In this way, customer program developers will attain attention. (9) Do not change the macro defined in the header file provided by the DLL. 2. Version of DLL: If the primary DLL changes, it is best to change the name of the DLL file, just like Microsoft's MFC DLL. For example, the DLL file can be named in accordance with the following format: DLL_NAME_XX.DLL, where XX is the version number of the DLL. Sometimes a big change in DLL makes it unable to solve the backward compatibility problem. A new DLL should be generated at this time. When this new DLL is installed to the system, the old DLL remains. Thus, the old client can still use the old DLL, and the new client program (compiled using a new DLL) can use the new DLL, both of which do not interfere.
3. Dial-rear compatibility test of the DLL: There are still many mids that may undermine the backward compatibility of the DLL, so the backward compatibility test of the implementation of the DLL is very necessary!
Next, I will discuss a virtual function problem and a corresponding solution.
Virtual function and inheritance
First come and see the following virtual functions and inheritance structure:
/ ********** DLL exported class ********** / class export_dll_prefix virtfunctClass {public: virtfunctClass () {} ~ virtfunctClass () {} Virtual void dosmth () {/ / this-> DoAnything (); // Uncomment of this line after the corresponding method // will be added to the class declaration} // virtual void DoAnything () {} // Adding of this virtual method will make shift in // Table of Virtual Methods}}; / ********* Client, from DLL Export class derived a new subclass ********** / Class VirtfunctClassChild: public virtfunctclass {public: VirtfunctClassChild (): VirtfunctClass ()} ~ virtfunctclasschild () {}; virtual void dosomething () {}};
Assuming the two classes above, VirtFunctClass is implemented in My.dll, and VirtfunctClassChild is implemented in the client. Next, we do some changes, let go of the following two notes: // virtual void doanything () {} and // this-> doanysting ();
In other words, the class exported by the DLL has changed! Now if the customer program does not recompile, the VirtfunctClasschild in the client program will not know that the VirtfunctClass class in the DLL has changed: add a virtual function void doanysting (). Therefore, virtual functions of the VirtfunctClassChild class still contain two functions: 1. Void dosmth () 2. Void Dosomething ()
And in fact, this is already right, the correct virtual function table should be: 1. Void dosmth () 2. Void doanything () 3. Void Dosomething ()
The problem is that if you instantiate VirtFunctClassChild, if it calls its void dosmth () function, the dosmth () function is turned to call the void doanything () function, but at this time, the base class VirtfunctClass only knows that the virtual function table is to call the virtual function table. Two functions, and the second function in the virtual function table of the VirtfunctClassChild class is still void dosomething (), so the problem is coming out!
In addition, it is not necessary to increase the virtual function in the derived class (VirtFunctClasschild in the above example) in the derived class of the DLL. Because if there is no Virtual Void Dosomething () function in the VirtfunctClassChild class, the void doanything () function in the base class (the second function in the virtual function table) call will point to an empty memory address (because the virtual virtuality of the VirtfunctClassChild " The function table only maintains a function address).
It can now be seen that adding virtual functions in the DLL export class is a serious problem! However, if the virtual function is used to handle the callback event, we have a way to solve this problem (hereinafter lists). COM and other
It can now be seen that the DLL backward compatibility problem is a very famous issue. Solving these issues, not only from some conventions, but also through other advanced technologies, such as COM technology. So if you want to get rid of "DLL Hell" problem, use COM technology or other suitable techniques.
Let's go back to the task I accept (the task I spent on this article) ---- Solve a backward compatibility problem of using DLL products.
I have some understanding of COM, so my first suggestion is to use COM technology to overcome all the questions in that project. But this suggestion was finally vetoed because of the following reasons: 1. That product has already had a COM server in an internal layer. 2. Write a large pile of interface to COM, and put it relatively large. 3. Because that product is a DLL library, and there are many applications that use it. Therefore, they don't want to force their customers to rewrite their app.
In other words, the task I have requested is to solve this DLL backward compatibility problem with the smallest price. Of course, I should point out that the most important issue of this project is to increase the new member and the virtual recovery function on the interface class. The first question can be simply solved by adding a pointer pointing to a data structure in a class declaration (which can add new members any additional). This method I have mentioned above. But the second problem, the problem with the virtual recovery function is new. Therefore, I propose the minimum price below, the most effective solution.
Demagical adjustment function and inheritance
However, we have a DLL, it exports several classes; the client application will derive new classes from these export classes to implement the virtual function to handle the callback event. We want to make a small change in the DLL. This change allows us to add new virtual recovery functions to the export class "painless" in the future. At the same time, we don't want to affect the application of the current version of the DLL. What we expect is that these applications only share a new version of DLL once again to recompile when they are not allowed. Therefore, I gave the following solution:
We can keep each virtual recovery function in the DLL export class. We only need to remember, add a new virtual function in any class definition, if the application is not co-compiling the new version of the DLL, will result in a serious problem. What we do is to avoid this problem. Here we can "monitor" mechanisms. If the virtual function defined in the DLL export class is used as a processing callback, we can transfer these virtual functions to a separate interface.
Let us look at the example below:
// If you want to test the DLL that is changed, let's release the following definition // # define dll_example_modified
#ifdef dll_export #define DLL_PREFIX __DECLSPEC (DLLEXPORT) #ELSE #define DLL_PREFIX __DECLSPEC (DLLIMPORT) #ENDIF
/ ********** DLL export class ********** / # define class_uiid_def static short getclassuiid () {return 0;} # define object_uiid_def virtual short getObjectuiid () {Return THIS -> getclassuiid ();
// All callback processed basic interface struct dll_prefix iCallback {class_uiid_def Object_uiid_def};
#undef class_uiid_def
#define class_uiid_def (x) public: static short getclassuiid () {return x :: getclassuiid () 1;} // Only when the DLL_EXAMPLE_MODIFIED macro has been defined, the interface extension #if defined (DLL_EXAMPLE_MODIFIED) // Added interface extension struct DLL_PREFIX ICallBack01: public ICallBack {CLASS_UIID_DEF (ICallBack) OBJECT_UIID_DEF virtual void DoCallBack01 (int event) = 0; // new callback}; # endif // defined (DLL_EXAMPLE_MODIFIED)
class DLL_PREFIX CExample {public: CExample () {mpHandler = 0;} virtual ~ CExample () {} virtual void DoCallBack (int event) = 0; ICallBack * SetCallBackHandler (ICallBack * handler); void Run (); private: ICallBack * Mphandler;
Obviously, in order to provide convenience to the extension of the extended DLL (add new virtual functions), we must do the following: 1. Increase ICallback * setCallbackHandler (ICALLBACK * HANDLER); function; 2. In each definition of each export class Add corresponding pointers; 3. Define 3 macros; 4. Define a universal iCallback interface.
In order to demonstrate the new virtual recovery function to the CEXAMPLE class, I add a definition of an iCallback01 interface here. Obviously, the new virtual recovery function should be added to the new interface. Each DLL update adds an interface (of course, each time the DLL is updated, we can also add multiple virtual recovery functions at the same time).
Note that each new interface must be inherited from the previous version of the interface. In my example, I only define an extension interface iCallback01. If the DLL will add a new virtual recovery function, we can define an icallback02 interface, pay attention to the ICallback02 interface to derive from the iCallback01 interface, just like the ICALLBACK01 interface is the same as the iCallback interface.
Several macros have also been defined in the above code to define functions that need to check the interface version. For example, we have to add new functions to ICallback01 for new interface iCallback01. If we want to call iCallback * Mphandler; if a member, you should check it on the CEXAMPLE class. This check should be implemented as follows:
IF (MPHANDLER! = null && mphandler-> getObjectuiid ()> = iCallback01 :: getClassuiid ()) {(iCallback01 *) mphandler) -> Docallback01 (2);}
We see that after the new callback interface increases, simply insert a new callback call in the implementation of the Cexample class.
Now you can see that our above changes will not affect the client application. The only thing you need to do is just the first DLL version after this new design (the macro definition is added to the DLL export class, the callback basic interface iCallback, set the callback processing setCallbackHandler function, and the ICALLBACK interface "pointer) Applications are compiled again. (Expand the new callback interface in the future, the application's recompilation is not required!) After someone wants to add new callback processing, he can implement the new interface (we add iCallback01 in an example). Obviously, this change does not cause any problems, because the order of the virtual function has not changed. Therefore, the application is still running in the previous way. The only thing you have to pay attention is that unless you implement a new interface in your application, you will receive no new callback calls.
We should notice that DLL users can still work with it. Below is an implementation example of a class in the client program:
! // DLL_EXAMPLE_MODIFIED If not defined, using the previous version of the DLL # if defined (DLL_EXAMPLE_MODIFIED) // this case not use the extended interface ICallBack01class CClient: public CExample {public: CClient (); void DoCallBack (int event);};
#ELLSE / /! Defined (DLL_EXAMPLE_MODIFIED) // After the DLL adds new interface iCallback01, the client can modify its own class // (but not necessarily, if he doesn't want to handle new callback event) Class Cclient: Public Cexample , public iCallback01 {public: cclient (); void Docallback (int event);
// Declare the DOCALLBACK01 function (the client is to implement it to handle new callback events) // (Docallback01 is the new virtual function of Icallback01 interface) Void Docallback01 (int event);}; # ENDIF // defined (DLL_EXAMPLE_MODIFIED)
Routines ---> Code Download (6.26K)
With the contents of this article, I have a demos DLL_HELL_SOLUTION.
1. DLL_EXAMPLE: DLL implementation item; 2. DLL_CLIENT_EXAMPLE: DLL client application project.
Note: Currently DLL_HELL_SOLUTION / DLL_EXAMPLE / DLL_EXAMPLE.H file DLL_EXAMPLE_MODIFIED definition is commented. If you release this comment, you can generate updated DLL versions; you can then test the client application again.
To ensure that the reader can demonstrate properly, follow the steps: 1. Do not change any code (this time DLL_EXAMPLE_MODIFIED is not defined) Compile DLL_EXAMPLE and DLL_CLIENT_EXAMPLE two projects. Run the customer program and experience the initial situation. 2. Release the annotation of DLL_EXAMPLE_MODIFIED and recompile DLL_EXAMPLE. Re-run the client (using a new version of the DLL at this point) should still run normal. 3. Re-build DLL_Client_example to generate a new client. We see the newly added callback function is called!