Click here to Skip to main content
Click here to Skip to main content

Using C# from native C++ with the help of C++/CLI

, 4 Apr 2014
Rate this:
Please Sign up or sign in to vote.
Using C# from native C++ with the help of C++/CLI.

Introduction

The up-to-date article is there: http://pragmateek.com/using-c-from-native-c-with-the-help-of-ccli-v2/[^] You should refer to it if you want to copy/paste some code. If you only want to have a general idea of the topic, then you can skim through this one.

When it comes to software development in a professional environment, heterogeneity is the rule, not the exception: you often need to interact with systems developed with other technologies.

I’ve been recently faced with such a situation: a team that uses only native C++ needed to retrieve data using the object-oriented API of another team that develops only in .NET with C#. This is a relatively uncommon scenario (just look at the number of articles on the subject), the standard case being new systems based on the .NET platform, developed in C# or VB.NET, needing to interact with legacy systems developed in native C++.

I’ve used the C++/CLI platform due to its unique ability to mix managed (.NET) and native code in one place and is then the ideal tool for building bridges between these two worlds using simple wrappers: the native face of the wrapper can be consumed by the legacy components and its managed face can directly use the C# API.

In this article, I’ll illustrate how I’ve tackled the issue by building a simple C++/CLI wrapper, using a similar use-case: market-data retrieval from Yahoo.

(All the source code of this article is available in this ZIP archive.)

The C# Library

Here is a simple C# class that retrieves financial data using the Yahoo finance API:

using System.Net; // WebClient
using System.Globalization; // CultureInfo

public class YahooAPI
{
    private static readonly WebClient webClient = new WebClient();

    private const string UrlTemplate = "http://finance.yahoo.com/d/quotes.csv?s={0}&f={1}";

    private static double ParseDouble(string value)
    {
         return double.Parse(value.Trim(), CultureInfo.InvariantCulture);
    }

    private static string[] GetDataFromYahoo(string symbol, string fields)
    {
        string request = string.Format(UrlTemplate, symbol, fields);

        string rawData = webClient.DownloadString(request).Trim();

        return rawData.Split(',');
    }

    public double GetBid(string symbol)
    {
        return ParseDouble(GetDataFromYahoo(symbol, "b3")[0]);
    }

    public double GetAsk(string symbol)
    {
        return ParseDouble(GetDataFromYahoo(symbol, "b2")[0]);
    }

    public string GetCapitalization(string symbol)
    {
        return GetDataFromYahoo(symbol, "j1")[0];
    }

    public string[] GetValues(string symbol, string fields)
    {
        return GetDataFromYahoo(symbol, fields);
    }
}

Nothing to notice that has to do with our problematic, this is just plain-vanilla C#.

We compile it to obtain our “YahooAPI.dll” managed DLL:

csc /target:library YahooAPI.cs
Microsoft (R) Visual C# Compiler version 4.0.30319.17929
for Microsoft (R) .NET Framework 4.5
Copyright (C) Microsoft Corporation. All rights reserved.

The C++/CLI Wrapper

Source Code

Here comes the interesting part, our C++/CLI wrapper that uses the C# library:

#using "YahooAPI.dll"

#include <msclr\auto_gcroot.h>

using namespace System::Runtime::InteropServices; // Marshal

class YahooAPIWrapperPrivate
{
    public: msclr::auto_gcroot<YahooAPI^> yahooAPI;
};

class __declspec(dllexport) YahooAPIWrapper
{
    private: YahooAPIWrapperPrivate* _private;

    public: YahooAPIWrapper()
    {
        _private = new YahooAPIWrapperPrivate();
        _private->yahooAPI = gcnew YahooAPI();
    }

    public: double GetBid(const char* symbol)
    {
        return _private->yahooAPI->GetBid(gcnew System::String(symbol));
    }

    public: double GetAsk(const char* symbol)
    {
        return _private->yahooAPI->GetAsk(gcnew System::String(symbol));
    }

    public: const char* GetCapitalization(const char* symbol)
    {
        System::String^ managedCapi = _private->yahooAPI->GetCapitalization(gcnew System::String(symbol));

        return (const char*)Marshal::StringToHGlobalAnsi(managedCapi).ToPointer();
    }

    public: const char** GetValues(const char* symbol, const char* fields)
    {
        cli::array<System::String^>^ managedValues = _private->yahooAPI->GetValues
        (gcnew System::String(symbol), gcnew System::String(fields));

        const char** unmanagedValues = new const char*[managedValues->Length];

        for (int i = 0; i < managedValues->Length; ++i)
        {
            unmanagedValues[i] = (const char*)Marshal::StringToHGlobalAnsi(managedValues[i]).ToPointer();
        }

        return unmanagedValues;
    }

