Monday, June 25, 2007

Read the story of building a web service that uses the C#Builder and Delphi for .NET CodeDOMs to instantly convert C# code samples to Delphi code

Abstract: Read the story of building a web service that uses the C#Builder and Delphi for .NET CodeDOMs to instantly convert C# code samples to Delphi code

CodeDOM overview

A "CodeDOM" is a Document Object Model that represents logic expressed in source code. Both the Delphi and C#Builder designers for WinForms and ASP.NET to generate and update source code based on the components that are dropped, properties that are set, and events created by the user through the designer surface. The application for CodeDOMs goes beyond design-time support, and are used at run-time by the ASP.NET engine to generate compilable code for ASP.NET WebForms, web services, and user controls.

In this introduction to .NET's CodeDOM technology, the author notes the following advantages:

1. The CodeDom is based upon a single model for rendering source code. Therefore, source code may be generated for any language that supports the CodeDom specification.
2. The CodeDom allows programs to be dynamically created, compiled, and executed at runtime.
3. The CodeDom provides a language independent object model for representing the structure of source code in memory.
4. Future releases of the CodeDom could translate source code files between languages a lot like graphic file converter programs do with graphics files today. For example, a VB.NET program could be represented by the CodeDom then translated into C# for another developer. Cool huh?

Advantage number four (4) is indeed a cool idea, and you can now test an implementation of it! BabelCode is an experimental service provided by Borland that allows you to convert C# code to Delphi for .NET code. It uses the CodeDOMs Borland has implemented for both C#Builder and Delphi for .NET. Corbin Dunn, one of our .NET IDE engineers, created an assembly for me to call in the BabelCode web service that takes an input stream of C# code, and converts it to Delphi code.
Creating the BabelCode web service

