Wrapping C with C++ in .NET

A common software requirement is for code written in one computer language to be used by software developed in a different computer language. George shows how existing C software can be wrapped in C++, thus making it more accessible from .NET languages such as C# and VB.NET.


January 01, 2004
URL:http://www.drdobbs.com/wrapping-c-with-c-in-net/184401742

A common software requirement is for code written in one computer language to be used by software developed in a different computer language. For instance, it may be necessary to access C functions from a .NET or Java application. In this article, I show how existing C software can be wrapped in C++ and thus made easily accessible from .NET languages such as C# and VB.NET. Although I use the NAG C library from the Numerical Algorithms Group (http://www.nag.co.uk/) as an example, the method is general and can be applied to other C software. In particular, I illustrate the technique by considering four NAG C library numeric routines, which have applications in computational finance:

COM, .NET Assemblies, and Managed C++

Microsoft COM already enables the creation of numeric components that can be used by the complete range of Windows programming languages. In fact, COM objects can be used within .NET. However, from a programmer's point of view, wrapping C code in COM C++ classes has the disadvantage that there is visible Microsoft COM baggage that needs to be carried around. This has the effect of obscuring the code and also making it difficult to implement the C++ classes on UNIX platforms. Another limitation is that the classes contained within a COM object cannot be used to create other derived classes.

.NET assemblies have improved this situation. In a nutshell, the classes in an assembly can be coded in any .NET language, then used by any other .NET language. Thus, it is possible to create assemblies in managed C++ that provide class wrappers for C routines, then use these from C# and VB.NET software [6].

Listing 1 shows the ANSI function prototypes of four NAG C functions. Listing 2 is the managed C++ code used to create an assembly that wraps the C functions. I call this assembly "naglib," and it defines the namespace NAGLIB and the managed class NAG_FUNCTIONS, which provides functions to access native C routines contained within the DLL "nagc." Listing 2 is meant for illustrative purposes and not intended to be a statement of good programming practice. However, I have included some useful features, such as flagging errors and setting default parameter values via the constructor NAG_FUNCTIONS().

As you can see, the code is almost standard C++ and (in contrast to the equivalent COM approach) could easily be ported to UNIX platforms. I now examine each nonstandard C++ (that is, Microsoft-specific) feature.

The NAG C library routines used are contained in a DLL called "nagc." Here, each function is imported into the C++ project by name. For instance:

[DllImport("nagc")]
extern "C" Double s15abc(Double x);

is used to import the function s15abc, which computes the cumulative normal distribution.

The directive gc indicates that the code is managed and memory is allocated on the garbage collected (GC) heap; unmanaged code is indicated by nogc.

In Listing 2, the .NET data types Double and Int32 have been used so that the assembly can be accessed by both C# and VB.NET code. All managed .NET code written in VB.NET, C#, and C++ is compiled to the same intermediate language (IL) code. To permit interoperability within .NET, there is a common type system (CTS) that standardizes the basic data types across all languages. Table 1 provides a summary of the .NET data types corresponding to the C++ types double and long.

In the case of numerical integration and optimization, user-defined functions (or call-back functions) need to be passed as parameters to the NAG C library routine. This is done in .NET by declaring a delegate with the same signature (that is, return type and parameter types) as the callback function. For example:

public __delegate double INTEGRAND_FUN_TYPE(Double x);

declares the delegate INTEGRAND_FUN_TYPE with a signature corresponding to functions that return a Double and have a single Double parameter passed by value. This delegate is used by the numerical integration routine d01ajc for defining the integrand. The declaration of a delegate is similar to the declaration of a function prototype with the additional words public (or private) and delegate. Also, the declaration and use of delegates in Listing 2 has similarities with the declaration and use of function pointers in Listing 1.

A more complicated delegate example is:

public __delegate void OBJ_FUN_TYPE (Int32 n, double *x, double * objf, double *g, Int32 comm);

which declares the delegate OBJ_FUN_TYPE. This is done with a signature that applies to subroutines (that is, functions that return void) with parameters of type Int32 and double *. As you can see, I had to use double * instead of the more general Double *. This delegate is used by the numerical optimization routine e04dgc for specifying the objective function to be minimized.

