Using TAO and OpenDDS with .NET [Part I]

Middleware News Brief (MNB) features news and technical information about Open Source middleware technologies.

INTRODUCTION

A major advantage of TAO and OpenDDS, open-source implementations of the Object Management Group's CORBA and the Data Distribution Service, is the wide variety of platforms to which they have been ported. While retaining platform neutrality is a worthy goal, the dominance of Microsoft Windows in the PC marketplace, over 90% [1] market share as of the writing of this article, encourages the use of Windows-specific features when developing for that platform.

Since the release of Visual Studio.NET in 2002, Microsoft's direction for development has been that of the .NET Framework [2]. In a manner similar to Java bytecode, high-level languages are compiled into an assembly-like intermediate language, standardized as the Common Language Infrastructure by Ecma International in ECMA-335 [3], which is then ultimately compiled and run on the target machine.

Although special languages such as C# have been created for .NET development, C++ has the ability to use the .NET Framework as well. How well-integrated C++ code is with the .NET Framework, though, is dependent upon how many code changes are made to conform to the new C++/CLI syntax. As TAO and OpenDDS are written in standard C++, this article series will show how they can be adapted to be used in a .NET application.

APPLICATION OVERVIEW

For several years, OCI has been engaged with a customer in the maintenance of a legacy data-acquisition application. Data is collected by remote sensing devices and stored in a database, and the sensing devices are managed, and the data viewed, by an application written for Microsoft Windows.

Although originally a single-user application referencing a local database, over time the application has evolved into one where multiple users can simultaneously connect to a single centralized database. If one user makes a change to the database, all other connected users must be made aware of the change so their local states can be updated.

A solution to this problem is to create a single process to manage access to the database, and to provide database change notifications to interested client applications. Having a single process interact with the database on behalf of clients, instead of allowing each client direct access to the database, ensures that no database change can be made without the system, as a whole, becoming aware of it. Additionally, abstracting the details of the database access from the clients allows the database process be moved to other hosts, or even be implemented on a different platform, without requiring more than just a reference to the new location of the database process to be changed in the configuration of the clients — no client code changes would be necessary.

TAO and OpenDDS were selected as the middleware to accomplish this task for two main reasons. The first is that it is easy to be up and running quickly — the prototype that was developed to illustrate the architecture was completed in under three days. The second is that, as both TAO and OpenDDS are open source, there are no license fees or other costs involved. The resulting application can be deployed widely without incurring a per-seat, or per-CPU, or even a development/SDK charge.

The existing legacy application interacts with a database via ADO.NET [4], a series of classes provided by the .NET Framework, which provide a uniform means of accessing various data source types. The code that uses ADO.NET is in a library, written in C#. The main application that uses this library is written in C++.

In order to solve the problem outlined above, the following architecture was designed. In this diagram, components written in C# shown with box hatching and components written in C++ shown with angled hatching.

Figure 1. Architecture

Figure 1. Architecture

The separate application to manage the database, called the DataServer, was written. This application, in C++, uses the same C# library as before. It now, however, acts as a CORBA server, processing requests from clients to perform database operations. As operations are performed, the DataServer publishes a DDS data sample to clients to notify them of the results of the operation. DDS is used instead of the CORBA Notification Service for notifications as DDS samples are strongly typed (rather than being the CORBA Any type), plus DDS provides quality-of-service policies that the Notification Service does not. This article describes the core elements of the DataServer, as well as a client application, written in C#, that makes use of it. The client performs CORBA operation invocations, and subscribes to the DDS data samples. This article will also illustrate the use of MPC for project maintenance.

settings blue

Sidebar

The code in this article was developed with Microsoft Visual Studio 2005. It was compiled against TAO version 1.6aOpenDDS version 1.3, and MPC version 3.7.2. Inline assembly was disabled to prevent the .NET-related compiler warning C4793, as the use of __asm forces native code generation. Wide character support was enabled, as .NET uses Unicode for string representation. The build settings for these features are as follows:

// add to %ACE_ROOT%\ace\config.h
#define ACE_LACKS_INLINE_ASSEMBLY 1
#define ACE_USES_WCHAR 1

