Search Forum
(53671 Postings)
Search Site/Articles

Archived Articles
712 Articles

C# Books
C# Consultants
What Is C#?
Download Compiler
Code Archive
Archived Articles
Advertise
Contribute
C# Jobs
Beginners Tutorial
C# Contractors
C# Consulting
Links
C# Manual
Contact Us
Legal

GoDiagram for .NET from Northwoods Software www.nwoods.com


 
Printable Version

COM and .Net Interop (A C# Sybase Sql Interpreter)
By Narendra Maharaj

An introduction to .Net Interoperation Services. We develop a COM object providing access to Sybase databases for .Net applications.
Prologue
This document arose when I began to seriously think about connecting to Sybase databases from C# applications. My first attempt was to use the ODBC layer. It was immediately clear that this approach was not going to work. It was about 100 times slower than the usual CT-Library/C++ solution. The framework comes equipped with native, managed drivers for Oracle and Microsoft SQL Server, but not for Sybase. This is not surprising since as far as I know, this database is not widely used outside of the financial district in the United States. As such this article may have a limited audience outside Wall St. So, what can we do? There is no question about it, COM is an entrenched technology. Like most programmers using Microsoft technology, we have used and developed COM objects over the years. With the Interoperation services available in .Net an intriguing solution came to mind. Build a COM object that would expose a subset of the Sybase CT-Library API, and use it to access the Sybase database from .Net/C#. We'll begin with a tiny introduction to .Net Interoperation.
Introduction to .Net Interop
.Net interoperation is the mechanism by which managed code can expose and be exposed to unmanaged code. There are two types of interaction between .Net applications and unmannaged COM code. The first is the ability of .Net clients to call methods implemented by COM interfaces. This is made possible by the Runtime Callable Wrapper (RCW). The other part of the equation is the ability of calling .Net methods from COM code. This is made possible by the COM Callable Wrapper (CCW). There are two main sub-topics that involve RCWs. The first uses the PInvoke service to access the Win32 API. The second uses the type library importer tlbimp.exe to access custom COM code. The "hello world" example of RCW is the Win32 API MessageBox example:

      using System;
      using System.Runtime.InteropServices;
      namespace iopmb
      {
        class Win32Wrapper
        {
          [DllImport("user32.dll", EntryPoint="MessageBoxW", CharSet=CharSet.Auto, ExactSpelling=true)]
          public static extern int MsgBox (int hwnd, string txt, string cap, int type);
        }

        class Class1
        {
          [STAThread]
          static void Main(string[] args)
          {
            Win32Wrapper.MsgBox(0, "Hello Narendra!", "C# MsgBox", 0);
          }
        }
      }
	    
The DllImport attribute is used by the InteropServices to determine where and how to invoke the Win32 call. The argument ExactSpelling indicates that the EntryPoint parameter is the exact name of the method to call. No overhead is incured by the runtime matching the correct method in the specified dll. Note the non-optional "static extern" method decoration. The program produces the message dialog box:
RCW seeks, as much as possible, to hide COM details from .Net clients. The CLR uses its Garbase Collector to manage object lifetimes, but COM uses a counting method, involving the classic AddRef() and Release methods of the IUnknown interface. The RCW handles this counting for us. What's involved in calling custom COM methods is the subject of the main example, the construction of a COM object to access Sybase databases. The following is a basic RCW diagram.
An unmannaged COM Sybase Driver
This section assumes a knowledge of COM and ATL programming, as well as a thorough understanding of Sybase CT-Library programming. I used an inproc server for the COM object since we'll be dealing with potentially large amounts of data. The approach is to start with a standard ATL/COM dll project and build on it. We need only construct a single interface and expose all our CT-Lib methods in it. For this simple program I only implement three CT-Lib calls, ct_connect(), ct_send(), and ct_fetch(). These I expose as co_connect(), co_send() and co_fetch() in the Isyb interface we construct.

      // The Isyb COM interface generated by the ATL IDE.
      interface Isyb : IUnknown
      {
        [helpstring("method co_connect")] HRESULT co_connect(BSTR user,   BSTR passwd,
                                                             BSTR dbsrvr, BSTR host);
        [helpstring("method co_fetch")] HRESULT co_fetch(BSTR* row, BSTR* heading);
        [helpstring("method co_send")] HRESULT co_send(BSTR sqlstr, [out, retval]int* ret);
      };
	    
These are the most important parts of the CT-Lib API, as far as connecting and doing a query. The co_connect() method does the standard CT-Lib connection to a database: Allocate context and connection objects, apply the properties and call ct_connect(). The co_send() method is also straightforward: Setup a command structure with the required query and call ct_send(). I pass the return code back to the caller via an int pointer:
STDMETHODIMP Csyb::co_send(BSTR sqlstr, int* ret) ...
The final API, co_fetch() is a little more involved since I combined the main CT-Lib API's used to fetch results in this one COM call. In CT-Lib, two loops are used to collect result sets. The outer loop controlled by ct_results() and the inner loop by ct_fetch(). Note, I am only concerned with ct_results() of type CS_ROW_RESULT, there are many others. I also separate the heading of the result set from the actual result set into two BSTR return types.
STDMETHODIMP Csyb::co_fetch(BSTR* row, BSTR* heading) ...
This way a caller may disregard the heading if necessary. A code fragment follows, see the zip file for the complete code.

      STDMETHODIMP Csyb::co_fetch(BSTR* row, BSTR* heading)
      {
        USES_CONVERSION;
        while ((ct_results (cmd, &res_type)) == CS_SUCCEED)
        {
          if (res_type == CS_ROW_RESULT)

          //  *** CODE DELETED  ***

          retcode = CS_SUCCEED;
          while (retcode == CS_SUCCEED)
          {
            retcode = ct_fetch(cmd, CS_UNUSED, CS_UNUSED, CS_UNUSED, &rows_read);
            if (retcode == CS_SUCCEED)
            {
              for (i = 0; i < numcols; i++)
              {
                br += d[i].val;
                br += L"\t";
              }
              br += L"\n";
          }  // inner while
        } // outer while

        //  *** CODE DELETED  ***

      }
	    
Build the project to create the sybcom.dll library.
A C# Sybase Sql Interpreter
In order to run this program, you must have both access to a Sybase server and a correctly configured client environment. If you can log into your Sybase database from isql then you should be fine. First open the Visual Studio .Net Command Prompt and goto the directory containing the newly created sybcom.dll library. Invoke the type library importer with the following:
tlbimp.exe symcom.dll /out:sybcomassembly.dll
We just generated a .Net assembly. It contains all the information necessary to correctly call our COM interface methods. Open the new assembly with ILDasm and take a look at the manifest. Notice how the methods of the interface are mapped, see the diagram highlighted in grey.
The parameters with [in] attributes are mapped as C# in parameters, the [out] attribute parameters are C# references and the [out, retval] attribute is a return value.
To call these methods from C# (.Net) we simply reference the assembly and instiantiate the COM object. Then we are free to call the COM interface methods as any other C# method. Notice how output parameters are passed as ref, and the [out, retval] parameter is mapped to a return value automatically.

				...
      syb = new sybClass();
      syb.co_connect(muser, mpasswd, mserver, mhost);
				...
      if (sc.syb.co_send(sqltxt.Text) == 0)
      {
        string row_data = null;
        string heading_data = null;
        sc.syb.co_fetch(ref row_data, ref heading_data);
      }
				...
	    
The Sql Interpreter is very simple. It consists of two paneled RichTextBoxes separated by a splitter. Enter sql queries in the top box and see the results in the bottom box.
Download sybseinterop.zip

Conclusion

I must stress that the COM object presented here for accessing Sybase databases is not production quality. In particular there is little in the way of error checking. Also, it only provides access to returned row result sets, not return parameters, or stored procedure return codes. However it is very fast since it uses the same method a C++/CT-Lib programmer would use. Finally there is a commercially available, managed Sybase driver from DataDirect, it is also very fast. You may also want to consider that option.
References
ATL Developer's Guide - Tom Armstrong (The best COM book I have read)
Begining ATL COM - Richard Grimes
Professional DCOM Programming - Richard Grimes
Essential COM - Don Box
Open Client Client-Library/C Reference Manual - Sybase Corp.
The excellent Interop articles on this and other sites.
December 22, 2002