Delphi 8 for .NET Assemblies; Packages and Libraries
|
Delphi 8 for .NET Assemblies; Packages and Libraries DLLs and .NET Assemblies Why am I telling you this? Because the world has changed. The .NET Framework is taking over the Windows world, and the DLL is being replaced by a new kind of library, called the .NET Assembly. .NET Assemblies are more powerful than DLLs in a number of ways. First of all, where a DLL can only export functions (and not classes), the .NET Assembly can define classes that can be used and extended by other .NET Assemblies or applications. Another significant benefit is the fact that the .NET Framework is language neutral. As a consequence, a .NET Assembly written in language X can be used by any language, including Delphi 8 for .NET. No more C DLL header conversions ever! Add References
The same dialog can also be used to add references to COM Objects and ActiveX to your project, using the COM Interop tab. Delphi 8 for .NET Assemblies
Both solutions will work (although in general one is preferred over the other), but you may need a little additional information about their use and deployment specifics. Package or Library? Library
As you can see, this is a Library project with a reference node that has no references added to it as this time. You can add functionality to this Library project by adding a unit to it. As example unit for this article, I want to use the following unit that defined a class TRandomNumber that we can "export" from the .NET Assembly: unit Hardcore.Delphi.Final.Issue;
interface
type
TRandomNumber = class
constructor Create;
function Random10: Integer; virtual;
end;
implementation
constructor TRandomNumber.Create;
begin
inherited;
Randomize
end;
function TRandomNumber.Random10: Integer;
begin
Result := Random(10)
end;
end.
It"s only a very simple example, but it will be good enough to demonstrate the way in which we can build and use the two different .NET Assembly types with Delphi 8 for .NET. For the Library Assembly, right-click on the project node and do Add, which offers a dialog in which you can select the Hardcore.Delphi.Final.Issue.pas unit. Now, compile the project, resulting in a Library1.dll of 1.3 MBytes. Wow! How come? Well, take a look at the Library source code in Library1.dpr and you"ll see the following uses clause: uses
SysUtils,
Classes,
System.Reflection,
Hardcore.Delphi.Final.Issue in "Hardcore.Delphi.Final.Issue.pas";
I don"t why SysUtils and Classes are automatically added to the uses clause, but it causes almost the entire VCL for .NET to be linked in with the DLL Assembly. So, to make it a bit smaller, remove the SysUtils and Classes units from the uses clause and recompile the Library1 project. This time the result is a 94 KByte Project1.dll. That"s more like it. Note that the Library project still doesn"t have any references added to it in the Project Manager. This means that the Project1.dll Assembly doesn"t depend on any other .NET Assemblies, apart from mscorlib and System, which are always present. You can use this Library assembly with any development environment like Visual Studio.NET, C#Builder, etc. with one exception: you can"t add the Project1.dll Assembly to a Delphi 8 for .NET application. Again, you read it right: the Project Assembly that we just made with Delphi 8 for .NET cannot be loaded in any other project we make with Delphi 8 for .NET. The reason is simple, but surprising. The fact that the Library project contained no external references, resulted in the fact that the Borland.Delphi.System unit was linked into it. And adding this Library Assembly to another Delphi 8 for .NET project, which also contains the Borland.Delphi.System unit, leads to a Fatal Error message that says that it could not import assembly "Library1" because it contains namespace "Borland.Delphi.System" (which is also included in the new project itself). According to Danny Thorpe, the main issue is process-wide global data defined in the system unit. In order to solve this problem, you need to reload the Library Assembly project in the IDE, and right-click on the Library1 project node to add a reference to the Borland.System.dll. This way, the Library Assembly will still use the Borland.Delphi.System unit, but from an external assembly reference. Like a run-time package. And this time, the Library1.dll will even be 6.5 KBytes big! Now when you add the Library1.dll Assembly to a Delphi 8 for .NET project, it will compile just fine, since both the Library1.dll assembly and the Delphi 8 for .NET project will refer to the same external Borland.System.dll. Package OK, so far for the Library Assembly. Let"s now see what the Package Assembly offers us. As soon as you select the New Package option, you get a different view in the Project Manager, as shown in Figure 4 (compared to Figure 3).
A Delphi 8 for .NET package automatically gets the Borland.Delphi.dll assembly added to the Requires list, so we won"t get any problems with duplicate Borland.Delphi.System namespaces if we want to use it in a Delphi 8 for .NET project later. And we can add units to the Contains node, like the Hardcore.Delphi.Final.Issue unit with our TRandomNumber class. Compiling the package will lead to a Package1.dll Assembly of 6.5 KBytes big. Hmm, I"ve seen that number before. That"s exactly the same size we ended with when building the Library1.dll assembly - only took it three steps to get it right. Using Package1 Assembly So, just as an example, let"s see how easy we can use the Package1 assembly in a Delphi 8 for .NET application. Start a new project - WinForms or VCL for .NET - and right-click on the project node to add a reference. In the Add Reference dialog, select the Package1.dll assembly to add it to the reference list. You can now right-click on the Package assembly again, and note two special options. There is Copy Local option, which is already checked. This means that the external assembly will be copied to your project directory, to ease the deployment later. The other interesting option is the Link in Delphi Units, and is only available for Assemblies build with Delphi 8 for .NET. It offers us the ability to do the reverse of using external packages: link in all Delphi 8 for .NET assemblies and result in one big executable. Note that this should only be done with executables, and not with libraries since we"ve just seen that these should use references as much as possible to be useful in the first place. Adding the Package1 assembly to the references list is one thing, but that doesn"t ensure that we can use the TRandomNumber class. We now have to add the namespace (that defined the TRandomNumber class) of the Package1 assembly to the uses clause. That namespace is Hardcore.Delphi.Final.Issue, since it was unit Hardcore.Delphi.Final.Issue that contained the definition and implementation of the TRandomNumber class. So, add Hardcore.Delphi.Final.Issue to the uses clause of the project (you may want to rename the Hardcore.Delphi.Final.Issue.pas unit on your machine to make sure Delphi isn"t cheating and using the unit source code instead of the imported namespace from the Package1.dll assembly). Now that the namespace is available, you can get to the TRandomNumber class, create it, call the Random10 method, or even derive a new class from it. The latter is also possible it you"re creating a new Package assembly, adding the original one to the Requires list, and defining a new TBetterRandomNumber class derived from TRandomNumber. To summarise the differences between a Package and a Library: if your assemlby (.dll) is built from Delphi library syntax, there is no Delphi symbol information available, so our view of the DLL will be limited to what Delphi can represent in CLR metadata. We will lose Delphi language specific details such as virtual constructors, virtual class functions, and set types, to name a few. Another thing we will lose is the notion that a function or procedure was originally declared as a global proc in a unit. The recommandation from the Delphi R&D Team is to build .NET Assemblies using the Delphi package syntax, and link against it using the .dcpil. Back to the Library So what is it with these library project? If packages are the way to create new .NET Assemblies, why were library project kept in Delphi 8 for .NET as viable targets? The answer can be found in a unique way of offering Win32 interoperability. Several people were involved, like Brian Long, Roy Nelson and Danny Thorpe, and in the end a special feature of the .NET Loader was added to Delphi 8 for .NET library projects. The ability to have a .NET assembly that can be loaded by a .NET application but also by an unmanaged Win32 application. And I"m not talking about a single source project, but about a single binary project. The same binary, available to be used from .NET as well as unmanaged Win32. The only special thing that"s required, is that the .NET assembly has to be marked as unsafe. As an example of this feature, consider the following Delphi 8 for .NET library, which is marked with the {$UNSAFECODE ON} compiler directive to produce unsafe code. library HashPassword;
{辧phiDotNetAssemblyCompiler "$(SystemRoot)microsoft.netframeworkv1.1.4322System.Web.dll"}
{辧phiDotNetAssemblyCompiler "c:program filescommon filesorland shareddsshared assemblies2.0Borland.Delphi.dll"}
{$UNSAFECODE ON}
uses
System.Web.Security;
function HashPasswordMD5(const Passwd: String): String;
begin
Result := FormsAuthentication.
HashPasswordForStoringInConfigFile(Passwd, "MD5")
end;
function HashPasswordSHA1(const Passwd: String): String;
begin
Result := FormsAuthentication.
HashPasswordForStoringInConfigFile(Passwd, "SHA1")
end;
exports
HashPasswordMD5,
HashPasswordSHA1;
end.
The resulting (unsafe) assembly can be used from both .NET applications and Win32 applications.
Calling from .NET implementation
uses
HashPassword;
...
procedure TWinForm.Button1_Click(sender: System.Object; e: System.EventArgs);
begin
MessageBox.Show("MD5: ["
HashPassword.Unit.HashPasswordMD5(TextBox1.Text) "]");
MessageBox.Show("SHA1: ["
HashPassword.Unit.HashPasswordSHA1(TextBox1.Text) "]");
end;
Note the Unit part of the HashPassword.Unit.HashPasswordMD5 and HashPassword.Unit.HashPasswordSHA1 methods calls. This is due to the fact that the .NET platform does not support the notion of a stand-alone global procedure. All procedures and functions must be members of a class type. Note that implementation details such as how the Delphi compiler represents global procedures and other non-.NET concepts are subject to change between major releases. Global procedures can be placed somewhere else in future versions of Delphi! Calling from Win32 program Win32Client;
{$APPTYPE CONSOLE}
type
TPasswordFormat = (SHA1, MD5);
function HashPasswordMD5(Password: PChar): PChar; stdcall;
external "HashPassword.dll";
function HashPasswordSHA1(Password: PChar): PChar; stdcall;
external "HashPassword.dll";
var
Passwd: String;
begin
write("Password: ");
readln(Passwd);
writeln("[" Passwd "]");
write("MD5: ");
writeln(HashPasswordMD5(PChar(Passwd)));
write("SHA1: ");
writeln(HashPasswordSHA1(PChar(Passwd)));
end.
This is a neat way to expose .NET functionality to good-old Win32 applications written in Delphi 7.
Summary We can sum it up as follows: libraries are an oddity in .NET, while packages are the "natural" thing for .NET. This is exactly the opposite of Win32. Finally, Danny Thorpe reminded me to be careful of relying on implementation details such as HashPassword.Unit.HashPasswordMD5. The way the compiler exports Delphi global procedures in metadata is very likely to change in future releases. The same is true for any implementation detail of how Delphi language features are implemented at the IL level that are not directly supported by CLR. 软件开发网 www.mscto.com The best way to avoid relying on details like HashPassword.Unit.HashPasswordMD5 is to implement HashPasswordMD5 as a class static method of a Delphi class. This is the normal CLR way of doing things and eliminates the question of how Delphi exposes non-CLR things (like global procs) to CLR. References 源代码网推荐 源代码网供稿. |
