|
Antwort |
Registriert seit: 4. Jun 2010 15.473 Beiträge |
#1
Or: don't create your own DLLs without reading this article!
This article is not about EurekaLog, but about writing your own DLLs in general. This article is based on questions on the forums: "How do I return a string from a DLL?", "How do I pass and return an array of records?", "How do I pass a form to a DLL?". So that you do not spend half your life figuring it out - in this article I will bring everything on a silver platter. Important note: the article must be read sequentially. Code examples are given only as examples: the code of examples is added with new details at each step of the article. For example, at the very beginning of the article there is no error handling, "classic" methods are used (such as using GetLastError, the sdtcall convention, etc.), which are replaced by more adequate ones in the course of the article. This is done for the reason that "new" ("unusual") designs do not raise questions. Otherwise, with each example, one would have to insert a note like: "this is discussed in that paragraph below, but that - in this one here." In any case, at the end of the article there is a link to the sample code written by taking into account everything said in the article. You can just grab it and use it. And the article explains why it is created the way it is. If you are not interested in "why" - scroll to the end to the conclusion and find a link to download the example. Content
General Concepts When you develop your own DLL, you must come up with the prototypes of the functions exported from it (i.e. "headers"), as well as the contract based on them (i.e. calling rules). Together, this forms your DLL's API. API or Application Programming Interface (Application Programming Interface) is a description of the ways in which one code can interact with another. In other words, API is a tool for application integration. When you develop your DLL, you must determine under what conditions it will be used:
If you go this route, then you should consider using run-time packages (BPLs) instead of DLLs. BPL packages are specialized DLLs that are specifically tailored for use only in Delphi, which gives you a lot of goodies. But more on that later. On the other hand, if you're developing a "generic DLL" then you can't use features in your language that don't exist in other programming languages. And in this case, you can only use "well-known" data types. But more on that below. This article is mainly about "generic DLLs" in Windows. What you will need to create when developing your DLL API:
Data Types If you want a "generic DLL", then you can't use Delphi-specific data types in your API because they don't have a counterpart in other languages. For example, string, array of, TObject, TForm (and in general - any objects, and even more so components) and so on. What can be used? Integer types (Integer, Cardinal, Int64, UInt64, NativeInt, NativeUInt, Byte, Word etc., I wouldn't recommend using Currency unless you really need it), real types (Single and Double; I would recommend avoiding the Extended and Comp types unless you really need them), TDateTime (it is alias for system's OLEDate), enumerated and subrange types (with some caveats), character types (AnsiChar and WideChar - but not the Char), strings (only as WideString/BSTR), boolean (BOOL, but not the Boolean), interfaces (interface) whose methods use valid types, records (record) from the above types, and pointers to them (including pointers to arrays of the above types, but not dynamic arrays). Arrays are usually passed as two parameters: a pointer to the first element of the array and the number of elements in the array. How do you know which type can be used and which can't? A relatively simple rule - if you don't see a type in this list, and the type is not in the Windows unit (Winapi.Windows unit since Delphi XE2), then that type cannot be used. If the type is listed above or it is in the Windows/Winapi.Windows unit, use it. This is a rather rough rule, but it will do for a start. In case of using records (record) - you need to specify data alignment. Use either the packed keyword (no alignment) or the {$A8} directive (8-byte alignment) at the beginning of the header file. In case of using enumerated types (Color = (clRed, clBlue, clBlack);) - add the {$MINENUMSIZE 4} directive to the beginning of the headers (the size of the enumerated type is at least 4 bytes ). String Data and Encodings If you need to pass strings to the DLL or return strings from the DLL - use only the BSTR type. Why?
If for some reason you can't use the BSTR type then use PWideChar:
type LPWSTR = PWideChar; and then use LPWSTR. The LPWSTR is the name of the system data type, which is called PWideChar in Delphi. Of course, you get a bunch of cons when using LPWSTR/PWideChar instead of WideString:
ANSI and Unicode From the above, it directly follows that all your exported functions must be in Unicode. Do not just look at the Windows API to make two versions of functions (with -A and -W suffixes) - just make one version (no suffix, just Unicode). Yes, do that even if you are developing on the ANSI version of Delphi (like Delphi 7): you don't need to make ANSI versions of the exported functions. It's not 1995 now. Shared Memory Manager (and why you shouldn't use it) In programming languages, dynamic memory is allocated and deallocated by a special code in the program - the so-called memory manager. For example, the memory manager implements functions like GetMem and FreeMem in Delphi. All other memory management methods (New, SetLength, TForm.Create, etc.) are just adapters (i.e. somewhere internally they call GetMem and FreeMem). The problem is that each executable module (be it a DLL or an exe) has its own memory manager code, and, for example, the Delphi memory manager does not know anything about the Microsoft C++ memory manager (and vice versa). Therefore, if you allocate memory in Delphi and, for example, try to transfer it to Visual C++ code, then nothing good will happen. Moreover, even if you allocate memory in Delphi DLL and return it to Delphi exe, things will be even worse: both executable modules use two different, but same type memory managers. The exe memory manager will look at the memory and it will seem to him that this is his memory (after all, it is allocated by a similar memory manager), he will try to free it, but only damage the accounting data. The solution to this problem is simple - you need to use the rule: whoever allocates memory, frees it. This rule can be enforced in a number of ways. Often mentioned method: using the so-called shared memory manager. The essence of the method is that several modules "agree" to use the same memory manager. When you create a DLL, you are told about this feature by a comment at the beginning of the .dpr file of the DLL: { Important note about DLL memory management: ShareMem must be the first unit in your library's USES clause AND your project's (select Project-View Source) USES clause if your DLL exports any procedures or functions that pass strings as parameters or function results. This applies to all strings passed to and from your DLL--even those that are nested in records and classes. ShareMem is the interface unit to the BORLNDMM.DLL shared memory manager, which must be deployed along with your DLL. To avoid using BORLNDMM.DLL, pass string information using PChar or ShortString parameters. } This is a monstrously wrong comment:
What's wrong with using a shared memory manager?
API Memory Management So how do you transfer memory from the DLL to the caller and vice versa? There are several ways. Incorrect Way First, what not to do. First, don't "do it like Delphi": don't use a shared memory manager - for the reasons mentioned above. Secondly, don't "do it like Windows": many look at the Windows API and do the same. But they miss the fact that this API was created in 1995, and many functions come from even earlier: 16-bit Windows. Those environments and conditions for which these functions were created no longer exist today. Today there are much simpler and more convenient ways. For example, here is a typical Windows function: function GetUserName(lpBuffer: PWideChar; var nSize: DWORD): BOOL; stdcall; ParametersTo get a result from such a Windows function, it must be called twice. First you call the function to determine the size of the buffer, then you allocate the buffer, and only then you call the function again. But what if the data changes during this time? Function may run out of space again (on the second call). Thus, to reliably get the complete data, you have to write a loop. This is horror. Don't do that. Strings Strings are easy - just use the BSTR (which is WideString). We have discussed this above in details. Note that in some cases you can return complex structured data (objects) as JSON or a similar way of packing the data into a string. And if this is your case - you can also use the BSTR type. In all other cases, you need to use one of the three methods below. System Memory Manager You can fulfill the "who allocates memory - releases it" rule as follows: ask a third party to allocate and release memory, which both the called and the caller know about. For example, such a third party could be any system memory manager. This is exactly how BSTR/WideString works. Here are some options you can use:
Pretty big list. Which one is better to use?
Here is an example of how it might look in code. In DLL (simplified code without error handling): uses ActiveX; // or uses OLE2; function GetDynData(const AFlags: DWORD; out AData: Pointer; out ADataSize: DWORD): BOOL; stdcall; var P: array of Something; begin P := { ... prepare data to return ... }; ADataSize := Length(P) * SizeOf(Something); AData := CoTaskMemAlloc(ADataSize); Move(Pointer(P)^, AData^, ADataSize); Result := True; end; exe: uses ActiveX; // or uses OLE2; var P: array of Something; Data: Pointer; DataSize: DWORD; begin GetDynData(0, Data, DataSize); SetLength(P, DataSize div SizeOf(Something)); Move(Data^, Pointer(P)^, DataSize); CoTaskMemFree(Data); // Work with P end; Note: it is just an example. In real applications, you can (on the callee side) both prepare data immediately in the returned buffer (provided that you know its size in advance), and (on the caller side) work with the returned data directly, without copying it to another type of buffer. Of course, at the same time, your SDK should have documentation on the GetDynData function, which will explicitly say that the returned memory must be freed by calling CoTaskMemFree, like this: GetDynDataNote: of course, the CoTaskMemAlloc/CoTaskMemFree calls can be replaced with HeapAlloc/HeapFree or any other option convenient for you. Note that with this method, you typically need to copy the data twice: in the callee (to copy the data from the prepared location to a location suitable for return to the caller) and possibly in the caller (to copy the returned data into structures suitable for further use). Sometimes you can get away with a single copy if the caller can use the data right away. But it is rare to get rid of copying data in the callee. Dedicated Wrapper Functions Another option is to wrap your preferred memory manager in an exportable function. Accordingly, the documentation for the function should indicate that to free the returned memory, you need to call not CoTaskMemFree (or whatever you used there), but your wrapper function. Then you can simply return prepared data immediately, without copying. For example, in a DLL (simplified code without error handling): function GetDynData(const AFlags: DWORD; out AData: Pointer; out ADataSize: DWORD): BOOL; stdcall; var P: array of Something; begin P := { ... prepare your data ... }; ADataSize := Length(P) * SizeOf(Something); Pointer(AData) := Pointer(P); // copy the pointer, not the data itself Pointer(P) := nil; // block the auto-release Result := True; end; procedure DynDataFree(var AData: Pointer); stdcall; var P: array of Something; begin if AData = nil then Exit; Pointer(P) := Pointer(AData); // and again: copy just the pointer AData := nil; Finalize(P); // a matching release function // (it is optional in this particular case) end; ? exe: var P: array of Something; Data: Pointer; DataSize: DWORD; begin GetDynData(0, Data, DataSize); SetLength(P, DataSize div SizeOf(Something)); Move(Data^, Pointer(P)^, DataSize); DynDataFree(Data); // Work with P end; Note: we can't just copy the pointer to the array on the caller's side because the GetDynData contract says nothing about the compatibility of the returned data with Delphi's dynamic array. Indeed, a DLL can be written in MS Visual C++, which does not have dynamic arrays. As in the previous case, this contract must also be explicitly stated in your SDK's documentation: AData [out]Note that by using a wrapper function you can reduce the amount of data copying, because now you don't need to copy the data on the caller's side, because you use the same memory manager for both calculations and for returning the data. The disadvantage of this method is the need to write additional wrapper functions. Sometimes you can get away with one generic wrapper function common to all exported functions. But more often than not, you will need an individual cleanup function for each exported function (returning data) if you want to use "just one copy" advantage. If you use one generic cleanup function, you can return it as the IMalloc interface. This will be more familiar to those familiar with the basics of COM. But it will also allow you not only to return memory to the caller, but also to accept memory from the caller with transfer of ownership. For example: uses ActiveX; // or Ole2 type TAllocator = class(TInterfacedObject, IMalloc) function Alloc(cb: Longint): Pointer; stdcall; function Realloc(pv: Pointer; cb: Longint): Pointer; stdcall; procedure Free(pv: Pointer); stdcall; function GetSize(pv: Pointer): Longint; stdcall; function DidAlloc(pv: Pointer): Integer; stdcall; procedure HeapMinimize; stdcall; end; { TAllocator } function TAllocator.Alloc(cb: Integer): Pointer; begin Result := AllocMem(cb); end; function TAllocator.Realloc(pv: Pointer; cb: Integer): Pointer; begin ReallocMem(pv, cb); Result := pv; end; procedure TAllocator.Free(pv: Pointer); begin FreeMem(pv); end; function TAllocator.DidAlloc(pv: Pointer): Integer; begin Result := -1; end; function TAllocator.GetSize(pv: Pointer): Longint; begin Result := -1; end; procedure TAllocator.HeapMinimize; begin // does nothing end; function GetMalloc(out AAllocator: IMalloc): BOOL; stdcall; begin AAllocator := TAllocator.Create; Result := True; end; //_______________________________________ function GetDynData(const AOptions: Pointer; out AData: Pointer; out ADataSize: DWORD): BOOL; stdcall; var P: array of Something; begin P := { ... prepare the data with AOptions ... }; // Assume the AOptions was passed with ownership FreeMem(AOptions); ADataSize := Length(P) * SizeOf(Something); AData := GetMem(ADataSize); Move(Pointer(P)^, Pointer(AData)^, ADataSize); Result := True; end; exe: var A: IMalloc; Options: Pointer; P: array of Something; Data: Pointer; DataSize: DWORD; begin GetMalloc(A); Options := A.Alloc({ options' size }); { Preparing Options } GetDynData(Options, Data, DataSize); // Do not free Options, because we have passed ownership to the GetDynData SetLength(P, DataSize div SizeOf(Something)); Move(Data^, Pointer(P)^, DataSize); A.Free(Data); // Work with P end; Note: of course, it is a bit of a nonsensical example, because in this particular case there is no need to pass the ownership of the AOptions to the GetDynData function: the caller can clean up the memory himself, then the callee will not need to free the memory. But it is just an example. In real applications, you may need to keep AOptions inside the DLL for longer than the function's call. The example shows how this can be done by hiding the memory manager behind a facade. Also note that if you implement the TAllocator.GetSize method, then the ADataSize parameter can be removed. Interfaces Instead of using the system memory manager and/or special export functions (two ways above), it is much more convenient to use interfaces for the following reasons:
type IData = interface ['{C79E39D8-267C-4726-98BF-FF4E93AE1D44}'] function GetData: Pointer; stdcall; function GetDataSize: DWORD; stdcall; property Data: Pointer read GetData; property DataSize: DWORD read GetDataSize; end; TData = class(TInterfacedObject, IData) private FData: Pointer; FDataSize: DWORD; protected function GetData: Pointer; stdcall; function GetDataSize: DWORD; stdcall; public constructor Create(const AData: Pointer; const ADataSize: DWORD); end; constructor TData.Create(const AData: Pointer; const ADataSize: DWORD); begin inherited Create; if ADataSize > 0 then begin GetMem(FData, ADataSize); Move(AData^, FData^, ADataSize); end; end; function TData.GetData: Pointer; stdcall; begin Result := FData; end; function TData.GetDataSize: DWORD; stdcall; begin Result := FDataSize; end; //________________________________ function GetDynData(const AFlags: DWORD; out AData: IData): BOOL; stdcall; var P: array of Something; begin P := { ... preparing the data ... }; AData := TData.Create(Pointer(P), Length(P) * SizeOf(Something)); Result := True; end; ? exe: var P: array of Something; Data: IData; begin GetDynData(0, Data); SetLength(P, Data.DataSize div SizeOf(Something)); Move(Data^, Data.Data^, Data.DataSize); // Work with P end; In this case, we have made one universal IData interface that can be written once and be used in all functions. Although it does not require writing special code for every function, it also results in data copying on the side of the callee, as well as lack of typing. Here's what an improved DLL might look like: type IData = interface ['{C79E39D8-267C-4726-98BF-FF4E93AE1D44}'] function GetData: Pointer; stdcall; function GetDataSize: DWORD; stdcall; property Data: Pointer read GetData; property DataSize: DWORD read GetDataSize; end; TSomethingArray = array of Something; TSomethingData = class(TInterfacedObject, IData) private FData: TSomethingArray; FDataSize: DWORD; protected function GetData: Pointer; stdcall; function GetDataSize: DWORD; stdcall; public constructor Create(var AData: TSomethingArray); end; constructor TSomethingData.Create(var AData: TSomethingArray); begin inherited Create; FDataSize := Length(AData) * SizeOf(Something); if FDataSize > 0 then begin Pointer(FData) := Pointer(AData); Pointer(AData) := nil; end; end; function TSomethingData.GetData: Pointer; stdcall; begin Result := Pointer(FData); end; function TSomethingData.GetDataSize: DWORD; stdcall; begin Result := FDataSize; end; function GetDynData(const AFlags: DWORD; out AData: IData): BOOL; stdcall; var P: TSomethingArray; begin P := { ... preparing the data ... }; AData := TSomethingData.Create(P); Result := True; end; In this case, the outer wrapper (that is, the interface) remains unchanged, only the DLL code changes. So the caller's code (in the exe) doesn't change either. But if you change the contract (interface), then you can do the following: type ISomethingData = interface ['{CF8DF791-1E8D-4363-94A2-9FF035A9015A}'] function GetData: Pointer; stdcall; function GetDataSize: DWORD; stdcall; function GetCount: Integer; stdcall; function GetItem(const AIndex: Integer): Something; stdcall; property Data: Pointer read GetData; property DataSize: DWORD read GetDataSize; property Count: Integer read GetCount; property Items[const AIndex: Integer]: Something read GetItem; default; end; TSomethingArray = array of Something; TSomethingData = class(TInterfacedObject, ISomethingData) private FData: TSomethingArray; FDataSize: DWORD; protected function GetData: Pointer; stdcall; function GetDataSize: DWORD; stdcall; function GetCount: Integer; stdcall; function GetItem(const AIndex: Integer): Something; stdcall; public constructor Create(var AData: TSomethingArray); end; constructor TSomethingData.Create(var AData: TSomethingArray); begin inherited Create; FDataSize := Length(AData) * SizeOf(Something); if FDataSize > 0 then begin Pointer(FData) := Pointer(AData); Pointer(AData) := nil; end; end; function TSomethingData.GetData: Pointer; stdcall; begin Result := Pointer(FData); end; function TSomethingData.GetDataSize: DWORD; stdcall; begin Result := FDataSize; end; function TSomethingData.GetCount: Integer; stdcall; begin Result := Length(FData); end; function TSomethingData.GetItem(const AIndex: Integer): Something; stdcall; begin Result := FData[AIndex]; end; function GetDynData(const AFlags: DWORD; out AData: ISomethingData): BOOL; stdcall; var P: TSomethingArray; begin P := { ... preparing the data ... }; AData := TSomethingData.Create(P); Result := True; end; and then the caller turns into this: var Data: ISomethingData; begin GetDynData(0, Data); // No need to copy, just work with Data: for X := 0 to Data.Count do AddToList(Data[X]); end; In general, there are quite wide possibilities, you can do almost anything you want. And even if you first made a contract through IData, then later you can add ISomethingData by simply extending the interface with inheritance. However, older clients of your version 1 DLL will use IData, while version 2 clients may request the more convenient ISomethingData. As you can see from the code above, interfaces are more useful the more complex the returned data. It is very easy to return complex objects as interfaces, while returning a simple block of memory means writing a lot of code. The obvious downside is the need to write more code for interfaces, since you need a thunk object to implement the interface. But this minus is easily neutralized by the next paragraph (see "Error Handling" below). It's also partially removed if you originally need to return an object (because it doesn't require a thunk object, the returned object itself can implement the interface). Note: the code above is just an example. In real code, you need to add error handling and move the IData/ISomethingData interface definitions to separate files (your SDK headers). Error Handling (and calling convention) When a programmer writes code, he determines the sequence of actions in the program, placing operators, function calls, and so on in the right order. At the same time, the implemented sequence of actions corresponds to the logic of the algorithm: first we do this, then this, and finally this. The main code corresponds to the "ideal" situation, when all files are in their places, all variables have valid values, and so on. But during the actual operation of the program, situations inevitably occur when the code written by the programmer will operate in an unacceptable (and sometimes unforeseen) environment. Such (and some other) situations are called by the generalized word "error". Therefore, the programmer must somehow determine what he will do in such situations. How will he determine the admissibility of the situation, how to react to it, etc. As a rule, the minimum blocks subject to control are a function or procedure (subroutine). Each subroutine performs a specific task. And we can expect a different level of "success" for this task: the task was successful or an error occurred during its execution. To write reliable code, we absolutely need a way to detect error situations - how do we determine that an error has occurred in a function? And responding to them is the so-called "error recovery" (i.e.: what will we do when an error occurs?). Traditionally, there are two main ways to handle errors: error codes and exceptions. Error codes (and why not to use them) Error codes are perhaps the easiest way to respond to errors. Its essence is simple: the subroutine must return some sign of the success of the task. There are two options here: either it will return a simple sign (success/failure), or it will return the execution status (in other words - "error description"), i.e. a certain code (number) of one of several predefined situations: the function parameters are incorrectly set, the file is not found, etc. In the first case, there may be an additional function that returns the execution status of the last function called. With this approach, errors found in the function are usually passed up (to the calling function). Each function must check the results of other function calls for errors and perform appropriate processing. Most often, the processing is simply passing the error code even higher, to the "higher" calling function. For example: function A calls B, B calls C, C detects an error and returns an error code to B. B checks the return code, sees that an error occurred, and returns an error code to A. A checks the return code and issues an error message (or decides to do something else). For example, here is a typical Windows API function: RegisterClassExIt is a typical way to handle errors in the classic Windows API. In this case: the so-called Win32 error codes. The Win32 error code is a usual DWORD number. Error codes are fixed and declared in the Windows unit. The absence of an error is taken as ERROR_SUCCESS or NO_ERROR (equal to 0). Constants are defined for all possible errors. Those begin (usually) with the ERROR_ word, for example: { Incorrect function. } ERROR_INVALID_FUNCTION = 1; { dderror } { The system cannot find the file specified. } ERROR_FILE_NOT_FOUND = 2; { The system cannot find the path specified. } ERROR_PATH_NOT_FOUND = 3; { The system cannot open the file. } ERROR_TOO_MANY_OPEN_FILES = 4; { Access is denied. } ERROR_ACCESS_DENIED = 5; { The handle is invalid. } ERROR_INVALID_HANDLE = 6; // ... and so on A description of the Win32 error can be obtained via the FormatMessage function. There is (specifically for our case) a more convenient wrapper in Delphi for this system function with a bunch of parameters: the SysErrorMessage function. It returns the human-readable description of the passed Win32 error code. By the way, note that messages are returned localized. In other words, if you have Russian Windows, then the messages will be in Russian. If English - in English. Summarizing what has been said, you have to call such functions like this: { prepare the WndClass } ClassAtom := RegisterClassEx(WndClass); if ClassAtom = 0 then begin // some error occurred, failure reason is indicated by GetLastError Application.MessageBox( PChar('There was an error: ' + SysErrorMessage(GetLastError)), PChar('Error'), MB_OK or MB_ICONSTOP); Exit; end; // ... continue normal execution As in the memory management case - it is the same: do not follow the example of Windows. This style has long been outdated. And here's what's wrong with him (it is not a complete list):
Despite all the disadvantages, error codes have a plus: since they are just numbers, they are understandable to any programming language. In other words, error codes are compatible between different languages. Exceptions (and why not to use them) Exceptions don't have many of the disadvantages of error codes:
But despite all the pluses, exceptions have one significant minus, which crosses out all the pluses (in relation to the DLL API). Recall how exceptions are raised in Delphi: var E: Exception; begin E := EMyExceptionClass.Create('Something'); raise E; end; I split the typical "raise EMyExceptionClass.Create('Something');" line into two to make the problem even more obvious. We create a Delphi object (exception) and "throw" it. And whoever wants to handle this exception does this: except on E: EMyException do begin ShowMessage(E.Message); end; // - E will be deleted here end; It means that the Delphi object is passed from the callee (DLL) where the exception is thrown to the caller (exe) where the exception is handled. As we learned earlier (see the Data Types section above), this is a problem. Other programming languages don't know what a Delphi object is, nor how to read it, nor how to delete it. Even Delphi itself doesn't always know this (for example, if an exception is thrown by code built on Delphi 7 but caught by code built on Delphi XE, or vice versa). Other programming languages use similar constructs: an exception is represented by an object. Accordingly, Delphi code has no idea how to work with objects in other languages. In other words, exceptions should not be used due to language incompatibilities. Corollary 1: Exceptions should not leave your DLL. Corollary 2: You must catch all exceptions in your exported functions. Corollary 3: all exported functions must have the global construct try-except. function GetDynData(const AFlags: DWORD; out AData: IData): BOOL; stdcall; begin try // ... function's code, useful payload ... Result := True; except // ... exception handling ... Result := False; end; end; What can and should be used (and which calling convention to use) If we can't use error codes and we can't use exceptions, then what should we use? Well, we need to use their combination - and here is how it looks. Delphi has built-in compiler magic that can wrap any function in a hidden try-except block with an automatic (hidden) call to the processing function. And there is a compiler magic that works the other way around: on the returned error code, it automatically raises the appropriate exception. Before we get to know this magic, we need to get acquainted with error codes in the form of the HRESULT type. The HRESULT is also a number, but now of the Integer type. The HRESULT is no longer just an error code, it consists of several parts, which we will not go into in detail, but suffice it to say that they include the error code itself (what used to be Win32 code), a sign success or failure, identifier of the error agent. Error codes typically start with the E_ prefix (for example, E_FAIL, E_UNEXPECTED, E_ABORT or E_ACCESSDENIED), and success codes typically start with the S_ prefix (for example, S_OK or S_FALSE). It is easy to determine the success of the HRESULT code by comparing it with zero: HRESULT error codes must be less than zero. Highlighting the success/error indicator means that now there is no need for the function to return only the success/failure (via BOOL), and the error code itself - through a separate function (GetLastError). Now the function can return both information at once, in one call: function GetDynData(const AFlags: DWORD; out AData: IData): HRESULT; stdcall; begin try // ... function's code, useful payload ... Result := S_OK; except // ... exception handling ... Result := E_FAIL; // some error code end; end; Along with the introduction of the HRESULT type, the IErrorInfo interface was added - which allows you to associate additional information with the returned HRESULT: arbitrary description, GUID of the raising party (interface), the location of the error (arbitrary line), help. You don't even need to implement this interface, the system already has an object ready - returned by the CreateErrorInfo function. Finally, Delphi has the already mentioned compiler magic that can make writing such code easier. To do this, the function must have a calling convention stdcall and return the HRESULT type. If before the function returned some Result, then this Result should be converted to the last out-parameter, for example: // Was: function GetDynData(const AFlags: DWORD): IData; // Became: function GetDynData(const AFlags: DWORD; out AData: IData): HRESULT; stdcall; If a function satisfies these requirements, then you can declare it like this: function GetDynData(const AFlags: DWORD): IData; safecall; This would be binary equivalent (i.e. fully compatible) to: function GetDynData(const AFlags: DWORD; out AData: IData): HRESULT; stdcall; By declaring a function as safecall you turn on compiler magic for it, namely:
How to use safecall correctly Now that we've covered the benefits of the safecall, it's time for a fly in the ointment. The fact is that the safecall magic works in a minimal mode "out of the box". And to get maximum benefit from it, we need to take additional steps. Luckily, they only need to be made once and can be reused in the future. Item number one: simple exported functions: procedure DoSomething; safecall; begin // ... function's code, useful payload ... end; exports DoSomething; Unfortunately, the compiler does not allow customizing the process of converting an exception to HRESULT for ordinary functions, always returning a fixed code and losing additional error information. Therefore, instead of exported functions, you need to use interfaces with methods. Before: procedure DoSomething; safecall; begin // ... function's code, useful payload ... end; function GetDynData(const AFlags: DWORD): IData; safecall; begin // ... function's code, useful payload ... end; function DoSomethingElse(AOptions: IOptions): BSTR; safecall; begin // ... function's code, useful payload ... end; exports DoSomething, GetDynData, DoSomethingElse; After: type IMyDLL = interface ['{C5DBE4DC-B4D7-475B-9509-E43193796633}'] procedure DoSomething; safecall; function GetDynData(const AFlags: DWORD): IData; safecall; function DoSomethingElse(AOptions: IOptions): BSTR; safecall; end; TMyDLL = class(TInterfacedObject, IMyDLL) protected procedure DoSomething; safecall; function GetDynData(const AFlags: DWORD): IData; safecall; function DoSomethingElse(AOptions: IOptions): BSTR; safecall; end; procedure TMyDLL.DoSomething; safecall; begin // ... function's code, useful payload ... end; function TMyDLL.GetDynData(const AFlags: DWORD): IData; safecall; begin // ... function's code, useful payload ... end; function TMyDLL.DoSomethingElse(AOptions: IOptions): BSTR; safecall; begin // ... function's code, useful payload ... end; function GetFunctions(out AFunctions: IMyDLL): HRESULT; stdcall; begin try AFunctions := TMyDLL.Create; Result := S_OK; except on E: Exception do Result := HandleSafeCallException(E, ExceptAddr); end; end; exports GetFunctions; where HandleSafeCallException is our function, which we will describe below. As you can see, we have placed all exported functions in a single interface (object) - this will allow us to set up/control the process of converting exceptions to HRESULT. In this case, the DLL exports the only function that we had to write manually, without safecall - which also allowed us to control the conversion process. Don't forget that it is binary compatible with safecall, so if you want to use this DLL in Delphi you can do this: function GetFunctions: IMyDLL; safecall; external 'MyDLL.dll'; and it will work just fine. For objects, when an exception is thrown in a safecall method, the compiler calls the TObject.SafeCallException virtual method which does nothing useful by default and which we can replace with our own method: type TMyDLL = class(TInterfacedObject, IMyDLL) protected procedure DoSomething; safecall; function GetDynData(const AFlags: DWORD): IData; safecall; function DoSomethingElse(AOptions: IOptions): BSTR; safecall; public function SafeCallException(ExceptObject: TObject; ExceptAddr: Pointer): HResult; override; end; function TMyDLL.SafeCallException(ExceptObject: TObject; ExceptAddr: Pointer): HResult; begin Result := HandleSafeCallException(ExceptObject, ExceptAddr); end; Further, when the code calls a safecall method, the compiler wraps the method's call in a CheckAutoResult wrapper, which (in case of erroneous code) raises an exception through the SafeCallErrorProc global variable function, which, again, we can replace with our own: procedure RaiseSafeCallException(ErrorCode: HResult; ErrorAddr: Pointer); begin // ... our code ... end; initialization SafeCallErrorProc := RaiseSafeCallException; end. Now we just need to make our HandleSafeCallException and RaiseSafeCallException work as a pair and do something useful. First we need two helper wrapper functions: uses ActiveX; // or Ole2 function SetErrorInfo(const ErrorCode: HRESULT; const ErrorIID: TGUID; const Source, Description, HelpFileName: WideString; const HelpContext: Integer): HRESULT; var CreateError: ICreateErrorInfo; ErrorInfo: IErrorInfo; begin Result := E_UNEXPECTED; if Succeeded(CreateErrorInfo(CreateError)) then begin CreateError.SetGUID(ErrorIID); if Source '' then CreateError.SetSource(PWideChar(Source)); if HelpFileName '' then CreateError.SetHelpFile(PWideChar(HelpFileName)); if Description '' then CreateError.SetDescription(PWideChar(Description)) ; if HelpContext 0 then CreateError.SetHelpContext(HelpContext); if ErrorCode 0 then Result := ErrorCode; if CreateError.QueryInterface(IErrorInfo, ErrorInfo) = S_OK then ActiveX.SetErrorInfo(0, ErrorInfo); end; end; procedure GetErrorInfo(out ErrorIID: TGUID; out Source, Description, HelpFileName: WideString; out HelpContext: Longint); var ErrorInfo: IErrorInfo; begin if ActiveX.GetErrorInfo(0, ErrorInfo) = S_OK then begin ErrorInfo.GetGUID(ErrorIID); ErrorInfo.GetSource(Source); ErrorInfo.GetDescription(Description); ErrorInfo.GetHelpFile(HelpFileName); ErrorInfo.GetHelpContext(HelpContext); end else begin FillChar(ErrorIID, SizeOf(ErrorIID), 0); Source := ''; Description := ''; HelpFileName := ''; HelpContext := 0; end; end; As you can easily imagine, they are intended to pass and receive additional information along with HRESULT. Next, we need a way to somehow pass the class name of the exception. You can do this in different ways. For example, pass it directly to HRESULT. To do this, it needs to be encoded. For example, like this: uses ComObj, // for the EOleSysError and EOleException VarUtils; // for the ESafeArrayError const // ID for our DLL API rules ThisDllIID: TGUID = '{AA76E538-EF3C-4F35-9914-B4801B211A6D}'; // "Customer" bit, it is always 0 for Microsoft-defined codes CUSTOMER_BIT = 1 shl 29; // Delphi uses this value to pass EAbort // It is assumed that E_Abort should show "Aborted" message, // while EAbortRaisedHRESULT should be handled silently. EAbortRaisedHRESULT = HRESULT(E_ABORT or CUSTOMER_BIT); function Exception2HRESULT(const E: TObject): HRESULT; function NTSTATUSFromException(const E: EExternal): DWORD; begin // ... end; begin if E = nil then Result := E_UNEXPECTED else if not E.InheritsFrom(Exception) then Result := E_UNEXPECTED else if E.ClassType = Exception then Result := E_FAIL else if E.InheritsFrom(ESafecallException) then Result := E_FAIL else if E.InheritsFrom(EAssertionFailed) then Result := E_UNEXPECTED else if E.InheritsFrom(EAbort) then Result := EAbortRaisedHRESULT else if E.InheritsFrom(EOutOfMemory) then Result := E_OUTOFMEMORY else if E.InheritsFrom(ENotImplemented) then Result := E_NOTIMPL else if E.InheritsFrom(ENotSupportedException) then Result := E_NOINTERFACE else if E.InheritsFrom(EOleSysError) then Result := EOleSysError(E).ErrorCode else if E.InheritsFrom(ESafeArrayError) then Result := ESafeArrayError(E).ErrorCode else if E.InheritsFrom(EOSError) then Result := HResultFromWin32(EOSError(E).ErrorCode) else if E.InheritsFrom(EExternal) then if Failed(HRESULT(EExternal(E).ExceptionRecord.Except ionCode)) then Result := HResultFromNT(Integer(EExternal(E).ExceptionRecord .ExceptionCode)) else Result := HResultFromNT(Integer(NTSTATUSFromException(EExter nal(E)))) else Result := MakeResult(SEVERITY_ERROR, FACILITY_ITF, Hash(E.ClassName)) or CUSTOMER_BIT; end; Here we are checking for a few special predefined classes, and we also have the ability to pass Win32 codes and hardware exception codes directly. For all other (Delphi specific) exception classes, we use the hash on the class name along with FACILITY_ITF. As a hash, you can use, for example, SDBM - this is a very simple hash function with good randomization of the result. Of course, you can use any other method - for example, just manually extract and fix the codes for each exception class. HRESULTs with FACILITY_NULL and FACILITY_RPC codes have a generic value because they are defined by Microsoft. HRESULT with FACILITY_ITF code are defined by the interface function or method from which they are returned. This means that the same 32-bit value in FACILITY_ITF but returned by two different interfaces can have different meanings. In this way, Microsoft can define multiple generic error codes while still allowing other programmers to define new error codes without fear of conflict. The coding convention looks like this:That's why in the code above we also defined ThisDllIID which is an "interface" identifier that gives meaning to returned codes of type FACILITY_ITF. This value must be passed as ErrorIID to the SetErrorInfo defined above. The 29th "Customer" bit was originally a reserved bit, which was later allocated to be used as a flag indicating whether the code is defined by Microsoft (0) or by a third party (1). In a way, this bit duplicates FACILITY_ITF. Usually even third party developers only use FACILITY_ITF. In this case, we set it to reduce possible problems with bad code (which does not take into account the GUID of the interface). Wverything is a little more complicated with the reverse conversion (code to exception): we need tables to search for the exception class by code. A simple implementation might look like this: function HRESULT2Exception(const E: HRESULT): Exception; function MapNTStatus(const ANTStatus: DWORD): ExceptClass; begin // ... end; function MapException(const ACode: DWORD): ExceptClass; begin // ... end; var NTStatus: DWORD; ErrorIID: TGUID; Source: WideString; Description: WideString; HelpFileName: WideString; HelpContext: Integer; begin if GetErrorInfo(ErrorIID, Source, Description, HelpFileName, HelpContext) then begin if Pointer(StrToInt64Def(Source, 0)) nil then ErrorAddr := Pointer(StrToInt64(Source)); end else Description := SysErrorMessage(DWORD(E)); if (E = E_FAIL) or (E = E_UNEXPECTED) then Result := Exception.Create(Description) else if E = EAbortRaisedHRESULT then Result := EAbort.Create(Description) else if E = E_OUTOFMEMORY then begin OutOfMemoryError; Result := nil; end else if E = E_NOTIMPL then Result := ENotImplemented.Create(Description) else if E = E_NOINTERFACE then Result := ENotSupportedException.Create(Description) else if HResultFacility(E) = FACILITY_WIN32 then begin Result := EOSError.Create(Description); EOSError(Result).ErrorCode := HResultCode(E); end else if E and FACILITY_NT_BIT 0 then begin // Get exception's class by code NTStatus := Cardinal(E) and (not FACILITY_NT_BIT); Result := MapNTStatus(NTStatus).Create(Description); // Create a dummy ExceptionRecord just in case ReallocMem(Pointer(Result), Result.InstanceSize + SizeOf(TExceptionRecord)); EExternal(Result).ExceptionRecord := Pointer(NativeUInt(Result) + Cardinal(Result.InstanceSize)); FillChar(EExternal(Result).ExceptionRecord^, SizeOf(TExceptionRecord), 0); EExternal(Result).ExceptionRecord.ExceptionCode := cDelphiException; EExternal(Result).ExceptionRecord.ExceptionAddress := ErrorAddr; end else if (E and CUSTOMER_BIT 0) and (HResultFacility(E) = FACILITY_ITF) and CompareMem(@ThisDllIID, @ErrorIID, SizeOf(ErrorIID)) then Result := MapException(HResultCode(E)).Create(Description) else Result := EOleException.Create(Description, E, Source, HelpFileName, HelpContext); end; In general, the code is fairly straightforward, with the exception of hardware exceptions. We make emulation for them. Also note that the Source field of the IErrorInfo interface must point to the location where the error occurred. This field is arbitrary and is determined by the interface developer (ie, again, by GUID). In this case, we just write the address of the exception there. But, for example, if you use an exception tracer (such as EurekaLog), you can write the call stack there. Then with the above helper functions, our HandleSafeCallException and RaiseSafeCallException become trivial: function HandleSafeCallException(ExceptObj: TObject; ErrorAddr: Pointer): HRESULT; var ErrorMessage: String; HelpFileName: String; HelpContext: Integer; begin if ExceptObj is Exception then ErrorMessage := Exception(ExceptObj).Message else ErrorMessage := SysErrorMessage(DWORD(E_FAIL)); if ExceptObj is EOleException then begin HelpFileName := EOleException(ExceptObj).HelpFile; HelpContext := EOleException(ExceptObj).HelpContext; end else begin HelpFileName := ''; if ExceptObj is Exception then HelpContext := Exception(ExceptObj).HelpContext else HelpContext := 0; end; Result := SetErrorInfo(Exception2HRESULT(ExceptObj), ThisDllIID, '$' + IntToHex(NativeUInt(ErrorAddr), SizeOf(ErrorAddr) * 2), ErrorMessage, HelpFileName, HelpContext); end; procedure RaiseSafeCallException(ErrorCode: HResult; ErrorAddr: Pointer); var E: Exception; begin E := HRESULT2Exception(ErrorCode, ErrorAddr); raise E at ErrorAddr; end; Note: in our model, we do not use help fields of the IErrorInfo interface. It should be noted that if an interface uses HRESULT and IErrorInfo together, then it should also implement the ISupportErrorInfo interface. Some programming languages require this. By calling ISupportErrorInfo.InterfaceSupportsErrorInfo, the client side can determine that an object supports additional information. And the last point - in the Delphi implementation for Windows 32-bit there is nasty bug that doesn't exist in 64-bit RTL, as well as on other platforms. The fix for this bug is included in the code examples at the link at the end of the article. DllMain Workaround The DllMain is a special function in a DLL that is called by the system when the DLL is loaded into, unloaded from a process (and attached/detached to/from a thread). For example, the initialization and finalization sections of your Delphi modules are executed inside DllMain. The problem is that DllMain is a very special function. It is called while holding the critical section of the loader (modules) of the operating system. In long and detailed terms - see the links at the end of this paragraph, and in short: DllMain is a weapon from which you can easily shoot yourself. There are not many things that can be done legally in DllMain. But it's incredibly easy to do something forbidden - you constantly need to be sure that this very function that you just called can never, under any circumstances, do something forbidden. This makes it incredibly difficult to use code written elsewhere. The compiler won't tell you anything. And the code will most of the time work like it should... but sometimes it will crash or freeze. The solution to the problem is to do nothing in DllMain (read: don't write code in initialization and finalization sections of your units when you create a DLL). Instead, you need to make separate DLL initialization and finalization functions. You need to do them even if your DLL doesn't need any initialization or cleanup steps. After all, such a need may arise in the future, and if you do not provide separate initialization and finalization functions in your API, you will not be able to solve this problem later. Here is the code template: // In headers: type IMyDll = interface ['{C5DBE4DC-B4D7-475B-9509-E43193796633}'] procedure InitDLL(AOptional: IUnknown = nil); safecall; procedure DoneDLL; safecall; // ... end; // In DLL: type TInitFunc = procedure(const AOptional: IUnknown); TDoneFunc = procedure; TInitDoneFunc = record Init: TInitFunc; Done: TDoneFunc; end; procedure RegisterInitFunc(const AInitProc: TInitFunc; const ADoneFunc: TDoneFunc = nil); // ... var GInitDoneFuncs: array of TInitDoneFunc; procedure RegisterInitFunc(const AInitProc: TInitFunc; const ADoneFunc: TDoneFunc); begin SetLength(GInitDoneFuncs, Length(GInitDoneFuncs) + 1); GInitDoneFuncs[High(GInitDoneFuncs)].Init := AInitProc; GInitDoneFuncs[High(GInitDoneFuncs)].Done := ADoneFunc; end; procedure TMyDLL.InitDLL(AOptional: IUnknown); safecall; var X: Integer; begin for X := 0 to High(GInitDoneFuncs) do if Assigned(GInitDoneFuncs[X].Init) then GInitDoneFuncs.Init(AOptional); end; procedure TMyDLL.DoneDLL; safecall; var X: Integer; begin for X := 0 to High(GInitDoneFuncs) do if Assigned(GInitDoneFuncs[X].Done) then GInitDoneFuncs.Done; end; // In your units: procedure InitUnit(const AOptional: IUnknown); begin // ... code from unit's initialization sections end; procedure DoneUnit; begin // ... code from unit's finalization section end; initialization RegisterInitFunc(InitUnit, DoneUnit); end; The AOptional parameter is designed for possible future use. It is not used in the code above, but later (in the next version of the DLL) you can use it to pass initialization parameters. IUnknown is the base interface from which all other interfaces are inherited (i.e. some analogue of TObject for interfaces). I hope this code is clear enough. Of course, it must be distributed among different units and sections. Interface - in headers, RegisterInitFunc declaration - in interface of the common DLL module, you need to call it from the initialization section of other units. Of course, your SDK documentation should say that the user (client) of your DLL must call the InitDLL method immediately after loading your DLL with the LoadLibrary function and call the DoneDLL just before the DLL is unloaded by FreeLibrary: var DLL: HMODULE; DLLApi: IMyDll; begin DLL := LoadLibrary('MyDLL.dll'); Win32Check(DLL 0); try DLLApi.InitDLL(nil); // working with DLL, for example, calling DLLApi.GetDynData finally DLLApi.DoneDLL; DLLApi := nil; FreeLibrary(DLL); end; end; More information about the DllMain:
Callback Functions A callback function - passing the executable code as one of the parameters of another code. For example, if you want to set a timer using the Windows API, you can call SetTimer function, passing it a pointer to your own function, which will be the callback function. The system will call your function every time the timer fires: procedure MyTimerHandler(Wnd: HWND; uMsg: UINT; idEvent: UINT_PTR; dwTime: DWORD); stdcall; begin // Will be called after 100 ms timeout end; procedure TForm1.Button1Click(Sender: TObject); begin SetTimer(Handle, 1, 100, @MyTimerHandler); end; Here's another example: if you want to find all windows on the desktop, you can use the EnumWindows function: function MyEnumFunc(Wnd: HWND; lpData: LPARAM): Bool; stdcall; begin // Will be called for each found window end; procedure TForm1.Button1Click(Sender: TObject); begin EnumWindows(@MyEnumFunc, 0); end; Since the callback function usually performs the same task as the code that sets it up, it turns out that both pieces of code need to work with the same data. Therefore, the data from the setting code must somehow be passed to the callback function (or visa versa). For this purpose, so-called user parameters are provided in the callback functions: it is either a pointer or an integer (necessarily of the Native(U)Int type, but not just (U)Int), which are not used by the API itself in any way and are transparently passed to the callback function. Or (in rare cases) it can be some value that uniquely identifies the function's call. For example, SetTimer has idEvent and EnumWindows has lpData. We can use these parameters to pass arbitrary data. For example, here is how you can find all windows of a given class: type PEnumArgs = ^TEnumArgs; TEnumArgs = record ClassName: String; Windows: TStrings; end; function FindWindowsOfClass(Wnd: HWND; lpData: LPARAM): Bool; stdcall; var Args: PEnumArgs; WndClassName, WndText: String; begin Args := Pointer(lpData); SetLength(WndClassName, Length(Args.ClassName) + 2); SetLength(WndClassName, GetClassName(Wnd, PChar(WndClassName), Length(WndClassName))); if WndClassName = Args.ClassName then begin SetLength(WndText, GetWindowTextLength(Wnd) + 1); SetLength(WndText, GetWindowText(Wnd, PChar(WndText), Length(WndText))); Args.Windows.Add(Format('%8x : %s', [Wnd, WndText])); end; Result := True; end; procedure TForm1.Button1Click(Sender: TObject); var Args: TEnumArgs; begin // Edit can contain values like: // 'TForm1', 'IME', 'MSTaskListWClass', 'Shell_TrayWnd', 'TTOTAL_CMD', 'Chrome_WidgetWin_1' Args.ClassName := Edit1.Text; Args.Windows := Memo1.Lines; Memo1.Lines.BeginUpdate; try Memo1.Lines.Clear; EnumWindows(@FindWindowsOfClass, LPARAM(@Args)); finally Memo1.Lines.EndUpdate; end; end; Note: here's another example of how not to do it - don't do it like in Windows. If you just need to get a list of something - don't make a callback, just return the list in an array (wrap it in an interface or pass it as a block of memory - as discussed above). The callback function should only be used if creating the list can take a long time and you don't need all the elements. Then the callback function can return the "stop" flag without completing the list to the end. Note: some analogue of user-parameters are Tag and Data properties, although their use is not always ideologically correct (correct: create a derived class). The conclusion follows from the above: if your API needs to make a callback function, then it must have a custom Pointer size parameter that will not be used by your API. For example: // Incorrect! type TNotifyMeProc = procedure; safecall; IMyDllAPI = interface // ... procedure NotifyMe(const ANotifyEvent: TNotifyMeProc); safecall; end; // Correct type TNotifyMeProc = procedure(const AUserArg: Pointer); safecall; IMyDllAPI = interface // ... procedure NotifyMe(const ANotifyEvent: TNotifyMeProc; const AUserArg: Pointer = nil); safecall; end; And if you forget to do this, the caller will have to use ugly hacks to get around your bad API design. Naturally, instead of a function + parameter, you can just use an interface: // Correct type INotifyMe = interface ['{07FA30E4-FE9B-4ED2-8692-1E5CFEE4CF3F}'] procedure Notify; safecall; end; IMyDllAPI = interface // ... procedure NotifyMe(const ANotifyEvent: INotifyMe); safecall; end; This is preferable, because error handling via safecall in interfaces is simpler, and the interface can contain as many parameters as you like, and it is even more convenient to integrate with objects (form). For example: type TForm1 = class(TForm, INotifyMe) // ... procedure Notify; safecall; private FAPI: IMyDllAPI; public function SafeCallException(ExceptObject: TObject; ExceptAddr: Pointer): HResult; override; end; // ... procedure TForm1.FormCreate(Sender: TObject); begin // ... load DLL, get API FAPI.NotifyMe(Self); // Ask DLL to call us on some event end; procedure TForm1.Notify; begin ShowMessage('Something just happenned'); // Our form is available here (in the callback), // so we can just use it, no need to pass it manually end; Other Rules
Conclusion Download sample DLL API here. The archive contains a group of two projects (a DLL and an application that uses it). The DLL implements an sample API with example functions. The SDK folder contains the SDK, which consists of:
Headers are presented only in the form of Delphi code. Translation into other programming languages left as homework. The DelphiSupport.pas module can be included both in the DLL and in applications that use it. It contains:
The DLL API has sample functions:
The calling application shows both "load-use-upload" and "load, use, upload". Weiterlesen... |
Zitat |
Ansicht |
Linear-Darstellung |
Zur Hybrid-Darstellung wechseln |
Zur Baum-Darstellung wechseln |
ForumregelnEs ist dir nicht erlaubt, neue Themen zu verfassen.
Es ist dir nicht erlaubt, auf Beiträge zu antworten.
Es ist dir nicht erlaubt, Anhänge hochzuladen.
Es ist dir nicht erlaubt, deine Beiträge zu bearbeiten.
BB-Code ist an.
Smileys sind an.
[IMG] Code ist an.
HTML-Code ist aus. Trackbacks are an
Pingbacks are an
Refbacks are aus
|
|
Nützliche Links |
Heutige Beiträge |
Sitemap |
Suchen |
Code-Library |
Wer ist online |
Alle Foren als gelesen markieren |
Gehe zu... |
LinkBack |
LinkBack URL |
About LinkBacks |