    public: ~YahooAPIWrapper()
    {
        delete _private;
    }
};

Some explanations:

  • The hats “^ represent managed references, i.e. they point to managed objects (like System::String) allocated on the managed heap; they are to managed objects what native pointers are to native objects allocated on the native heap.
  • The “gcnew” operator is used for allocating objects on the managed heap, whereas the new operator allocates only on the native heap.
  • cli::array is the C++/CLI representation of a managed array.
  • auto_gcroot” is a wrapper around a managed reference: you can’t directly embed a managed reference inside a native type especially because the way memory is handled in the native and managed worlds is quite different; moreover, compared to gcroot, which too avoid explicit management like pinning, auto_gcroot is automatically disposed when going out of scope.
  • The “StringToHGlobalAnsi” method converts a managed “System::String” which is made of UTF-16 chars to an array of ANSI chars it allocates on the native heap; it returns a pointer to this array as an IntPtr which is a managed wrapper around a native pointer that we obtain with the ToPointer method as a void*.
  • __declspec(dllexport) asks the compiler to publicly export the whole interface of the class and to generate a .lib file we’ll use to link the native C++ program with the C++/CLI DLL.
  • Moreover, you may wonder why we use const char* instead of std::string: because publicly exposing STL types is brittle as their implementation could differ from vendor to vendor and even between different versions from the same vendor.

Well, after all, there is quite a bunch of things to explain. :) But if you use C++/CLI on a regular basis, you’ll quickly become familiar with this at first cryptic stuff.

Design Rationales

You may have one more question: why the gcroot field has been isolated in its own structure? First, you should know that in native C++ you must declare all the members of a type, including the private part. This may seem strange to C# programmers, because in C# the private part is hidden; but for native C++, header files are more than a simple description of the interface of the types, they describe their memory structure too, then all the information must be available to the calling code so that it is able to correctly allocate the memory for the instances of the types, otherwise you’ll get memory corruption (believe me you don’t want to live such a situation ;) ).

But in that case, why not simply add the gcroot field to the class declaration? Because gcroot is pure C++/CLI stuff that has no sense for native C++ so your code won’t even compile; moreover, even if it compiled, this would be an ugly leak of the abstraction we’re trying to build. With this design, we’ve hidden all the C++/CLI stuff inside the “YahooAPIWrapperPrivate” structure so that our wrapper has an interface compatible with native C++. Finally, I’ve later discovered that I’ve only reinvented the wheel: the PIMPL principle used in native C++ development; so it seems like one more compelling argument in favor of this design.

OK for using an additional structure, but why a pointer to the structure instead of an instance? Because as I’ve said, you must fully describe the memory layout of your types, this includes the memory layout of objects inlined into the types, so here the compiler of the native C++ code should have to know the layout of the YahooAPIWrapperPrivate type too, so it should know about … gcroot. We have not this issue with pointers because they all have the same size; indeed the values of pointers are memory addresses: their size depends only on the platform (Intel 8088 (16 bits), x86/IA-32 (32 bits), IA-64 (64 bits)…); then the compiler can allocate a fixed amount of memory per pointer without having to worry about the objects pointed to.

Header File

Here is the header file (that will be consumed by the native C++ code) for the wrapper:

class YahooAPIWrapperPrivate;

class YahooAPIWrapper
{
    private: YahooAPIWrapperPrivate* _private;

    public: YahooAPIWrapper();
 
    public: ~YahooAPIWrapper();
    
    public: double GetBid(const char* symbol);

    public: double GetAsk(const char* symbol);
    
    public: const char* GetCapitalization(const char* symbol);
    
    public: const char** GetValues(const char* symbol, const char* fields);
};

Nothing special except the forward declaration of the “YahooAPIWrapperPrivate” class, needed so that the compiler knows “YahooAPIWrapperPrivate” refers to some class defined somewhere; where it is and what it is is irrelevant for the compiler at this stage, it only needs to know it has to emit code that allocates a memory area whose size is the size of any pointer on this platform.

Compilation

Here is how to compile the C++/CLI wrapper:

cl /clr /LD YahooAPIWrapper.cpp
Microsoft (R) C/C++ Optimizing Compiler Version 16.00.40219.01
for Microsoft (R) .NET Framework version 4.00.30319.18034
Copyright (C) Microsoft Corporation.  All rights reserved.

YahooAPIWrapper.cpp
Microsoft (R) Incremental Linker Version 10.00.40219.01
Copyright (C) Microsoft Corporation.  All rights reserved.