Accessing the Naglib Assembly from C#

The C# code in Listing 3 (and project screen in Figure 1) illustrate how the assembly naglib can be accessed from a C# console project created using Visual Studio .NET. You can see that the C# code defines the classes DCLASS and RUNIT. DCLASS is derived from the class NAG_FUNCTIONS and supplies definitions for the callback functions used by the member functions OPTIMIZE and QUADRATURE.

The class RUNIT only contains the member function Main. This function is run by the example console application, and all the computations are performed by a single numeric object (tt) of type DCLASS.

The assembly containing the namespace NAGLIB is accessed with the statement using NAGLIB, which occurs on the third line of Listing 3. (See Listing 4 for the output from Listing 3.) I use Math.PI to return the value of , Math.Sin(x) to compute sin(x), and Math.Exp(x) to evaluate exp(x). These functions are members of the class Math, which is contained in the namespace System.

This directive is necessary because C# does not really support pointers. The keyword unsafe lets you use pointers and thus easily pass scalars and arrays by reference to the managed C++ class NAG_FUNCTIONS contained in the namespace NAGLIB.

The statement DCLASS tt = new DCLASS() creates a numeric object tt with the type of the derived class DCLASS. Since DCLASS was derived from NAG_FUNCTIONS, it allows access not only to the public member functions objfun and the_integrand_c, but also the public member functions of NAG_FUNCTIONS: CUM_NORM, OPTIMIZE, QUADRATURE, and REAL_SYMM_EIGEN. This means that I can compute the cumulative normal distribution and perform eigenvalue computations by using statements of the form:

the_answer = tt.CUM_NORM(x);
flag = 0;
// first row
a[0,0] = 0.5;
a[0,1] = 0.0;
 ...
// fourth row
a[3,0] = -2.6;
a[3,1] = -0.7;
a[3,2] = 0.0;
a[3,3] = 0.5;
tt.REAL_SYMM_EIGEN(n, ref a[0,0], tda, ref r[0], ref flag);

Note that the keyword ref is used to pass the address of a[0,0], r[0], and flag to the member function REAL_SYMM_EIGEN.

Using Numeric Objects with Member Functions Requiring Delegates

We will now consider how to call the numerical integration function QUADRATURE. This is achieved using the following C# statement:

INTEGRAND_FUN_TYPE myfun_c = new INTEGRAND_FUN_TYPE (tt.the_integrand_c);

to declare (and also define) the delegate myfun_c, of type INTEGRAND_FUN_TYPE, which corresponds to the user-defined function the_integrand_c contained in the derived class DCLASS. The next step is to pass the appropriate parameters to the function tt.QUADRATURE. For example:

a1 = 0.0;
b1 = Math.PI*2.0;
flag = 0;
the_answer = 0.0;
tt.QUADRATURE(a1, b1, ref the_answer, ref abserr, ref flag, myfun_c);

The method for calling the numerical optimization member function is similar. For instance, in the example code, we use:

OBJ_FUN_TYPE myobjfun = new OBJ_FUN_TYPE (tt.objfun);
x2[0] = -1.0;
x2[1] = 1.0;
n2 = 2;
flag = 0;
tt.OPTIMIZE(n2, ref x2[0], ref g[0], ref objf, ref flag, myobjfun);

The initial parameter estimates and computed optimal values are contained in the array x2. The estimated gradient at the solution point is returned in the array g, and the parameter objf contains the value of the minimized objective function.

Accessing the Naglib Assembly from VB.NET

The assembly naglib can be used from VB.NET in a similar manner to that described for C#; see Listing 5.

Conclusion

A major benefit of the approach presented here over COM is that the managed C++ wrapper code can, with little effort, be used on UNIX platforms. In addition, unlike COM, you can create C# or VB.NET derived classes from the managed C++ (base) classes. As more software supports .NET (as, for example, Excel 2003 will), the future of object-oriented numerics in .NET looks promising.

References

[1] Black, F. and M. Scholes. "The Pricing of Corporate Liabilities," Journal of Political Economy, 1973.

[2] Rebonato, R. Interest-rate Option Models, Second Edition, John Wiley, 1998.

