Programming in C++/CLI

De Ensiwiki
Aller à : navigation, rechercher

Introduction

This page contains serveral tips and tricks on C++/CLI programming. Here you will find a summary of the differences in syntax between native C++ and C++/CLI, as well as a tutorial showing how native and managed libraries can interact. Answers to other questions about C++/CLI, Visual Studio or other libraries that are used in the Monte Carlo course (such as libpnl) can be found here.

Syntax differences between native and managed code

NativeManaged
Class declaration

class NativeClass{ (...) }

ref class ManagedClass{ (...) }

Pointers (*), handles (^)

NativeClass *clNat = new NativeClass();

ManagedClass ^clMan = gcnew ManagedClass();

Dereferencing, references, tracking references

  • NativeClass clNat2 = *clNat;
  • NativeClass &clNat3 = clNat2;
  • NativeClass *clNat4 = &clNat2;

  • ManagedClass clMan2 = *clMan;
  • ManagedClass %clMan3 = clMan2;
  • ManagedClass ^clMan4 = %clMan2;

Arrays

  • int oneDimension[10];
  • int twoDimensions[10][15];

  • array<int,1> ^oneDimension;
  • array<int,2> ^twoDimensions;

A complete example

This is a step-by-step description of how to construct an application (Windows Form, ASP.NET or WCF) that computes the price of a vanilla call using a Monte Carlo algorithm.
The part about the way to handle native dlls is a condensed version of this page.
The code described here should work under VS2010, VS2012 and VS2013.

The solution consists of:

  • A native C++ project that handles the Monte Carlo simulations and uses the libpnl library;
  • A C++/CLI project for the interface between native and managed code;
  • A .NET project for the interface.

Warning: this tutorial assumes that libpnl has already been installed on the computer. (see this page)


Native C++ project

Creating the project

  1. In Visual Studio, go to the menu File > New > Project, select an Empty project in the Visual C++ tab
  2. Choose a project name ("Computations" in this example) and a solution name ("ProjetTest" in this example)
  3. In the Win32 wizard, click on Next, select "DLL", "Empty project" and click on Done.
    Creating a new project Empty project
  4. Set the solution to be compiled in 64 bits (see this item).

Code to insert

In the Solution explorer, choose the Header files folder, add a new header file ("Computations.hpp" in this example), and in the "Source files" folder, add a cpp file ("Computations.cpp" in this example).

Add the following code to the header file:

#pragma once
#define DLLEXP   __declspec( dllexport )
namespace Computations{
   DLLEXP void calleuro (double &ic, double &prix, int nb_samples, double T,
              double S0, double K, double sigma, double r); 
}

And the following code to the source file:

#include "Computations.hpp"
#include <iostream>
#include <time.h>
#include "pnl/pnl_random.h"

using namespace std;


void Computations::calleuro (double &ic, double &prix, int nb_samples, double T,
               double S0, double K, double sigma, double r)
{
  double drift = (r - sigma * sigma/2.) * T;
  double sqrt_T = sqrt (T);
  double sum = 0;
  double var = 0;
  PnlRng *rng = pnl_rng_create (PNL_RNG_MERSENNE);
  pnl_rng_sseed (rng, time(NULL));
  double payoff;
  for ( int i=0 ; i<nb_samples ; i++ )
    {
      payoff = S0 * exp (drift + sigma * sqrt_T * pnl_rng_normal (rng));
      payoff = MAX(payoff - K, 0.);
      sum += payoff;
      var += payoff * payoff;
    }

  prix = exp (-r*T) * sum / nb_samples;
  var = exp (-2.*r*T) * var / nb_samples - prix * prix;
  ic = 1.96 * sqrt (var / nb_samples); 
  pnl_rng_free (&rng);
}

Properly link libpnl to the project (see this page).

Build the project. If everything works as expected, the 'x64/Debug' folder should contain the native project's dll and lib files (Computations.dll and Computations.lib).

C++/CLI project

This project is meant to be an interface between the native code from the previous project and the managed code that will be implemented afterwards.

  1. Add a new project (Visual C++ class library, named "Wrapper" in this example) to the solution
  2. Add the path to the native project to the additional Include folders
  3. Add the native project as a reference to the new project (Right-click >References ... > Add a new reference)
  4. In the case where the native dll will be invoked from the web (ASP.NET or a web service), go to Properties -> Linker -> Input) and add the name of the native dll to the Delay Loaded Dlls field (Computations.dll in this example)
  5. Set the target platform to x64 (nb: remember to unselect the 'Create new solution platforms' checkbox when creating the x64 platform)