/out:YahooAPIWrapper.dll
/dll
/implib:YahooAPIWrapper.lib
YahooAPIWrapper.obj
   Creating library YahooAPIWrapper.lib and object YahooAPIWrapper.exp
  • /clr triggers the C++/CLI mode, by default CL acts as a native C++ compiler
  • /LD asks CL to generate a DLL instead of an EXE

We now have our “YahooAPIWrapper.dll” DLL ready to be used.

The Native C++ Application

Finally, here is the native C++ application that uses the C# API through the C++/CLI wrapper:

#include <iostream>

#include "YahooAPIWrapper.h"

int main()
{
    const char* stock = "GOOG";
    YahooAPIWrapper yahoo;
    
    double bid = yahoo.GetBid(stock);
    double ask = yahoo.GetAsk(stock);
    const char* capi = yahoo.GetCapitalization(stock);
    
    const char** bidAskCapi = yahoo.GetValues(stock, "b3b2j1");
    
    std::cout << "Bid: " << bid << std::endl;
    std::cout << "Ask: " << ask << std::endl;
    std::cout << "Capi: " << capi << std::endl;
    
    std::cout << "BidAskCapi[0]: " << bidAskCapi[0] << std::endl;
    std::cout << "BidAskCapi[1]: " << bidAskCapi[1] << std::endl;
    std::cout << "BidAskCapi[2]: " << bidAskCapi[2] << std::endl;
}

Compilation is straightforward:

cl test.cpp YahooAPIWrapper.lib
Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
Copyright (C) Microsoft Corporation.  All rights reserved.

test.cpp
C:\Program Files (x86)\Microsoft Visual Studio 10.0\VC\INCLUDE\xlocale(323) : 
warning C4530: C++ exception handler used, but unwind semantics are not enabled. 
Specify /EHsc
Microsoft (R) Incremental Linker Version 10.00.40219.01
Copyright (C) Microsoft Corporation.  All rights reserved.

/out:test.exe
test.obj
YahooAPIWrapper.lib
   Creating library test.lib and object test.exp 

As you can see, from the point of view of the native C++ application, things are transparent (be it the source code or the compilation process), there is no trace of any .NET stuff (except the name of the wrapper I’ve deliberately made explicit), just a plain old native C++ API.

And here are the results:

test.exe
Bid: 821.2
Ask: 822
Capi: 270.8B
BidAskCapi[0]: 821.20
BidAskCapi[1]: 822.00
BidAskCapi[2]: 270.8B

Conclusion

As you’ve seen, using C++/CLI wrappers for native to managed interop is rather straightforward, the only difficulty being the plumbing code necessary for converting to and from managed types. The other way around, that is managed to native interop, is almost identical except that you’ll wrap native objects and that the called side will be managed and the calling side will be native.

Though wrapping with C++/CLI is quite simple, it remains a tedious and error-prone process: imagine if you need to export dozens or more managed classes! You could of course factorize the repetitive code, like conversions, by building helpers but you’ll still need to implement the classes’ structures by-hand, so unless you have few types, you should not go down this path.

Fortunately there is tools, like SWIG, that can automate the process for this kind of scenario by generating the plumbing layer; they will isolate you from a significant part of the low-level details, probably avoiding you some headaches (maybe causing others ;) ), so taking the time to learn them should be worth it on the long term if you have a big project.

If you’ve used this approach in a real-life project too, I’d be really interested in hearing from you, so please let a comment with your feedback. If you have any remark, question or suggestion, feel free to let a comment as well.

Thanks for reading! :)

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

About the Author

Pragmateek
Instructor / Trainer Pragmateek
France (Metropolitan) France (Metropolitan)
To make it short I'm an IT trainer specialized in the .Net ecosystem (framework, C#, WPF, Excel addins...).
(I'm available in France and bordering countries, and I only teach in french.)
 
I like to learn new things, particularly to understand what happens under the hood, and I do my best to share my humble knowledge with others by direct teaching, by posting articles on my blog (pragmateek.com), or by answering questions on forums.
Follow on   Twitter   LinkedIn

Comments and Discussions

 
QuestionEmpty managed object Pinmembertingkengboon20-May-14 15:06 
AnswerRe: Empty managed object PinpremiumPragmateek20-May-14 21:52 
GeneralRe: Empty managed object Pinmembertingkengboon21-May-14 16:43 
GeneralRe: Empty managed object PinpremiumPragmateek21-May-14 21:26 
BugPlease, don't forget to free the memory! PinmemberLukas Wöhrl6-Apr-14 10:47 
GeneralRe: Please, don't forget to free the memory! PinpremiumPragmateek6-Apr-14 11:19 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web02 | 2.8.140721.1 | Last Updated 4 Apr 2014
Article Copyright 2013 by Pragmateek
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid