Delphi 8 for .NET Assemblies; Packages and Libraries
以下内容转自:http://www.drbob42.com/examines/examin55.htm
In this article, Bob Swart will explain what .NET Assemblies are, how we can use them in Delphi 8 for .NET applications, and especially how we can make them ourselves (it actually turns out that there is more than one way, and even some special way to use some of them in Win32 applications).
DLLs and .NET Assemblies
The world of Windows application development introduced the DLL - Dynamic Link Library. A DLL can contain a number of functions that can be called from an application that loads the DLL (statically or dynamically). The Windows API consists mainly of DLLs. These DLLs were written in C or C++, and came with a header file that described the interface of the DLL. For Pascal and Delphi developers, it was not easy to use the DLL if you didn't have a DLL import unit. And back in 1989 I released HeadConv - my first C DLL Header Converter, which is now available as free tool under management of the Delphi-Jedi DARTH project. The Delphi Jedi project itself was founded to produce Delphi import units for C DLL header files - a tedious job, but well appreciated by all Delphi developers who ever needed to use a C DLL header file.
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
For a Delphi 8 for .NET application to use an assembly, all we need to do is add a reference to the assembly. And this is as easy as it sounds: you can either use the Project | Add Reference dialog, or use the Project Manager and right-click on the project node and select Add Reference. In both cases, you get a dialog that will help you to select a specific .NET Assembly and add it to your project.
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
Using Delphi 8 for .NET, we can create our own assemblies as well. However, at first you may wonder how to start, since the Delphi 8 for .NET Object Repository actually contains two different project types that will both result in a .NET Assembly: the Library, and the Package.
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?
Let me tell you on beforehand that the recommendation from Borland is to use a Package when you're planning to write a .NET Assembly with Delphi 8 for .NET. However, because the Delphi 8 for .NET helpfiles contain information about packages and libraries, this isn't made very clear in the helpfile to be honest. And as a result, I've been using both a package and a library project to build my Delphi 8 for .NET Assemblies, with mixed results. So just to let you know the inside store and to explain how and why, I'll use both ways and show the details.
Library
Let's start with the Library Assembly. Use the dialog of Figure 2 to create a new Library project. This will lead to the following Library1 project in the Project Manager:
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.
Of course, there's one little issue: in order to deploy the Library1.dll now, you also need to deploy the Borland.System.dll, where previously you could get away with only deploying the Library1.dll (in order to use it in Visual Studio.NET or C#Builder for example).
One final remark: the Delphi 8 for .NET IDE is a bit sensitive when it comes to building or importing assemblies, and seems to lock them and keep a lock on them, so you can't rebuild an assembly before you close down and restart the IDE, and you cannot re-import an assembly correctly either. This problem will most likely be fixed in the upcoming second patch of Delphi 8 for .NET, but until that time you may want to restart the IDE before you start another project anyway.
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.
The Package1.dll assembly can be used in any development environment, including Delphi 8 for .NET, but it has the slight disadvantage that you need to deploy all required units together with the assembly. In this case, that's only the Borland.Delphi.dll (with the Delphi 8 for .NET system unit), but as soon as you include some VCL units, then you also need to add the Delphi.Vcl.dll assemblies to the requires list, since you cannot implicitly import units into the package. In fact, the original Delphi 8 for .NET would only give you warnings if you tried to implicitly import units into the package, but after the first update, the Delphi 8 for .NET compiler already turns this into errors. This will help to enforce the explicit linking of external references, so you will never get duplicate linked units like you can get with the Library assemblies.
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. 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;
{%DelphiDotNetAssemblyCompiler '$(SystemRoot)\microsoft.net\framework\v1.1.4322\System.Web.dll'}
{%DelphiDotNetAssemblyCompiler 'c:\program files\common files\borland shared\bds\shared assemblies\2.0\Borland.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
To call the assembly from a .NET application, we have to add it as a reference to the .NET project, add the HashPassword unit to the uses clause, and just use it as any other .NET assembly. The only difference is that this particular assembly has been marked with the {$UNSAFECODE ON} compiler directive, so our save, managed .NET executable is using an unsafe assembly. The executable itself is safe, but the assembly that it uses is not. 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.
Danny Thorpe explained that rather than reject all Delphi source code that contains global procedure definitions (not a popular choice), Borland decided to implemented global procedures and functions as members of a compiler-generated class called "Unit". The same goes for global variables - they have to live somewhere. Our Delphi source code never needs to know about those implementation details - the Delphi compiler translates our calls to these global routines into the implementation specifics automatically.
The end result is that Delphi 8 for .NET supports existing source code that declares global procedures, but only Delphi code will have transparent access to these non-.NET style things. These procedures and functions can be called by C# if the C# programmer works at it, but it will not feel as natural as in Delphi because C# doesn't have the concept of a global procedure with no 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
In order to call the same HashPassword.dll binary as a DLL from a Win32 application, we have to declare the function from the assembly using the stdcall calling convention and with the external keyword. Although the .NET assembly does not use the stdcall calling convention, the Inverse P/Invoke will require that the functions HashPasswordMD5 and HashPasswordSHA1 are declared with the stdcall calling convention (otherwise you'll get garbage results). 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
In this article, I've tried to explain what makes a .NET Assembly different from a Win32 DLL, how we can use them in Delphi 8 for .NET, and what the different ways are to make them yourself in Delphi 8 for .NET.
I've explained that Package Assemblies should be used to build your own .NET Assemblies in Delphi 8 for .NET, and Library Assemblies should be used and marked unsafe to build .NET Assemblies that can be used by both .NET and Win32 applications.
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.
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. For Delphi "round-tripping" (using Delphi generated assemblies from Delphi code), always use package syntax. Library syntax was included in Delphi 8 for .NET only for the purpose of producing DLLs with unmanaged entry points (callable from D7 Win32 code), and for ASP.NET (a topic for another time).
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.
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
For more background information about the library and package assemblies, read Allen Bauer's weblog and Brian Long's article about .NET Interoperability: .NET <-> Win32.