[3] Levy, G.F. Computational Finance, Numerical Methods for Pricing Financial Instruments, Heinemann Press, 2003.

[4] Hull, J.C. Options Futures and Other Derivatives, Prentice Hall, 1997.

[5] Markowitz, H.M. "The General Mean-Variance Portfolio Selection Problem," Phil. Trans. R. Soc. Lond. 1994.

[6] Challa, S. and A. Laksberg. Essential Guide to Managed Extensions for C++, Apress, 2002.


George Levy, who holds a doctorate in mathematical physics from Oxford University, is a consulting software engineer and author of the upcoming book Computational Finance: Numerical Methods for Pricing Financial Instruments. He can be contacted at [email protected].


January 04:

Figure 1: A view of the C# example project.

January 04:

Listing 1: The ANSI function pointers and function prototypes for the NAG C routines.

/* declaration of function pointer E04DGC_FUN */
typedef void (*E04DGC_FUN)(long, double *, double *, double *, Nag_Comm *);
/* declaration of function pointer D01AJC_FUN */
typedef double (*D01AJC_FUN)(double);
/* declaration of function prototypes */
void e04dgc(long n, E04DGC_FUN objfun, double x[], double *objf, double grad[],
                  Nag_E04_Opt *options, Nag_Comm *user_comm, NagError *fail);
void f02aac(long n, double *a, long tda, double *r, NagError *fail);
void d01ajc(D01AJC_FUN f, double a, double b, double epsabs, 
     double epsrel, long max_num_subint, double *result, 
     double *abserr, Nag_QuadProgress *qp, NagError *fail);
double s15abc(double x, NagError *fail);




January 04: 

Listing 2: Managed C++ code used to create the assembly naglib, which contains the namespace NAGLIB and wraps the NAG C library functions in the class NAG_FUNCTIONS.

extern "C" {
   #include <stdio.h>
   #include <nag.h>
   #include <nag_stdlib.h>
}
#using <mscorlib.dll>
using namespace System::Runtime::InteropServices;
using namespace System;
namespace NAGLIB {
   public __delegate double INTEGRAND_FUN_TYPE(Double x);
   public __delegate void OBJ_FUN_TYPE (Int32 n, double *x, 
                                    double *objf, double *g, Int32 comm);
   [DllImport("nagc")]
   extern "C" void e04dgc(Int32 n, OBJ_FUN_TYPE *f, Double *x, Double *objf,
               Double *g, Nag_E04_Opt *options, Int32 comm, NagError *flag);
   [DllImport("nagc")]
   extern "C" void e04xxc(Nag_E04_Opt *options);
   [DllImport("nagc")]
   extern "C" void d01ajc(INTEGRAND_FUN_TYPE *f, Double a, Double b,
        Double epsabs, Double epsrel, Int32 max_num_subint, Double* result,
        Double *abserr, Nag_QuadProgress *qp, NagError *flag);
   [DllImport("nagc")]
   extern "C" void f02aac(Int32 n, Double *a, Int32 tda, 
                         Double *r, NagError *eflag);
   [DllImport("nagc")]
   extern "C" Double s15abc(Double x);
   public __gc class NAG_FUNCTIONS
   {
      public:
      Double QUADRATURE_epsabs;
      Double QUADRATURE_epsrel;
      Int32 QUADRATURE_max_subint;
      NAG_FUNCTIONS()
      { // the constructor: set default values
      QUADRATURE_epsabs = 0.0;
      QUADRATURE_epsrel = 0.0001;
      QUADRATURE_max_subint = 200;
      }
      void REAL_SYMM_EIGEN (Int32 n, Double *a, Int32 tda, 
                                                 Double *r, Int32 *flag)
      {
         NagError eflag;
         INIT_FAIL(eflag);
         f02aac(n, a, tda, r, &eflag);
         *flag = (Int32)eflag.code;
     }
     Double CUM_NORM (Double x) {
        return s15abc(x);
     }
     void OPTIMIZE (Int32 n, Double *x, Double *g, Double *objf, 
                                       Int32 *flag, OBJ_FUN_TYPE *the_fun)
     {
        NagError eflag;
        Nag_E04_Opt options;
        INIT_FAIL(eflag);
        e04xxc(&options);
        options.print_level = Nag_NoPrint;
        options.list = 0;
        options.verify_grad = Nag_NoCheck;
        e04dgc(n, the_fun, x, objf, g, &options, (Int32)0, &eflag);
        *flag = (Int32)eflag.code;
     }
     void QUADRATURE (Double a, Double b, Double *result, 
                     Double *abserr, Int32 *flag, INTEGRAND_FUN_TYPE *the_fun)
     {
       Nag_QuadProgress qp;
       NagError eflag;
       INIT_FAIL(eflag);
       d01ajc(the_fun, a, b, QUADRATURE_epsabs, QUADRATURE_epsrel, 
                         QUADRATURE_max_subint, result, abserr, &qp, &eflag);
       *flag = (Int32)eflag.code;
     }
  };
}