// add to %ACE_ROOT%\bin\MakeProjectCreator\config\default.features
uses_wchar=1

DataLib

The core functionality of the Data Server is provided by DataLib, a library written in C# which interacts with the database. For this example, we'll use System.Data.SQLite, a public domain SQLite ADO.NET provider.

DataLib consists of a single file, DataLib.cs, containing a single class named Database, and referencing several .NET libraries as well as System.Data.SQLite. The structure of the Database class is below. Methods are provided to open and close the database, where, for simplicity, a single database connection is used. Additional methods are provided to create, read, update and delete records in an item table, where an item has both an autogenerated numeric ID and a description. For the implementation of these methods, please see the code archive that accompanies this article.

  1. namespace DataLib
  2. {
  3. using System;
  4. using System.Data;
  5. using System.Data.SQLite;
  6.  
  7. public class Database
  8. {
  9. // open and close the database connection
  10. public bool Open()
  11. public void Close()
  12.  
  13. // create a new item, and return the autogenerated ID
  14. public bool CreateItem(string description, out Int64 id)
  15.  
  16. // read the description from a specific item, given the item ID
  17. public bool ReadItem(Int64 id, out string description)
  18.  
  19. // update the description of an item given its ID
  20. public bool UpdateItem(Int64 id, string description)
  21.  
  22. // delete an item, given its ID
  23. public bool DeleteItem(Int64 id)
  24. }
  25. }

DataLib PROJECT CREATION

Although the DataLib project can be created from within Visual Studio, using MPC (the Makefile, Project, and Workspace Creator) provides a number of benefits:

  1. Different versions of Visual Studio can be supported with one set of project description files. Conversion of solution and project files between versions of Visual Studio is not necessary.
  2. MPC project description (.mpc) files can inherit from base project (.mpb) files, allowing settings such as output directory specification or the setting of a warning level to be made in one place and applied across all projects. If these settings were changed within Visual Studio, changes would have to be made over and over again, once for each project in the solution.
  3. Comments are supported in the project description files, allowing the rationale behind the various settings to be documented.
  4. Often, the default settings for projects can be used without needing to change them, making project descriptions simple.

Documentation for MPC can be found here, though we will describe features of MPC that are useful for this application.

As this system will consist of several projects, we create a base project to allow settings that all projects should inherit. We also set an environment variable, DATASERVER_ROOT, to represent the top-level directory of the project. This allows us to move the entire source tree while correctly maintaining any full paths used in the project files.

The base of all projects in the workspace is named DataServerBase.mpb, and has the following contents:

  1. // DataServerBase.mpb
  2. project {
  3. specific {
  4. Release::install = $(DATASERVER_ROOT)/Output/Release
  5. Debug::install = $(DATASERVER_ROOT)/Output/Debug
  6.  
  7. warning_level = 4
  8. }
  9. }

When generating projects for Visual Studio, it is important to remember that the variables set in the MPC or MPB file reflect ones set in the Visual Studio IDE. Generally, settings which correspond to strings are set directly in the MPC or MPB file, but those that represent dropdowns are set by the numeric index of the choice of interest in the dropdown. In this case, output directories are specified by name, but the warning level is set numerically to 4, which represents the choice of /W4 in the IDE.

As this project will contain different types of projects, written in different languages, it is useful to define base projects which apply to subsets of projects in the workspace. The contents of the file CSBase follows, the base for all C# projects.

  1. // CSBase.mpb
  2. project : DataServerBase {
  3. specific {
  4. // to avoid "Load of property 'ReferencePath' failed. Cannot
  5. // add '.' as a reference path as it is relative. Please specify
  6. // an absolute path." on C# project load into the IDE
  7. libpaths -= .
  8. }
  9. }

By default, MPC adds the current directory to the list of directories where libraries are found, though Visual Studio will generate a warning if the directory is not an absolute path. The entry above removes the current directory from the library paths. This project inherits from DataServerBase, so settings that are made in DataServerBase are applied in addition to what is in this file.