Add the following code to the header file:

#pragma once
#include "Computations.hpp"
using namespace System;

namespace Wrapper {

	public ref class WrapperClass
	{
	private:
		double confidenceInterval;
		double price;
	public:
		WrapperClass() {confidenceInterval = price = 0;};
		void getPriceCallEuro(int sampleNb, double T, double S0, double K, double sigma, double r);
		double getPrice() {return price;};
		double getIC() {return confidenceInterval;};
	};
}

Nb: the #include "Computations.hpp" directive can be added to the stdafx.h file, instead of to the header file.

Add the following code to the source file:

#include "stdafx.h"

#include "Wrapper.h"

using namespace Computations;
namespace Wrapper {
	void WrapperClass::getPriceCallEuro(int sampleNb, double T, double S0, double K, double sigma, double r) {
		double ic, px;
		calleuro (ic, px, sampleNb, T, S0, K, sigma, r);
		this->confidenceInterval = ic;
		this->price = px;
	}
}

Build the Wrapper project.

Using the native code

From a Windows Form

What follows is a description of the way of linking the different components in a clean way, without any unnecessary copies. A more basic way of running the application would be to simply copy all the necessary native dlls (all the dlls of the 'lib' folder of libpnl, the 'libpnl.lib' file, and the dll and lib file of the native project) into the folder containing the Windows Form .exe file. This copy can be done by hand, or in the build related events of the project; the automated copy instructions are explained in detailed here.