January 04: 

Listing 3: C# code that uses the assembly naglib in a C# console application.

using System;
using System.Runtime.InteropServices;
using NAGLIB;
namespace USE_NAGLIBS
{
   class DCLASS : NAG_FUNCTIONS
      {
         public unsafe void objfun (Int32 n, Double *x, 
                                   Double *objf, Double *g, Int32 comm)
         {
            Double ex1, x1, x2;
            ex1 = Math.Exp(x[0]);
            x1 = x[0];
            x2 = x[1];
            *objf = ex1*(4.0*x1*x1 + 2.0*x2*x2 + 4.0*x1*x2 + 2.0*x2 + 1.0);
            g[0] = 4.0*ex1*(2.0*x1 + x2) + *objf;
            g[1] = 2.0*ex1*(2.0*x2+2.0*x1 + 1.0);
        }
        public Double the_integrand_c(Double x)
        {
           Double pi = Math.PI;
           Double val;
           val = (x*Math.Sin(x*30.0)/1.0-x*x/(pi*pi*4.0));
           return val;
     }
}
class RUNIT
{
   static unsafe void Main(string[] args)
   {
      Int32 tda = 4, n = 4, n2 = 2, j, flag = 0;
      Double [] r = new Double [30];
      Double [] x2 = new Double [2];
      Double [] g = new Double [2];
      Double a1, b1, objf = 0.0, abserr = 0.0, the_answer, x;
      Double [,] a = new Double[n,n];
      DCLASS tt = new DCLASS();

      x = -1.0;
      the_answer = tt.CUM_NORM(x);
      Console.WriteLine ("The value of the cumulative normal = {0,8:F4}", the_answer);
      INTEGRAND_FUN_TYPE myfun_c=new INTEGRAND_FUN_TYPE (tt.the_integrand_c);
      OBJ_FUN_TYPE myobjfun = new OBJ_FUN_TYPE (tt.objfun);
      a1 = 0.0;
      b1 = Math.PI*2.0;
      flag = 0;
      the_answer = 0.0;
      tt.QUADRATURE(a1, b1, ref the_answer, ref abserr, ref flag, myfun_c);
      Console.WriteLine("The integral (default maximum number of 
                                     subintervals) = {0,8:F6} ",the_answer);
      flag = 0;
      tt.QUADRATURE_max_subint = 3;
      tt.QUADRATURE(a1, b1, ref the_answer, ref abserr, ref flag, myfun_c);
      Console.WriteLine("The integral (maximum number of 
                        subintervals set to 3) = {0,8:F6} ",the_answer);
      x2[0] = -1.0;
      x2[1] = 1.0;
      n2  = 2;
      flag = 0;
      tt.OPTIMIZE(n2, ref x2[0], ref g[0], ref objf, ref flag, myobjfun);
      Console.Write("The optimization solution vector is :");
      for (j = 0; j < 2; ++j) {
         Console.Write("{0,8:F4}",x2[j]);
      }
      Console.WriteLine();
      Console.WriteLine("The value of the objective function is: {0,8:E4}",objf);
      flag = 0;
      // first row
      a[0,0] = 0.5;
      a[0,1] = 0.0;
      a[0,2] = 2.3;
      a[0,3] = -2.6;
      // second row
     a[1,0] = 0.0;
     a[1,1] = 0.5;
     a[1,2] = -1.4;
     a[1,3] = -0.7;
     // third row
     a[2,0] = 2.3;
     a[2,1] = -1.4;
     a[2,2] = 0.5;
     a[2,3] = 0.0;
     // fourth row
    a[3,0] = -2.6;
    a[3,1] = -0.7;
    a[3,2] = 0.0;
    a[3,3] = 0.5;
    tt.REAL_SYMM_EIGEN(n, ref a[0,0], tda, ref r[0], ref flag);
    Console.Write("The Eigenvalues are:");
    for (j = 0; j <= 3; ++j) {
       Console.Write("{0,8:F4}", r[j]);
    }
    Console.WriteLine();
    }
  }
}