Rather than releasing an executable or providing an assembly for download, we decided to make the converter available as a web service. That way, we can make improvements to the converter (it definitely does not convert 100% of a given C# code sample to Delphi) and post the new version of the assembly on the web site to make it available for anyone who wants to use BabelCode.

Here's how I created the web service with Delphi for .NET:

1. File | New | Other | ASP.NET Web Service Application
2. Right mouse click on the project in the project manager and add a reference to the assembly containing the C# to Delphi converter
3. Insert the CSharpToDelphimethod into the interface for my class and remove the (already commented out) sample Hello method that gets generated by the wizard, so my class declaration looks like this:

BabelCode = class(System.Web.Services.WebService)
{$REGION 'Designer Managed Code'}
...
{$ENDREGION}
strict protected
///
/// Clean up any resources being used.
///

procedure Dispose(disposing: boolean); override;
private
{ Private Declarations }
public
constructor Create;
[WebMethod(Description='Converts C# code to Delphi code, with a few minor cleanup steps required.')]
function CSharpToDelphi(const Code:string): string;
end;

The [WebMethod(Description= ... ] attribute declaration allows me to document the purpose of this WebService method, which the ASP.NET run-time will automatically display in an attractive browser interface.

4. Finally, I hit Ctrl+Shift+C for class completion to create the declaration of my new method, and change the code to the following:


function BabelCode.CSharpToDelphi(const Code: string): string;
var
source: Stream;
bytes : array of byte;
begin
bytes := System.Text.ASCIIEncoding.Create.GetBytes(code);
Source := MemoryStream.Create(bytes);
try
Result := ToDelphi.Convert('any.cs', source);
finally
source.Close;
end;
end;

To see the web service I created, go to the BabelCode home page and click on the link to the web service.
Building BabelCode clients

Once the web service location is known, it is very easy to build a client to access the web service. Let's build a WinForm client, because a WebForm client is already available from the BabelCode home page. I'll be writing a separate article about building that client because it's a little more involved because it uses the BabelCode web service, the QualityCentral web service, and a custom .NET assembly for consuming the QualityCentral web service if you encounter a conversion error and want to report it.
Adding a new project

You can download the complete WinForms project we'll be creating from CodeCentral.

To create the new project, we'll right mouse click on the project in the project manager and add a new project.
Adding project to group

This menu option displays the object repository, and we'll select a Windows Form application
Creating new WinForm application

After the new project is created, we'll right mouse click on the project in the project manager and add a web service reference to the project
Adding a web service reference

In the UDDI browser that appears, simply put the BabelCode WSDL URL in the address box and hit the right arrow button to the right of the address bar. This will parse and verify the WSDL, and enable the "Add reference" button. (If the WSDL were listed in a UDDI directory, we could search for it using the UDDI browser.)
Importing the BabelCode WSDL

When the import completes, we get a Delphi language binding (again!) from the WSDL. We went from Delphi, to WSDL, to Delphi. It's kind of like what we're doing with the CodeDOM, isn't it?
Delphi language binding for Web Service

After we have the Delphi language binding for the web service, we make a reference to the imported unit from the Windows form, so we can actually make calls to the web service.
Using the imported web service

Because the source files can be quite large, we'll use RichTextBox components for the source code input and output to avoid any size limitation problems. Here are the steps required for designing the WinForm:

* Double-click on a RichTextBox component. Set the name to CSharpCode, dock it to the top of the form, change its font to Courier New, modify its text property to "C# Code goes here".
* Drop a Splitter, dock it to the top, set its cursor to HSplit
* Drop a Button, name it ConvertButton, dock it to the top, set its text property to "&Convert C# Code to Delphi Code"
* Drop a RichTextBox component. Set the name to DelphiCode, dock it to fill the remainder of the form, change its font to Courier New, modify its text property to "Delphi code goes here".

The completed form will look something like this:
The completed WinForm

Once the form is laid out, double click on the button to generate the ConvertButton_Click event. CodeInsight will help write the one line of code required.
CodeInsight showing available 'Ba*' variables

Here, at last, is the one line of code we actually need to write to call the web service and perform the conversion:


procedure TWinForm.ConvertButton_Click(sender: System.Object;
e: System.EventArgs);
begin
DelphiCode.Text := BabelCode.Create.CSharpToDelphi(CSharpCode.Text)
end;

Using BabelCode

Once the client is completed, you can just hit the Run button, put in some C# code, and hit the Convert button to call the web service. Here's some C# .NET SDK sample code:


/*=====================================================================
File: PublicKey.cs

Summary: Demonstrates public key cryptography using the .NET
Framework implementation of RSA.

---------------------------------------------------------------------
This file is part of the Microsoft .NET Framework SDK Code Samples.

Copyright (C) Microsoft Corporation. All rights reserved.

This source code is intended only as a supplement to Microsoft
Development Tools and/or on-line documentation. See these other
materials for detailed information regarding Microsoft code samples.

THIS CODE AND INFORMATION ARE PROVIDED "AS IS" WITHOUT WARRANTY OF ANY
KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A
PARTICULAR PURPOSE.
=====================================================================*/

using System;
using System.Security.Cryptography;
using System.IO;
using System.Text;

namespace PublicKey
{
class App
{
// Main entry point
static void Main(string[] args)
{
// Instantiate 3 People for example. See the Person class below
Person alice = new Person("Alice");
Person bob = new Person("Bob");
Person steve = new Person("Steve");

// Messages that will exchanged. See CipherMessage class below
CipherMessage aliceMessage;
CipherMessage bobMessage;
CipherMessage steveMessage;

// Example of encrypting/decrypting your own message
Console.WriteLine("Encrypting/Decrypting Your Own Message");
Console.WriteLine("-----------------------------------------");

// Alice encrypts a message using her own public key
aliceMessage = alice.EncryptMessage("Alice wrote this message");
// then using her private key can decrypt the message
alice.DecryptMessage(aliceMessage);
// Example of Exchanging Keys and Messages
Console.WriteLine();
Console.WriteLine("Exchanging Keys and Messages");
Console.WriteLine("-----------------------------------------");

// Alice Sends a copy of her public key to Bob and Steve
bob.GetPublicKey(alice);
steve.GetPublicKey(alice);

// Bob and Steve both encrypt messages to send to Alice
bobMessage = bob.EncryptMessage("Hi Alice! - Bob.");
steveMessage = steve.EncryptMessage("How are you? - Steve");

// Alice can decrypt and read both messages
alice.DecryptMessage(bobMessage);
alice.DecryptMessage(steveMessage);

Console.WriteLine();
Console.WriteLine("Private Key required to read the messages");
Console.WriteLine("-----------------------------------------");

// Steve cannot read the message that Bob encrypted
steve.DecryptMessage(bobMessage);
// Not even Bob can use the Message he encrypted for Alice.
// The RSA private key is required to decrypt the RS2 key used
// in the decryption.
bob.DecryptMessage(bobMessage);

} // method Main
} // class App

class CipherMessage
{
public byte[] cipherBytes; // RC2 encrypted message text
public byte[] rc2Key; // RSA encrypted rc2 key
public byte[] rc2IV; // RC2 initialization vector
}

class Person
{
private RSACryptoServiceProvider rsa;
private RC2CryptoServiceProvider rc2;
private string name;

// Maximum key size for the RC2 algorithm
const int keySize = 128;

// Person constructor
public Person(string p_Name)
{
rsa = new RSACryptoServiceProvider();
rc2 = new RC2CryptoServiceProvider();
rc2.KeySize = keySize;
name = p_Name;
}

// Used to send the rsa public key parameters
public RSAParameters SendPublicKey()
{
RSAParameters result = new RSAParameters();
try
{
result = rsa.ExportParameters(false);
}
catch (CryptographicException e)
{
Console.WriteLine(e.Message);
}
return result;
}

// Used to import the rsa public key parameters
public void GetPublicKey(Person receiver)
{
try
{
rsa.ImportParameters(receiver.SendPublicKey());
}
catch (CryptographicException e)
{
Console.WriteLine(e.Message);
}
}

public CipherMessage EncryptMessage(string text)
{
// Convert string to a byte array
CipherMessage message = new CipherMessage();
byte[] plainBytes = Encoding.Unicode.GetBytes(text.ToCharArray());

// A new key and iv are generated for every message
rc2.GenerateKey();
rc2.GenerateIV();

// The rc2 initialization doesnt need to be encrypted, but will
// be used in conjunction with the key to decrypt the message.
message.rc2IV = rc2.IV;
try
{
// Encrypt the RC2 key using RSA encryption
message.rc2Key = rsa.Encrypt(rc2.Key, false);
}
catch (CryptographicException e)
{
// The High Encryption Pack is required to run this sample
// because we are using a 128-bit key. See the readme for
// additional information.
Console.WriteLine("Encryption Failed. Ensure that the" +
" High Encryption Pack is installed.");
Console.WriteLine("Error Message: " + e.Message);
Environment.Exit(0);
}
// Encrypt the Text Message using RC2 (Symmetric algorithm)
ICryptoTransform sse = rc2.CreateEncryptor();
MemoryStream ms = new MemoryStream();
CryptoStream cs = new CryptoStream(ms, sse, CryptoStreamMode.Write);
try
{
cs.Write(plainBytes, 0, plainBytes.Length);
cs.FlushFinalBlock();
message.cipherBytes = ms.ToArray();
}
catch (Exception e)
{
Console.WriteLine(e.Message);
}
finally
{
ms.Close();
cs.Close();
}
return message;
} // method EncryptMessage


public void DecryptMessage(CipherMessage message)
{
// Get the RC2 Key and Initialization Vector
rc2.IV = message.rc2IV;
try
{
// Try decrypting the rc2 key
rc2.Key = rsa.Decrypt(message.rc2Key, false);
}
catch (CryptographicException e)
{
Console.WriteLine("Decryption Failed: " + e.Message);
return;
}

ICryptoTransform ssd = rc2.CreateDecryptor();
// Put the encrypted message in a memorystream
MemoryStream ms = new MemoryStream(message.cipherBytes);
// the CryptoStream will read cipher text from the MemoryStream
CryptoStream cs = new CryptoStream(ms, ssd, CryptoStreamMode.Read);
byte[] initialText = new Byte[message.cipherBytes.Length];

try
{
// Decrypt the message and store in byte array
cs.Read(initialText, 0, initialText.Length);
}
catch (Exception e)
{
Console.WriteLine(e.Message);
}
finally
{
ms.Close();
cs.Close();
}

// Display the message received
Console.WriteLine(name + " received the following message:");
Console.WriteLine(" " + Encoding.Unicode.GetString(initialText));
} // method DecryptMessage
} // class Person
} // namespace PublicKey

And here's the translation to Delphi from BabelCode:


//------------------------------------------------------------------------------
//
// This code was generated by a tool.
// Runtime Version: 1.1.4322.573
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
//

//------------------------------------------------------------------------------

unit PublicKey;

interface

type
Person = class;
CipherMessage = class;
TArrayOfString = array of string;
App = class
strict private
class procedure Main(args: TArrayOfString);static;
end;

TArrayOfByte = array of Byte;
CipherMessage = class
public
cipherBytes: TArrayOfByte;
rc2Key: TArrayOfByte;
rc2IV: TArrayOfByte;
end;

Person = class
strict private
rsa: RSACryptoServiceProvider;
rc2: RC2CryptoServiceProvider;
name: string;
keySize: Integer;
public
constructor Create(p_Name: string);
function SendPublicKey: RSAParameters;
procedure GetPublicKey(receiver: Person);
function EncryptMessage(text: string): CipherMessage;
procedure DecryptMessage(message: CipherMessage);
end;

implementation

{$AUTOBOX ON}
{$HINTS OFF}
{$WARNINGS OFF}

class procedure App.Main(args: TArrayOfString);
var
steveMessage: CipherMessage;
bobMessage: CipherMessage;
aliceMessage: CipherMessage;
steve: Person;
bob: Person;
alice: Person;
begin
alice := Person.Create('Alice');
bob := Person.Create('Bob');
steve := Person.Create('Steve');



Console.WriteLine('Encrypting/Decrypting Your Own Message');
Console.WriteLine('-----------------------------------------');
aliceMessage := alice.EncryptMessage('Alice wrote this message');
alice.DecryptMessage(aliceMessage);
Console.WriteLine;
Console.WriteLine('Exchanging Keys and Messages');
Console.WriteLine('-----------------------------------------');
bob.GetPublicKey(alice);
steve.GetPublicKey(alice);
bobMessage := bob.EncryptMessage('Hi Alice! - Bob.');
steveMessage := steve.EncryptMessage('How are you? - Steve');
alice.DecryptMessage(bobMessage);
alice.DecryptMessage(steveMessage);
Console.WriteLine;
Console.WriteLine('Private Key required to read the messages');
Console.WriteLine('-----------------------------------------');
steve.DecryptMessage(bobMessage);
bob.DecryptMessage(bobMessage);
end;

constructor Person.Create(p_Name: string);
begin
inherited Create;
Self.rsa := RSACryptoServiceProvider.Create;
Self.rc2 := RC2CryptoServiceProvider.Create;
Self.rc2.KeySize := keySize;
Self.name := p_Name;
end;

function Person.SendPublicKey: RSAParameters;
var
_result: RSAParameters;
begin
_result := RSAParameters.Create;
try
_result := Self.rsa.ExportParameters(False);
except
on e: CryptographicException do
Console.WriteLine(e.Message);
end;
Result := _result;
end;

procedure Person.GetPublicKey(receiver: Person);
begin
try
Self.rsa.ImportParameters(receiver.SendPublicKey);
except
on e: CryptographicException do
Console.WriteLine(e.Message);
end;
end;

function Person.EncryptMessage(text: string): CipherMessage;
var
cs: CryptoStream;
ms: MemoryStream;
sse: ICryptoTransform;
plainBytes: TArrayOfByte;
message: CipherMessage;
begin
message := CipherMessage.Create;
plainBytes := Encoding.Unicode.GetBytes(text.ToCharArray);
Self.rc2.GenerateKey;
Self.rc2.GenerateIV;
message.rc2IV := Self.rc2.IV;
try
message.rc2Key := Self.rsa.Encrypt(Self.rc2.Key, False);
except
on e: CryptographicException do
begin
Console.WriteLine(('Encryption Failed. Ensure that the'
+ ' High Encryption Pack is installed.'));
Console.WriteLine(('Error Message: ' + e.Message));
Environment._Exit(0);
end;
end;
sse := Self.rc2.CreateEncryptor;
ms := MemoryStream.Create;
cs := CryptoStream.Create(ms, sse, CryptoStreamMode.Write);
try
try
cs.Write(plainBytes, 0, plainBytes.Length);
cs.FlushFinalBlock;
message.cipherBytes := ms.ToArray;
except
on e: Exception do
Console.WriteLine(e.Message);
end;
finally
ms.Close;
cs.Close;
end;
Result := message;
end;

procedure Person.DecryptMessage(message: CipherMessage);
type
TArrayOfArrayOfByte = array of array of Byte;
var
initialText: TArrayOfByte;
cs: CryptoStream;
ms: MemoryStream;
ssd: ICryptoTransform;
begin
Self.rc2.IV := message.rc2IV;
try
Self.rc2.Key := Self.rsa.Decrypt(message.rc2Key, False);
except
on e: CryptographicException do
begin
Console.WriteLine(('Decryption Failed: ' + e.Message));
Exit;
end;
end;
ssd := Self.rc2.CreateDecryptor;
ms := MemoryStream.Create(message.cipherBytes);
cs := CryptoStream.Create(ms, ssd, CryptoStreamMode.Read);
initialText := New(TArrayOfArrayOfByte, message.cipherBytes.Length);
try
try
cs.Read(initialText, 0, initialText.Length);
except
on e: Exception do
Console.WriteLine(e.Message);
end;
finally
ms.Close;
cs.Close;
end;
Console.WriteLine((Self.name + ' received the following message:'));
Console.WriteLine((' ' + Encoding.Unicode.GetString(initialText)));
end;

end.

Reporting issues

As mentioned at the start of this article, BabelCode is an experimental service provided by Borland. It is unsupported, and definitely a "Use at your own risk" tool. However, it is also a great testing tool for our CodeDOMs for the languages we support. So, if you try BabelCode and encounter any bugs, I encourage you to use the BabelCode browser client to conveniently report the bugs into QualityCentral.

Even with any bugs you might encounter in the conversion, I believe any Delphi developer learning .NET will find BabelCode an extremely valuable tool for making any C# code you might encounter more familiar as Delphi code. Enjoy!

0 Comments: