Cross-Language Support

 
Chapter 8 - Assemblies
bySimon Robinsonet al.
Wrox Press 2002
  

One of the best features of COM was its support for multiple languages. It's possible to create a COM component with Visual Basic and make use of it from within a scripting client such as JScript. On the other hand, it's also possible to create a COM component using C++ that a Visual Basic program can't make use of. A scripting client has different requirements from a VB client, and a C++ client is able to use many more COM features than any other client language.

When writing COM components it's always necessary to have the client in mind. The server must be developed for a specific client language, or for a group of client languages. If designing a COM component for a scripting client, this component can also be used from within C++, but the C++ client then has some disadvantages. Many rules must be followed when different clients should be supported, and the compiler can't help with COM; the COM developer has to know the requirements of the client language, and has to create the interfaces accordingly .

How does this compare with the .NET platform? With the Common Type System (CTS), .NET defines how value types and reference types can be defined from a .NET language, the memory layout of such types. But the CTS does not guarantee that a type that is defined from any language can be used from any other language. This is the role of the Common Language Specification (CLS) . The CLS defines the minimum requirement of types that must be supported by a .NET language.

We briefly mentioned the CTS and CLS in the first chapter of this book. In this section, we shall go deeper, and explore:

  • The Common Type System and the Common Language Specification.

  • Language independence in action by creating a C++, Visual Basic .NET and a C# class that derive from each other. We look at the MSIL code that's generated from these compilers.

  • The requirements of the Common Language Specification.

The CTS and the CLS

All types are declared with the guidance of the Common Type System (CTS). The CTS defines a set of rules that language compilers must follow to define, reference, use, and store both reference and value types. Therefore, by following the CTS, objects written in different languages can interact with each other.

However, not all types are available to all programming languages. To build components that are accessible from all .NET languages the Common Language Specification (CLS) should be used. With the CLS, the compiler can check for valid code according to the CLS specification.

Any language that supports .NET isn't just restricted to the common subset of features that is defined with the CLS; even with .NET it's still possible to create components that can't be used from different languages. Having said that, supporting all languages is much easier with .NET than it was with COM. If you do restrict yourself to the CLS, it's guaranteed that this component can be used from all languages. It is most likely that libraries written by thirdparties will restrict to the CLS to make the library available to all languages.

The .NET Framework was designed from the ground up to support multiple languages. During the design phase of .NET, Microsoft invited many compiler vendors to build their own .NET languages. Microsoft itself delivers Visual Basic .NET, Managed C++, C#, J#, and JScript.NET. In addition, more than twenty languages from different vendors , such as COBOL, Smalltalk, Perl, and Eiffel are available. Each of these languages has its specific advantages, and many different features. The compilers of all these languages have been extended to support .NET.

Important 

The CLS is the minimum specification of requirements that a language must support. This means that if we restrict our public methods to the CLS, all languages supporting .NET can use our classes!