Finally, an MPC file is needed for the project itself. DataLib.mpc is as follows:

  1. // DataLib.mpc
  2. project : CSBase {
  3. // To remove the warning "Load of property 'ReferencePath' failed.
  4. // Cannot add '..\lib' as a reference path as it is relative. Please
  5. // specify an absolute path."
  6. expand(DATASERVER_ROOT) {
  7. $DATASERVER_ROOT
  8. }
  9.  
  10. lit_libs += System System.Data System.Xml
  11. lit_libs += System.Data.SQLite
  12. libpaths += $(DATASERVER_ROOT)\lib
  13. }

The expand option causes the environment variable to be treated as an absolute path — by default, MPC converts environment variables to relative paths. As with the previous issue with libpaths, this also prevents a warning in Visual Studio from being generated when the lib subdirectory is added.

As this is a .NET application, references to various .NET assemblies must be provided, in addition to the System.Data.SQLite assembly, which provides the database connectivity. For this example, System.Data.SQLite.dll is located in the lib subdirectory off of the main project directory, and the libpaths entry adds that directory to the library path.

This project inherits from CSBase, so has all of the settings supplied by CSBase.mpb and DataServerBase.mpb.

It is interesting to note what does not need to be specified. As this is an MPC file for a C# project, you do not need to provide specific names of .cs files — all .cs files in the same directory as the MPC file are automatically included. Also, you do not need to specify a file name for the output — in this instance, MPC will use the base name of the MPC file, which is what we want.

With the project file created, the last step is to create a workspace (.mwc) file, which corresponds to the contents of the Visual Studio solution (.sln) file. DataServer.mwc, located in the project root, looks like this:

  1. // DataServer.mwc
  2. workspace {
  3. specific {
  4. cmdline += -language csharp
  5. DataLib
  6. }
  7. }

For this project, DataLib.cs and DataLib.mpc are in a subdirectory named DataLib, off of the root. The workspace file specifies that the DataLib subdirectory is to be searched for MPC files, and that any MPC files found there should be treated as describing C# projects.

Running MPC on the MWC file generates the solution file. The solution file can then be opened in Visual Studio, the DataLib project compiled, and the DataLib assembly built. As this code was developed using Visual Studio 2005 (VC8), we can generate the solution file by executing:

%ACE_ROOT%\bin\mwc.pl -type vc8 DataServer.mwc

from a console prompt set to the project's root directory.

INTERFACE DEFINITION LANGUAGE (IDL)

Now that the library for data access has been developed, we can write a CORBA server which uses that library. We wish to expose the functionality of the library as a CORBA object, so an interface, described in IDL, must be created. In a subdirectory named IDL off of the root, we create a file, Database.idl, which contains that interface.

  1. // Database.idl
  2. interface Database
  3. {
  4. boolean CreateItem(in wstring description, out long long id);
  5. boolean ReadItem(in long long id, out wstring description);
  6. boolean UpdateItem(in long long id, in wstring description);
  7. boolean DeleteItem(in long long id);
  8. };

The operations of the interface correspond to the client-accessible methods of DataLib::Database, the class defined in DataLib. The IDL types that are used in the interface correspond to the types used in C#. In particular, strings in .NET are in Unicode, so wstring is used to pass them, and as database IDs are 64-bit, long long is needed.

In the IDL subdirectory, create the file IDL.mpc to allow the file to be compiled by the TAO IDL compiler.

  1. // IDL.mpc
  2. project : taoidldefaults {
  3. IDL_Files {
  4. Database.idl
  5. }
  6. custom_only = 1
  7. }

Inheriting from the taoidldefaults base project, a base project included in the TAO distribution, provides the needed infrastructure. We only need to list the IDL file in the IDL_Files section, and MPC generates the tao_idl compilation commands. We do need to indicate that the project has no executable output via the custom_only flag, however.

For this project to be added to the solution file, the workspace file, DataServer.mwc, must be modified to include the IDL directory. After the addition, DataServer.mwc looks like this:

  1. // DataServer.mwc
  2. workspace {
  3. specific {
  4. cmdline += -language csharp
  5. DataLib
  6. }
  7. IDL
  8. }

The IDL project is not in C#, so the IDL directory is listed outside of the specific section. The compilation of this project produces the client stub and server skeleton files, DatabaseC.[cpp,h,inl] and DatabaseS.[cpp,h,inl], respectively.