January 04: 

Listing 4: Output from Listing 3.

The value of the cumulative normal = 0.1587
The integral (default maximum number of subintervals) = -2.303835
The integral (maximum number of subintervals set to 3) = -3.168259
The optimization solution vector is : 0.5000 -1.0000
The value of the objective function is: 2.7457E-014
The Eigenvalues are: -3.0000 -1.0000 2.0000 4.0000




January 04: 

Listing 5: Using the numeric objects from VB.NET.

Imports System
Imports System.Runtime.InteropServices
Imports NAGLIB

Module Module1
   Public Class DCLASS
      Inherits NAG_FUNCTIONS
      Public Function the_integrand_c(ByVal x As Double) As Double
         Dim pi As Double
         Dim val As Double
         pi = Math.PI
         val = (x * Math.Sin(x * 30) / 1 - x * x / (pi * pi * 4))
         Return val
      End Function
   End Class
   Sub Main()
      Dim x, the_answer As Double
      Dim flag, j, tda, n As Integer
      Dim a(,), r(), a1, b1, abserr As Double
      Dim tt As New DCLASS()
      Dim myfun As New INTEGRAND_FUN_TYPE(AddressOf tt.the_integrand_c)
      a1 = 0
      b1 = Math.PI * 2
      flag = 0
      tt.QUADRATURE(a1, b1, the_answer, abserr, flag, myfun)
      Console.WriteLine("The integral (default number of 
                                     subintervals) = {0,8:F4}", the_answer)
      tt.QUADRATURE_max_subint = 3
      flag = 0
      tt.QUADRATURE(a1, b1, the_answer, abserr, flag, myfun)
      Console.WriteLine("The integral (number of subintervals 
                                     set to 3) = {0,8:F4}", the_answer)
      x = -1
      the_answer = tt.CUM_NORM(x)
      Console.WriteLine("The value of the cumulative  normal = {0,8:F4}", the_answer)
      flag = 0
      n = 4
      tda = n
      ReDim r(n - 1)
      ReDim a(n - 1, n - 1)
      ' first row
      a(0, 0) = 0.5
      a(0, 1) = 0
      a(0, 2) = 2.3
      a(0, 3) = -2.6
      'second row
      a(1, 0) = 0
      a(1, 1) = 0.5
      a(1, 2) = -1.4
      a(1, 3) = -0.7
      ' third row
      a(2, 0) = 2.3
      a(2, 1) = -1.4
      a(2, 2) = 0.5
      a(2, 3) = 0
     'fourth row
     a(3, 0) = -2.6
     a(3, 1) = -0.7
     a(3, 2) = 0
     a(3, 3) = 0.5
     tt.REAL_SYMM_EIGEN(n, a(0, 0), tda, r(0), flag)
     Console.Write("The Eigenvalues are:")
     For j = 0 To n - 1
        Console.Write("{0,8:F4}", r(j))
     Next j
     Console.WriteLine()
  End Sub
End Module






January 04: 

Listing 6: Output from Listing 5.

The integral (default number of subintervals) = -2.3038
The integral (number of subintervals set to 3) = -3.1683
The value of the cumulative normal = 0.1587
The Eigenvalues are: -3.0000 -1.0000 2.0000 4.0000




January 04: 

Table 1: Data types used by C++, C#, VB.NET, and the .NET common type system (CTS).

Terms of Service | Privacy Statement | Copyright © 2024 UBM Tech, All rights reserved.