Most, but not all, of the classes in the .NET Framework are CLS-compliant. The non-compliant classes and methods are specially marked as not compliant in the MSDN documentation. One example is the UInt32 structure in the System namespace. UInt32 represents a 32-bit unsigned integer. Not all languages (for example Visual Basic .NET or J#) support unsigned data types; such data types are not CLS-compliant.

Language Independence in Action

Let's see CLS in action. The first assembly we create will include a base class with Visual C++. The second assembly has a VB.NET class that inherits from the C++ class. The third assembly is a C# console application with a class deriving from the VB.NET code, and a Main function that's using the C# class. The implementation should just show how the languages make use of .NET classes, and how they handle numbers , so all these classes have a simple Hello() method where the System.Console class is used, and an Add() method where two numbers are added:

Writing the Managed C++ Class

The first project type to create is a Managed C++ Class Library , which is created from the Visual C++ Projects project type of Visual Studio .NET, and given the name HelloMCPP :

click to expand

The application wizard generates a class HelloMCPP that is marked with __gc to make the class a managed type. Without a special attribute, the class would be a normal unmanaged C++ class generating native code.

In the generated header file stdafx.h , you'll see a #using < mscorlib.dll > directive. In C++, other assemblies can be referenced with the #using preprocessor directive. The code for the .NET class can be found in the file HelloMCPP.h :

   // HelloMCPP.h     #pragma once     using namespace System;     namespace HelloMCPP     {     public __gc class Class1     {     // TODO: Add your methods for this class here.     };     }   

For demonstration purposes, I'm changing the namespace and class name, and I'm adding three methods to the class. The virtual method Hello2() is using a C runtime function printf() that demonstrates the use of native code within a managed class. To make this method available the header file stdio.h must be included. Within the Hello() method we are using the Console managed class from the System namespace. The C++ using namespace statement is similar to the C# using statement. using namespace System opens the System namespace , so we needn't write System::Console::WriteLine() . The Hello() method is also marked virtual , so that it can be overridden. We will override Hello() in the VB and C# classes. C++ member functions are not virtual by default. A third method, which returns the sum of two int arguments, is added to the class so that we can compare the generated MSIL to the different languages to see how they handle numbers. All three examples use the same namespace Wrox.ProCSharp.Assemblies.CrossLanguage .

 // HelloMCPP.h #pragma once   #include <stdio.h>     using namespace System;     namespace Wrox     {     namespace ProCSharp     {     namespace Assemblies     {     namespace CrossLanguage     {     public __gc class HelloMCPP     {     public:     virtual void Hello()     {     Console::WriteLine(S"Hello, Managed C++");     }     virtual void Hello2()     {     printf("Hello, calling native code\n");     }     int Add(int val1, int val2)     {     return val1 + val2;     }     };     }     }     }     }   

To compare the programs with running code we are using the release build instead of the debug configuration. Looking at the generated DLL using ildasm , we see two static methods used, printf() and DllMainCrtStartup() . Both of these methods are native unmanaged functions using pinvoke . DllMainCrtStartup() is used within every Managed C++ program. It is the entry point in the DLL, and is called when the DLL is loaded. printf() is used within our Hello() method. The private field $ArrayType$0xec5a014e holds our native string " Hello, calling native code\n ":

click to expand
click to expand

The Hello2() method pushes the address of the field $ArrayType$0xec5a014e, that keeps the string on the stack. In line IL_0005 a call to the static printf() method can be seen where a pointer to the string " Hello, calling native code " is passed.

printf() itself is called via the platform invoke mechanism (as shown below). With the platform invoke, we can call all native functions like the C runtime and Win32 API calls. There is more discussion of platform invoke in Chapter 17.

click to expand

The Hello() method is completely made up of MSIL code; there's no native code. Because the string was prefixed with an " S ", a managed string is written into the assembly and it is put onto the stack with ldstr . In line IL_0005 we are calling the WriteLine() method of the System.Console class using the string from the stack:

click to expand
click to expand

To demonstrate how numbers are used within Managed C++, we're now going to take a look at the MSIL code of the Add() method. With ldarg.1, and ldarg.2, the passed arguments are put on the stack, add adds the stack values, and puts the result on the stack, and in line IL_0003 the result is returned.

What's the advantage of using Managed C++ compared to C# and other languages of the .NET framework? Managed C++ makes it easier to make traditional C++ code available to .NET. MSIL code and native code can be mixed easily.

Be aware that it's not possible to create a Managed C++ application without native code. With the current implementation, there's always native start-up code.

Writing the VB.NET Class

Now we're going to use VB.NET to create a class. Again, we are using the Class Library wizard; the project will be called HelloVB :

click to expand

The namespace of the class should be changed to Wrox.ProCSharp.Assemblies.CrossLanguage . In a VB.NET project this can be done by changing the root namespace of the project in the project properties:

click to expand

To make it possible to derive the class from HelloMCPP a reference to HelloMCPP.dll is needed. The reference is added using Project Add Reference, or can also be added from inside the Solution Explorer . When building the assembly the reference can be seen inside the manifest: .assembly extern HelloMCPP . The referenced assembly is also copied to the output directory of the VB.NET project, so that we are independent of later changes made to the original referenced assembly:

click to expand

The class HelloVB inherits from HelloMCPP . VB.NET has the keyword Inherits to derive from a base class. Inherits must be in the same line as, and follow the Class statement. The Hello() method in the base class is overridden. The VB.NET Overrides keyword does the same thing as the C# override keyword. In the implementation of the Hello() method, the Hello() method of the base class is called using the VB.NET keyword MyBase . The MyBase keyword is the same as base in C#. The method Add() is implemented so that we can examine the generated MSIL code to see how VB.NET works with numbers. The Add() method from the base class is not virtual, so it can't be overridden. VB.NET has the keyword Shadows to hide a method of a base class. Shadows is similar to C#'s new :

   Public Class HelloVB     Inherits HelloMCPP         Public Overrides Sub Hello()     MyBase.Hello()     System.Console.WriteLine("Hello, VB.NET")     End Sub         Public Shadows Function Add(ByVal val1 As Integer, _     ByVal val2 As Integer) As Integer     Return val1 + val2     End Function         End Class   

Let's look at the MSIL code that is generated from the VB.NET compiler.

The HelloVB.Hello() method first calls the Hello() method of the base class HelloMCPP . In line IL_0006 , a string stored in the metadata is pushed on the stack using ldstr .

click to expand

The other method we are looking at is Add() . VB.NET uses add.ovf instead of the add method that was used in the MC++ generated MSIL code. This is just a single MSIL statement that's different between MC++ and VB.NET, but the statement add.ovf generates more lines of native code, as add.ovf performs overflow checking. If the result of the addition of the two arguments is too large to be represented in the target type, add.ovf generates an exception of type OverflowException . In contrast, add just performs an addition of the two values, whether or not the target fits. In the case where the target is not big enough, the true value of the summation is lost, and the result is a wrong number. So, add is faster, but add.ovf is safer:

click to expand

Writing the C# Class

The third class is created using the language we know best: C#. For this project, we will create a C# Console Application . The HelloVB assembly is referenced to make a derivation of the class HelloVB , and a reference to HelloMCPP is also added.

The methods implemented in the C# class are similar to the MC++ and the VB.NET classes. Hello() is an overridden method of the base class; Add() is a new method:

   namespace Wrox.ProCSharp.Assemblies.CrossLanguage   {    using System;     /// <summary>    ///    Summary description for HelloCSharp.    /// </summary>   public class HelloCSharp : HelloVB   {   public HelloCSharp()     {     }     public override void Hello()     {     base.Hello();     Console.WriteLine("Hello, C#");     }     public new int Add(int val1, int val2)     {     return val1 + val2;     }   [STAThread]       public static void Main()       {   HelloCSharp hello = new HelloCSharp();     hello.Hello();   }    } } 

As you can see, the generated MSIL code for the Hello() method is the same as the MSIL code from the VB.NET compiler:

click to expand

The Add() method differs , and yet is similar to the MC++ code. When doing calculations, the C# compiler doesn't use the methods with overflow; checking with the default compiler settings in a Visual Studio .NET project. The faster MSIL method add is used instead of add.ovf; but it's possible to change this option using the configuration properties of the project both with C# and VB.NET. By setting Check for overflow underflow to true in a C# project, the MSIL code that the C# compiler generates for our example will be the same as that generated by the VB.NET compiler. Unlike VB.NET, with C# it's also possible to choose this option on an expression-by-expression basis with the checked and unchecked operators:

click to expand

Finally, we can see the console application in action:

click to expand

Because all the .NET languages generate MSIL code and all the languages make use of the classes in the .NET framework, it's often said that there is no difference regarding performance. As you can see, however, small differences are still there. Firstly, depending on the language, some languages support different data types from others. Secondly, the generated MSIL code can still be different. One example that we've seen is that the number calculations are implemented differently: while the default configuration of VB.NET is for safety, the default for C# is for speed. C# is also more flexible.

CLS Requirements

We've just seen the CLS in action when we looked at cross-language inheritance between MC++, VB.NET, and C#. Until now we didn't pay any attention to the CLS requirements when building our project. We were lucky the methods we defined in the base classes were callable from the derived classes. If a method had the System.UInt32 data type as one of its arguments, we wouldn't be able to use it from VB.NET. Unsigned data types are not CLS-compliant; for a .NET language, it's not necessary to support this data type.

The Common Language Specification exactly defines the requirements to make a component CLS- compliant, which enables it to be used with different .NET languages. With COM we had to pay attention to language-specific requirements when designing a component. JScript had different requirements from VB6, and the requirements of VJ++ were different again. That's no longer the case with .NET. When designing a component that should be used from other languages, we just have to make it CLS, compliant; it's guaranteed that this component can be used from all .NET languages. If we mark a class as CLS, compliant, the compiler can warn us about non-compliant methods.

All .NET languages must support the CLS. When talking about .NET languages we have to differentiate between .NET consumer and .NET extender tools.

A .NET consumer tool just uses classes from the .NET Framework it can't create .NET classes that can be used from other languages. A consumer tool can use any CLS-compliant class. A .NET extender tool has the requirements of a consumer, and can in addition inherit any CLS-compliant .NET class, and define new CLS compliant classes that can be used by consumers. C++, VB.NET, and C# all are extender tools. With these languages, it's possible to create CLS compliant classes. The COBOL that's available for .NET currently is just a consumer tool. With COBOL.NET, we can use all CLS compliant classes, but not extend it.

CLSCompliant Attribute

With the CLSCompliant attribute, we can mark our assembly to be CLS compliant. Doing this guarantees that the classes in this assembly can be used from all .NET consumer tools. The compiler issues warnings when we are using non-CLS compliant data types in public and protected methods. The data types we use in the private implementation don't matter when using other languages outside of the class, we don't have direct access to private methods anyway.

To get compiler warnings when a data type is not compliant in public and protected methods, we set the attribute CLSCompliant in the assembly by adding this attribute to the file AssemblyInfo.cs :

   [assembly: System.CLSCompliant(true)]   

This way, all the defined types and public methods inside the assembly must be compliant. Using a non-compliant uint type as argument type, we get this error from the compiler:

  error CS3001: Argument type uint is not CLS-compliant  

When we mark an assembly as compliant, it's still possible to define methods that are not compliant. This can be useful if you want to override some method to make it available with both compliant and non-compliant argument data types. The methods that are not compliant must be marked, within the class, by the CLSCompliant attribute with a value of false . The CLSCompliant attribute can be applied to types, methods, properties, fields, and events:

   [CLSCompliant(false)]   void Method(uint i)  {    //... 

CLS Rules

The requirements for an assembly to be CLS-compliant are the following:

  • All types appearing in a method prototype must be CLS-compliant.

  • Array elements must have a CLS-compliant element type. Arrays must also be 0-indexed.

  • A CLS compliant class must inherit from a CLS-compliant class. System.Object is CLS compliant.

  • Although method names in CLS-compliant classes are not case-sensitive, no two method names can be different only in the case of the letters in their name.

  • Enumerations must be of type Int16 , Int32 , or Int64 . Enumerations of other types are not compliant.

All the listed requirements only apply to public and protected members . The private implementation itself doesn't matter non-compliant types can be used there, and the assembly is still compliant.

Besides these requirements, there are the more general naming guidelines that we read about in Chapter 6. These guidelines are not a strict requirement of being CLS compliant, but do make life much easier.

In addition to these naming guidelines to support multiple languages, it's necessary to pay attention to method names where the type is part of the name. Data type names are language-specific, for example, the C# int , long , and float types are equivalent to the VB.NET Integer , Long , and Single types. When a data type name is used in a name of a method, the universal type names, and not the language- specific type names should be used, in other words Int32 , Int64 , and Single :

   int ReadInt32();     long ReadInt64();     float ReadSingle();   

As you can see when complying with the CLS specs and guidelines, it's easy to create components that can be used from multiple languages. It's not necessary to test the component using all .NET consumer languages.

  


Professional C#. 2nd Edition
Performance Consulting: A Practical Guide for HR and Learning Professionals
ISBN: 1576754359
EAN: 2147483647
Year: 2002
Pages: 244

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net