DATABASE IMPLEMENTATION

With the database interface defined, we can create a C++ class that which implements the interface. We create a subdirectory off of the root named DataServer, and two files in that subdirectory, Database_i.h and Database_i.cpp. The files as presented here were based off of generated implementation files via the -GI option to tao_idl and modified accordingly. Amendments to the generated code are noted here — please see the code archive associated with this article for the full file listings.

These files define class Database_i, an implementation of the Database CORBA interface. An instance of this implementation is called a servant. As we would like the instance of the DataLib::Database class to be maintained by the server itself, we must be able to pass a reference to it to the servant. As DataLib::Database is a .NET class and Database_i is not (as it is a standard, unmanaged C++ class), a reference to the DataLib::Database object must be stored using gcroot<>, a templated helper class provided by the vcclr.h header file. We must add #include  to the top of Database_i.h, and add a class member variable to class Database_i to store the .NET reference (indicated by the caret) to DataLib::Database.

  1. // Database_i.h
  2. class Database_i
  3. : public virtual POA_Database
  4. {
  5. gcroot<DataLib::Database^> database_;
  6. ...

This variable is initialized by the Database_i constructor.

  1. // Database_i.h
  2. Database_i(gcroot<DataLib::Database^> database);
  1. // Database_i.cpp
  2. Database_i::Database_i(gcroot<DataLib::Database^> database) :
  3. database_(database)
  4. {
  5. }

In the methods of Database_i, we use the database_ member variable to reference the DataLib::Database object, such as in the implementation of CreateItem().

  1. // Database_i.cpp
  2. ::CORBA::Boolean Database_i::CreateItem(
  3. const ::CORBA::WChar * description,
  4. ::CORBA::LongLong_out id)
  5. {
  6. System::String^ netDescription = gcnew System::String(description);
  7. ::CORBA::Boolean result = database_->CreateItem(netDescription, id);
  8. delete netDescription;
  9. return result;
  10. }

Implementation of the CORBA Database interface is essentially a translation between CORBA and .NET. In this method, a string provided by CORBA is converted to a .NET String before being passed to DataLib::Database::CreateItem(). The code above also illustrates a benefit of C++/CLI. The variable netDescription is allocated on the garbage-collected heap via gcnew. It can still be determininstically freed, however, by a call to delete, as if it was an allocation made by new on the unmanaged heap. However, if an exception is thrown by the invocation of CreateItem() and delete is not called, netDescription will still be freed by the garbage collector when it executes at some point in the future.

The implementation of ReadItem() also involves string translation, but this time from .NET to CORBA.

  1. // Database_i.cpp
  2. ::CORBA::Boolean Database_i::ReadItem (
  3. ::CORBA::LongLong id,
  4. ::CORBA::WString_out description)
  5. {
  6. System::String^ netDescription;
  7. ::CORBA::Boolean result = database_->ReadItem(id, netDescription);
  8. if (result) {
  9. pin_ptr<const wchar_t> s = PtrToStringChars(netDescription);
  10. description = s;
  11. }
  12. return result;
  13. }

The pin_ptr<> template and PtrToStringChars() function are two more Visual Studio-provided helpers to assist in dealing with .NET types in standard C++. PtrToStringChars() provides a means to directly address .NET String contents, and, as .NET strings are in Unicode, the contents are represented as an array of wchar_t. As the .NET String is on the garbage-collected heap, pin_ptr<> is used to keep the string contents from being relocated until access to it is complete. It must remain accessible until the skeleton marshals it into the GIOP reply.

The implementation of UpdateItem() and DeleteItem() are analagous to the above.

DATASERVER

With the completion of the servant, we can now implement the DataServer itself. In the file DataServer.cpp, the main() function of DataServer begins as most simple CORBA servers do.

  1. // DataServer.cpp
  2. int ACE_TMAIN(int argc, ACE_TCHAR *argv[]) {
  3. try {
  4. // initialize the ORB
  5. CORBA::ORB_var orb = CORBA::ORB_init(argc, argv);
  6.  
  7. // get a reference to the RootPOA
  8. CORBA::Object_var obj =
  9. orb->resolve_initial_references("RootPOA");
  10. PortableServer::POA_var poa =
  11. PortableServer::POA::_narrow(obj.in());
  12.  
  13. // activate the POAManager
  14. PortableServer::POAManager_var mgr = poa->the_POAManager();
  15. mgr->activate();

To create the servant, we first create an object of type DataLib::Database, open the database connection, and pass a reference to the object to the servant's constructor. In this instance, the percent sign in .NET acts somewhat like an ampersand does in standard C++ — it provides a reference to an object.

  1. // open the database
  2. DataLib::Database database;
  3. if (!database.Open())
  4. throw std::exception("Cannot open the database");
  5.  
  6. // create the Database servant
  7. Database_i servant(%database);
  8. PortableServer::ObjectId_var oid =
  9. poa->activate_object(&servant);
  10. CORBA::Object_var database_obj = poa->id_to_reference(oid.in());

There are a number of ways to provide the IOR of an object to callers, such as via a file or via the Naming Service. For this application, we use the IORTable, a TAO-specific feature which allows a client to find a server via a corbaloc URL.

  1. CORBA::String_var ior_str =
  2. orb->object_to_string(database_obj.in());
  3. CORBA::Object_var tobj =
  4. orb->resolve_initial_references("IORTable");
  5. IORTable::Table_var table = IORTable::Table::_narrow(tobj.in());
  6. table->bind("DataServer", ior_str.in());
  7. std::cout << "DataServer bound to IORTable" << std::endl;

main() ends by calling run() on the ORB instance, and by providing cleanup and error reporting.

  1. // accept requests from clients
  2. orb->run();
  3. orb->destroy();
  4. }
  5. catch (CORBA::Exception& ex) {
  6. std::cerr << "CORBA exception: " << ex << std::endl;
  7. }
  8. catch (std::exception& ex) {
  9. std::cerr << "Exception: " << ex.what() << std::endl;
  10. }
  11.  
  12. return 0;
  13. }

We next create an MPC file for the DataServer. It is slightly more complicated than previous MPC files.

  1. // DataServer.mpc
  2. project : taoserver, CPPBase, iortable {
  3. after += IDL
  4. after += DataLib
  5.  
  6. includes += ../IDL
  7. Source_Files {
  8. Database_i.cpp
  9. DataServer.cpp
  10. ../IDL/DatabaseC.cpp
  11. ../IDL/DatabaseS.cpp
  12. }
  13.  
  14. managed = 1
  15. }

In the same way that the IDL project inherits from the taoidldefaults base project to derive behavior, TAO provides other base projects which allow features of TAO to be easily referenced by an application — these base projects set include paths, library linkages, preprocessor symbols and other configuration options so the user of TAO does not have to. As DataServer is an application that uses server components of TAO, it inherits from taoserver. It uses the IORTable, so it inherits from iortable as well — if it had used Naming Service functionality, it would have inherited from naming. We desire it to have the same attributes as other C++ applications in the solution, so it also inherits from CPPBase.

The after statements ensure that the IDL and DataLib projects are built prior to this one. As source code files are not all located in the same directory as the MPC file, we must specify them explicitly via the includes statement and the Source_Files section. Finally, the /clr compiler option must be set to allow .NET functionality to be directly used in C++ code, so managed = 1 is specified.

This project must also be added to the workspace, leading to a DataServer.mwc that looks like this:

  1. // DataServer.mwc
  2. workspace {
  3. specific {
  4. cmdline += -language csharp
  5. DataLib
  6. }
  7. IDL
  8. DataServer
  9. }

We then regenerate the solution file using MPC, rebuild, and now have a working server.

settings blue

SIDEBAR

As discussed in references [5] and [6], incomplete types will generate linker warning LNK4248 when compiled with /clr, and is seen with many types in TAO. For example:

warning LNK4248: unresolved typeref token (01000016) for 
    'TAO_ORB_Core'; image may not run

The actual type that will be used is defined in TAO itself, which is not compiled with /clr. In practice, this warning is harmless, though defining the symbol with an empty body in the module that generates the warning will suppress the message. Please see the file LNK4248.h in the code archive for an approach to this issue.

DataServerConnectorLib

With the server side complete, we can begin development of the client.

The DataServerConnectorLib library acts as a client of the DataServer. More specifically, the class DataServerConnector in this library makes the client-side CORBA calls to invoke methods on the server.

Although this class is implemented in C++ to make use of TAO, this class is a fully-fledged .NET type, so it can be used by the Client application, which is written in C#. For future convenience, the method Run() of this class is executed in a .NET thread to allow an ORB to continue execution independent of the code that uses DataServerConnector, and the Start() and Shutdown() methods manage this thread.

The other public methods of this class mirror the CORBA Database interface, in appropriate .NET syntax.

We start by creating the file DataServerConnectorLib.h in the DataServerConnectorLib subdirectory off of the root and add the following class definitions:

  1. using namespace System;
  2. using namespace System::Threading;
  3.  
  4. class DataServerConnectorState {
  5. CORBA::ORB_var orb_;
  6. Database_var database_;
  7. public:
  8. DataServerConnectorState(CORBA::ORB_ptr orb, Database_ptr database);
  9. Database_ptr DatabasePtr() { return database_; }
  10. CORBA::ORB_ptr OrbPtr() { return orb_; }
  11. };
  12.  
  13. public ref class DataServerConnector {
  14. DataServerConnectorState *state_;
  15. Thread^ thread_;
  16. AutoResetEvent startupEvent_;
  17. void Run();
  18. static void ThreadStart(Object^ param);
  19. public:
  20. DataServerConnector();
  21. ~DataServerConnector();
  22. void Start();
  23. void Shutdown();
  24. bool CreateItem(String ^description, Int64 %id);
  25. bool ReadItem(Int64 id, String^% description);
  26. bool UpdateItem(Int64 id, String^ description);
  27. bool DeleteItem(Int64 id);
  28. };

The using statements allow us to use types from various .NET assemblies without needing to specify the fully qualified names, such as Thread instead of System::Threading::Thread. The DataServerConnector class is declared as a public ref class. The public keyword indicates that the class is visible outside of the assembly; __declspec(dllexport) is not used with .NET types, as it would be with standard Windows dynamic link libraries to export symbols. The ref keyword indicates that the class is a garbage-collected .NET type, and not an unmanaged, standard C++ class.

DataServerConnectorState is, however, an unmanaged, standard C++ class. Unmanaged types (such as CORBA::ORB_var) cannot be member variables of a .NET class, but pointers to unmanaged types can be. DataServerConnectorState acts as a container for the unmanaged state of DataServerConnector.

The implementation of DataServerConnectorState is straightforward — it stores ORB and servant pointers for later use. It resides, with the implementation of DataServerConnector, in the file DataServerConnectorLib.cpp.

  1. // DataServerConnectorLib.cpp
  2. DataServerConnectorState::DataServerConnectorState(CORBA::ORB_ptr orb,
  3. Database_ptr database) {
  4. orb_ = CORBA::ORB::_duplicate(orb);
  5. database_ = Database::_duplicate(database);
  6. }

The Run() method contains the CORBA client implementation. Because ORB_init() requires C-style argc and argv, they must be constructed from the .NET command-line argument array, so we begin by performing that conversion, and then initialize the ORB.

  1. // DataServerConnectorLib.cpp
  2. void DataServerConnector::Run() {
  3. int argc = 0;
  4. wchar_t **argv = NULL;
  5.  
  6. try {
  7. // convert .NET arguments to standard argc/argv
  8. array<String^>^ arguments = Environment::GetCommandLineArgs();
  9. argc = arguments->Length;
  10. argv = new wchar_t *[argc];
  11. for (int i=0; i<argc; i++) {
  12. pin_ptr<const wchar_t> arg = PtrToStringChars(arguments[i]);
  13. argv[i] = _wcsdup(arg);
  14. }
  15.  
  16. CORBA::ORB_var orb = CORBA::ORB_init(argc, argv);

We now obtain an object reference to the Database object. Because the server registered the object in the IORTable, the client can locate it by passing

-ORBInitRef DataServer=
    corbaloc:iiop:server_hostname:server_port/DataServer

on its command line, and calling resolve_initial_references().

  1. // obtain the reference
  2. CORBA::Object_var database_obj =
  3. orb->resolve_initial_references("DataServer");
  4. if (CORBA::is_nil(database_obj.in()))
  5. throw std::exception("Could not get the Database IOR");
  6.  
  7. // narrow the IOR to a Database object reference.
  8. Database_var database = Database::_narrow(database_obj.in());
  9. if (CORBA::is_nil(database.in()))
  10. throw
  11. std::exception("IOR was not a Database object reference");

We now store references to the ORB and Database object for later use, run the ORB to process any requests, and perform cleanup on error. DataConnectorException, a subclass of the .NET Exception class, is defined to wrap and re-throw any exceptions that are generated. This allows native exceptions, such as CORBA::Exception to be propagated to the .NET world.

  1. // save the references via a pointer to an unmanaged class
  2. state_ = new DataServerConnectorState(orb, database);
  3.  
  4. // good to go - tell the outside world
  5. startupEvent_.Set();
  6.  
  7. // run the ORB
  8. orb->run();
  9. orb->destroy();
  10. }
  11. catch (CORBA::Exception& ex) {
  12. std::stringstream ss;
  13. ss << "Exception: " << ex;
  14. throw
  15. gcnew DataConnectorException(gcnew String(ss.str().c_str()));
  16. }
  17. catch (std::exception& ex) {
  18. std::stringstream ss;
  19. ss << "Exception: " << ex.what();
  20. throw
  21. gcnew DataConnectorException(gcnew String(ss.str().c_str()));
  22. }
  23. }

We must also implement methods that wrap the CORBA method invocations. As translation was performed in the DataServer, we do the same, but in reverse — from .NET to CORBA. For example, CreateItem() is defined below. The .NET string is converted to a CORBA::WString via the PtrToStringChars()/pin_ptr<> combination we have used before. The CORBA::LongLong used to store the out parameter from the CreateItem() CORBA interface method is converted to a .NET Int64 to be returned to the caller. Note that the percent sign in the argument list, in this usage, acts as an out parameter. As with Run(), exceptions are propagated as the DataConnectorException type. The other wrapper methods are analagous.

  1. // DataServerConnectorLib.cpp
  2. bool DataServerConnector::CreateItem(String ^description, Int64 %id) {
  3. try {
  4. pin_ptr<const wchar_t> cppDescription =
  5. PtrToStringChars(description);
  6. CORBA::WString_var desc = CORBA::wstring_dup(cppDescription);
  7. CORBA::LongLong cid;
  8. CORBA::Boolean result =
  9. state_->DatabasePtr()->CreateItem(desc, cid);
  10. id = cid;
  11. return result;
  12. } catch (CORBA::Exception& ex) {
  13. std::stringstream ss;
  14. ss << "Exception: " << ex;
  15. throw
  16. gcnew DataConnectorException(gcnew String(ss.str().c_str()));
  17. }
  18. }

After the .NET thread management methods are added, development of the DataServerConnectorLib is complete. We now create an MPC file for it. The options specified are similar to those used in DataServer.mpc.

  1. // DataServerConnectorLib.mpc
  2. project : taoexe, CPPBase {
  3. after += IDL
  4. includes += ../IDL
  5.  
  6. Source_Files {
  7. DataServerConnectorLib.cpp
  8. ../IDL/DatabaseC.cpp
  9. }
  10.  
  11. managed = 1
  12. }

We also add it to DataServer.mwc.

  1. // DataServer.mwc
  2. workspace {
  3. specific {
  4. cmdline += -language csharp
  5. DataLib
  6. }
  7. IDL
  8. DataServer
  9. DataServerConnectorLib
  10. }

Regenerating the solution with MPC and recompiling yields a working DataServerConnectorLib.

CLIENT

The last module we will create is a GUI in C# to demonstrate the system. The GUI consists of a ListView to display messages, and a series of Buttons and TextBoxes to exercise the database methods. Please refer to the code archive for details of the GUI itself.

The implementation of the button click methods invoke the corresponding database functions — the methods exposed by the DataServerConnector class. User input into the TextBoxes associated with each button is used, as appropriate. For instance, the click handler for the Create button is as follows:

  1. private void bCreate_Click(object sender, EventArgs e)
  2. {
  3. try
  4. {
  5. // if input is blank, do nothing, else create the item
  6. if (String.IsNullOrEmpty(tCreateDesc.Text))
  7. return;
  8.  
  9. // invoke the method
  10. long id = 0;
  11. if (dataConnector_.CreateItem(tCreateDesc.Text, ref id))
  12. Log("Item '" + tCreateDesc.Text + "' created with id " + id);
  13. else
  14. Log("Item '" + tCreateDesc.Text + "' could not be created");
  15. }
  16. catch (DataConnectorException ex)
  17. {
  18. Log(ex.Message);
  19. }
  20.  
  21. // after completion (or failure) clear the input
  22. tCreateDesc.Text = "";
  23. }

In this method, tCreateDesc is the TextBox associated with the Create button. If the user has entered text, it will be used as the item description of the item to be created. The call to DataServerConnector::CreateItem() invokes the CORBA method, the ID of the created item is returned (the ref in C# corresponds to the % in C++ in the argument list of DataServerConnector::CreateItem()), and displayed to the user in the ListView via the call to Log(). The other methods are implemented similarly.

With the code complete, we create an MPC file for the Client project, as follows:

  1. // Client.mpc
  2. project : CSBase {
  3. exename = Client
  4.  
  5. after += DataServerConnectorLib
  6.  
  7. specific {
  8. winapp = true
  9. }
  10.  
  11. Source_Files {
  12. *.cs
  13. *.Designer.cs
  14. }
  15.  
  16. Source_Files {
  17. subtype = Form
  18. Client.cs
  19. }
  20.  
  21. Resx_Files {
  22. generates_source = 1
  23. subtype = Designer
  24. Properties/Resources.resx
  25. }
  26.  
  27. lit_libs += System System.Data System.Xml
  28. lit_libs += System.Drawing System.Windows.Forms
  29. }

This MPC file is more complex than we have seen so far, due to the nature of a graphical .NET application. In this project, the file Program.cs contains the C# Main() function, so unless otherwise specified, the output will be named Program.exe. We use the exename keyword to change the name of the output to Client.exe. We must specify winapp = true as, by default, MPC will create a console-based C# application, and Client is a GUI-based one. Two Source_Files sections are necessary, as the file Client.cs contains a subclass of System.Windows.Forms.Form to act as the main window of the application. Form code requires additional infrastructure (e.g., support for one or more associated resource files) that normal code files do not. The resource file Resources.resx is similar in that it has an associated autogenerated C# file that provides access to the resources it contains.

With the MPC file complete, we now add it to the workspace, yielding:

  1. // DataServer.mwc
  2. workspace {
  3. specific {
  4. cmdline += -language csharp
  5. DataLib
  6. Client
  7. }
  8. IDL
  9. DataServer
  10. DataServerConnectorLib
  11. }

CONCLUSION

The following screen shots demonstrate the system. We start two Clients, as well as the DataServer (not shown). For this run, the server was run on the machine oci1373 and started with the following command (on a single line):

DataServer -ORBDottedDecimalAddresses 0 
    -ORBListenEndpoints iiop://:12346

Each of the Client instances were started with this command (on a single line):

Client -ORBDottedDecimalAddresses 0 
    -ORBInitRef DataServer=corbaloc:iiop:oci1373:12346/DataServer

We enter "My First Item" into the TextBox associated with the Create button on the first Client.

Figure 2. Client

Figure 2. Client

Pressing the Create button creates the database item, and the generated ID of 1 is reflected in the ListView.

Figure 3. With Description

Figure 3. With Description

On the second Client, we enter the ID of 1 into the TextBox associated with the Read button.

Figure 4. Read Button

Figure 4. Read Button

Pressing the Read button displays "My First Item" as the item description, demonstrating that the second Client has referenced the same database as the first Client.

Figure 5. Database

Figure 5. Database

This article has described how to use TAO in a .NET application to implement both a CORBA client and server. The next article in this series will show how to incorporate OpenDDS to provide database notifications.

REFERENCES