Add a new project to the solution (C# project: Windows Form), and define this project as the startup project (right-click -> set as startup project). Set the target platform to x64 (nb: remember to unselect the 'Create new solution platforms' checkbox when creating the x64 platform)


  1. Add the C++/CLI project as a reference to this new project
  2. In the project properties, Parameters tab, add a new parameter of type string (called AdditionalPaths in the code below), that contains the paths to the native dll and to the lib folder of libpnl (format : <path1>;<path2> )
    Paths to explore
  3. Use the Toolbox to add the necessary textboxes to retrieve the parameters of the option and the number of Monte Carlo samples, as well as two labels to output the price and confidence interval of the option ("priceLabel" and "icLabel" in the example).
  4. Add a button (called goBtn in the example)
  5. Add the directive
    using Wrapper;
    
    to the source code of the form (Form1.cs in the example)

Add the following code to the button clicked event (properties explorer, "lightning" symbol to show the names of the methods associated to events; double-click on the "click" event to create and show a code skeleton of the actions to perform when the button is clicked):

private void goBtn_Click(object sender, EventArgs e)
{
    // Retrieve the values of the parameters in the TextBoxes
    WrapperClass wc = new WrapperClass();
    wc.getPriceCallEuro(nbSamples, maturity, S0, strike, sigma, r);
    prixLabel.Text = wc.getPrice().ToString();
    icLabel.Text = wc.getIC().ToString();
}

In Program.cs, add the following code instead of Main:

static void Main()
{
    string newPath = string.Concat(Environment.GetEnvironmentVariable("PATH"), ";", Properties.Settings.Default.AdditionalPaths);
    Environment.SetEnvironmentVariable("PATH", newPath, EnvironmentVariableTarget.Process);
    Application.EnableVisualStyles();
    Application.SetCompatibleTextRenderingDefault(false);
    Application.Run(new Form1());
}

Compile and run.

From an ASP.NET web application

In an ASP.NET project, it is necessary for the server to know where the dlls can be found. It is sufficient to add them to a folder in the PATH, but the simplest way to proceed is to put all of the necessary dlls in the correct bin folder.

NB: if the web application is a 64 bit web app, in order to get it working, it is necessary to specify that IIS Express needs to run as a 64 bit process. See this link to specify this.

Add to the solution a new project (ASP.NET application), and set it as the startup project.

  1. Add the C++/CLI project as a reference to the new project (remember to add the name of the native dll to the Delay Loaded Dlls field)
  2. Add the necessary textboxes and labels to the 'Default.aspx' page
  3. Add a button to launch the computations
  4. Similarly to the Windows Form project, add the necessary instructions to the button clicked event to launch the computations and output the results

Copy the native dll, as well as the .dll and .lib files from the libpnl bin folder to the bin folder of the ASP.NET project. This can be done automatically, as described here.

In Global.asax, it is necessary to specify that some dlls can be found in the bin directory of the ASP.NET project. Add the following code:

 void Application_Start(object sender, EventArgs e)
 {
     String _path = String.Concat(System.Environment.GetEnvironmentVariable("PATH"), ";", System.AppDomain.CurrentDomain.RelativeSearchPath);
     System.Environment.SetEnvironmentVariable("PATH", _path, EnvironmentVariableTarget.Process);
 }

Launch on a web browser.

From a WCF web service

This section describes how to create a web service allowing the client to invoke the functions in the native dll.

Creating the WCF application

  1. Add a new project WCF Service Application
  2. Replace the code in IService1.cs by this one:
    namespace MyWcfService
    {
      [ServiceContract]
      public interface IService1
      {
        [OperationContract]
        double GetPrice(int sampleNb, int maturity, double S0, double strike, double sigma, double r);        
      }
    }
    
  3. Replace the code in Service1.svc.cs by this one:
    using Wrapper;
    namespace MyWcfService
    {
      public class Service1 : IService1
      {
        public double GetPrice(int sampleNb, int maturity, double S0, double strike, double sigma, double r)
        {
          WrapperClass wc = new WrapperClass();
          wc.getPriceCallEuro(sampleNb, maturity, S0, strike, sigma, r);
          return wc.getPrice();
        }
      }
    }
    
  4. Copy the native dlls (from the native project and libpnl) to the bin directory of the WCF project. It is necessary to be able to specify to the web service that additional dlls are available in this folder. There are several ways to accomplish this, we will construct our own ServiceHostFactory and specify the additional paths in the body of the ExecuteRuntime method.
    1. Replace the code in Service1.svc (nb: it may be necessary to edit the file from Windows Explorer) by:
      <%@ ServiceHost Language="C#" Debug="true" Service="MyWcfService.Service1" CodeBehind="Service1.svc.cs" Factory="MyWcfService.MyServiceHostFactory" %>
      
    2. Add the reference System.ServiceModel.Activation to the project
    3. Add a new class to the project, with the following code:
      using System.ServiceModel.Activation;
      using System.ServiceModel;
      namespace MyWcfService
      {
        public class MyServiceHostFactory : ServiceHostFactory
        {
          protected override System.ServiceModel.ServiceHost CreateServiceHost(Type serviceType, Uri[] baseAddresses)
          {
            return new MyServiceHost(serviceType, baseAddresses);
          }
        }
      
        public class MyServiceHost : ServiceHost
        {
          public MyServiceHost(Type serviceType, Uri[] baseAddresses) : base(serviceType, baseAddresses) { }
      
          protected override void InitializeRuntime()
          {
            base.InitializeRuntime();
            String _path = String.Concat(System.Environment.GetEnvironmentVariable("PATH"), ";", System.AppDomain.CurrentDomain.RelativeSearchPath);
            System.Environment.SetEnvironmentVariable("PATH", _path, EnvironmentVariableTarget.Process);
          }
        }
      }
      

      This code permits to update the PATH variable when the runtime is initialized.

  5. Build everything. It is possible to make sure the web service is correctly deployed and has access to all the dlls by right-clicking on the project to view it in the browser:
    View in Browser

Creating the client

We now create a very simple client that will invoke the web service.

  1. Add a new project "Console application" to the solution and set it as the startup project.
  2. Add a service reference to the project. The created web service should be found when clicking on discover.
    Add a service reference Discover the web service
  3. Replace the code in Program.cs by this one:
    namespace MyConsoleApplication
    {
      class Program
      {
        static void Main(string[] args)
        {
          ServiceReference1.Service1Client cl = new ServiceReference1.Service1Client();
          double result = cl.GetPrice(100, 1, 25, 25, 0.25, 0.002);
        }
      }
    }
    
  4. Compile and run.

Nb: if the web service is modified, it is necessary to reload the service reference so that the modifications can be taken into account.

From a REST Api (using ASP.NET Web Api 2 and Swagger)

  1. Create an empty ASP.NET web application, called RestComputation in this example, and select Web Api features.
    Empty project with basic Web Api characteristics
  2. In the App_Start folder, change the WebApiConfig file so that it looks like the one below:
    public static class WebApiConfig
        {
            public static void Register(HttpConfiguration config)
            {
                config.MapHttpAttributeRoutes();
            }
        }
    
  3. Select the Build tabl of the project Properties, and enable the XML documentation output. If the name of the project is RestComputation, the generated xml file should be RestComputation.xml.
    Generating xml comments
  4. Add the Swashbuckle and Swashbuckle.Examples Nuget packages to the project.
  5. Edit the SwaggerConfig.cs file located in the App_Data folder, and below the c.SingleApiVersion line add the line
    c.IncludeXmlComments(xmlCommentsPath);
    
  6. At the top of the SwaggerConfig.cs file (but still within the class), add the following private field (assuming the name of the project is RestComputation):
    private static string xmlCommentsPath = $@"{AppDomain.CurrentDomain.BaseDirectory}\bin\RestComputation.xml";
    
  7. Add a Models folder to the project, and a PricingParameters.cs class inside the folder; change the source code in this class so that to the listing below:
    public class PricingParameters : IExamplesProvider
        {
            /// <summary>
            /// Gets or sets the call option strike.
            /// </summary>
            /// <value>
            /// The strike.
            /// </value>
            public double Strike { get; set; }
            /// <summary>
            /// Gets or sets the call option maturity.
            /// </summary>
            /// <value>
            /// The maturity.
            /// </value>
            public double Maturity { get; set; }
            /// <summary>
            /// Gets or sets the Monte Carlo sample number.
            /// </summary>
            /// <value>
            /// The sample number.
            /// </value>
            public int SampleNumber { get; set; }
            /// <summary>
            /// Gets or sets the initial spot for the underlying asset.
            /// </summary>
            /// <value>
            /// The initial spot.
            /// </value>
            public double InitialSpot { get; set; }
            /// <summary>
            /// Gets or sets the volatility of the asset.
            /// </summary>
            /// <value>
            /// The volatility.
            /// </value>
            public double Volatility { get; set; }
            /// <summary>
            /// Gets or sets the risk free rate.
            /// </summary>
            /// <value>
            /// The risk free rate.
            /// </value>
            public double RiskFreeRate { get; set; }
    
            /// <summary>
            /// Generates examples of <see cref="PricingParameters"/> objects, for Swagger.
            /// </summary>
            /// <returns></returns>
            public object GetExamples()
            {
                return new PricingParameters() { Strike = 10, Maturity = 1, SampleNumber = 50000, InitialSpot = 10, Volatility = 0.2, RiskFreeRate = 0.01 };
            }
        }
    
  8. Add a ComputationsController class within the Controllers folder, and replace the code in the class by the listing below:
        /// <summary>
        /// Controller for the Monte Carlo computation of the price of call options.
        /// </summary>
        /// <seealso cref="System.Web.Http.ApiController" />
        [RoutePrefix("")]
        public class ComputationController : ApiController
        {
            [HttpGet]
            [Route("")]
            public IHttpActionResult About()
            {
                return Ok("Simple REST api for estimating call prices by Monte Carlo sampling. Endpoint: computation; HTTP method: POST");
            }
    
            /// <summary>
            /// Computes the price of the call option specified by the parameters of the <see cref="PricingParameters"/> object.
            /// </summary>
            /// <param name="parameters">The call option parameters.</param>
            /// <returns>The price of the call option.</returns>
            [HttpPost]
            [Route("computation")]
            [SwaggerRequestExample(typeof(PricingParameters), typeof(PricingParameters))]
            public IHttpActionResult Compute(PricingParameters parameters)
            {
                WrapperClass wc = new WrapperClass();
                wc.getPriceCallEuro(parameters.SampleNumber, parameters.Maturity, parameters.InitialSpot, 
                    parameters.Strike, parameters.Volatility, parameters.RiskFreeRate);
                return Ok(wc.getPrice().ToString());
            }
        }
    
  9. Change the Global.asax page so that it looks like the one below:
        public class WebApiApplication : System.Web.HttpApplication
        {
            protected void Application_Start()
            {
                String _path = String.Concat(System.Environment.GetEnvironmentVariable("PATH"), ";", System.AppDomain.CurrentDomain.RelativeSearchPath);
                System.Environment.SetEnvironmentVariable("PATH", _path, EnvironmentVariableTarget.Process);
                GlobalConfiguration.Configure(WebApiConfig.Register);           
            }
        }
    
  10. Make sure all required .dll and .lib files are located in the bin folder, compile and run the project. By navigating to the swagger endpoint (e.g. http://localhost:<portnumber>/swagger), it should be possible to try out the Monte Carlo engine on the call option provided as an example in the PricingParameters class.