How to Call C# Code From C++

  1. Use C++/Cli as an Intermediate DLL to Call C# Code From C++
  2. Use the Reverse P/Invoke Technology to Call C# Code From C++
  3. Use the COM System to Call C# Code From C++
  4. Use the ICLRRuntimeHost::ExecuteInDefaultAppDomain() CLR Hosting to Call C# Code From C++
  5. Use IPC - Interprocess Communication to Call C# Code From C++
  6. Host an HTTP Server to Call C# Code From C++
How to Call C# Code From C++

C++ offers several ways to invoke C# functions to offer programmers a perfect cross-platform solution. This tutorial will teach you every possible approach to calling C# code from C++.

You can use C++/CLI as an intermediate DLL, reverse P/Invoke, use COM, CLR Hosting, interprocess communication (IPC), or host an HTTP server and invoke via HTTP verbs to call C# code/functions from C++. CLR hosting is a great way to do so, but it limits the programmer to invoke only methods having the int method(string arg) format.

Platform invocation, C++ interop, COM interop, and Embedded interop types are some of the mechanisms for achieving interoperability. Programmers must acknowledge that C#/C++ interop code can be quite difficult to debug, especially across the boundary between unmanaged/managed code.

Use C++/Cli as an Intermediate DLL to Call C# Code From C++

Generally, you can either marshal a delegate as a function pointer and pass it to the unmanaged side, which requires the managed side to begin the interaction or use C++/CLI as the mediating layer which is a better way to achieve optimal performance while calling C# code from C++.

Heterogeneity is the rule, not the exception, when it comes to software development in a professional environment, as you need to interact with systems developed with other technologies, and native C++ can require retrieving data using the object-oriented API of another system designed in .NET with C#.

The C++/CLI platform holds the unique ability to mix managed (C#) code and native (C++) code in one place and works as an ideal tool for building bridges between these two languages using simple wrappers.

It is a straightforward approach that remains a tedious and error-prone process, except the complexity can occur when plumbing code is necessary for converting to and from managed types.

The C# Library:

using System;  // contains the definition of random class

namespace _stockManager {
  public struct _recruite {
    public double _tactics { get; set; }
    public double _mind { get; set; }
    public double _agility { get; set; }
    public double _experience { get; set; }
  }

  public class API {
    private Random _random = new Random();

    public _recruite _manager(string symbol) {
      double mind = _random.Next(95, 105);

      return new _recruite { _tactics = mind - 0.1, _mind = mind, _agility = mind + 0.1,
                             _experience = mind * 10e6 };
    }
  }
}

The C++/CLI Wrapper (Header File):

class stockmanager_APIWrapper_private;

struct __declspec(dllexport) _recruite {
  double tactics;
  double mind;
  double agility;
  double experience;
};

class __declspec(dllexport) APIWrapper_stockmanager {
 private:
  stockmanager_APIWrapper_private* _private;

 public:
  APIWrapper_stockmanager();

 public:
  ~APIWrapper_stockmanager();

 public:
  const _recruite _manager(const char* symbol);
};

C++ Definition File:

#using "StockMarketAPI.dll"

#include <msclr\auto_gcroot.h>

#include "APIWrapper_stockmanager.h"

class stockmanager_APIWrapper_private {
 public:
  msclr::auto_gcroot<StockMarket::API ^> API;
};

APIWrapper_stockmanager::APIWrapper_stockmanager() {
  _private = new stockmanager_APIWrapper_private();
  _private->API = gcnew _stockManager::API();
}

const _recruite APIWrapper_stockmanager::_manager(const char* symbol) {
  _stockManager::_recruite managedQuote =
      _private->API->_manager(gcnew System::String(symbol));

  _recruite nativeQuote;
  nativeQuote.tactics = managedQuote._tactics;
  nativeQuote.mind = managedQuote._mind;
  nativeQuote.agility = managedQuote._agility;
  nativeQuote.experience = managedQuote._experience;

  return nativeQuote;
}

APIWrapper_stockmanager::~APIWrapper_stockmanager() { delete _private; }

The Native C++ Application:

#include <iostream>

#include "APIWrapper_stockmanager.h"

int main() {
  const char* stock = "GOOG";

  APIWrapper_stockmanager API;

  _recruite get_recruite = API._manager(stock);

  std::cout << "Tactics: " << get_recruite.tactics << std::endl;
  std::cout << "Mind: " << get_recruite.mind << std::endl;
  std::cout << "Agility: " << get_recruite.agility << std::endl;
  std::cout << "Experience: " << get_recruite.experience << std::endl;
}

Output:

test.exe
Tactics: 103.9
Mind: 104
Agility: 104.1
Experience: 1.04e+09

Use the Reverse P/Invoke Technology to Call C# Code From C++

It allows C++ programmers to access structs, callbacks, and functions in managed libraries from your unmanaged code or vice versa. In C#, the System and System.Runtime.InteropServices namespaces contain the P/Invoke APIs and allow you to communicate with the native components.

You can perform both, either writing C# or C++ code and the runtime allows this communication to flow in both directions. It enables you to call back into managed code from native functions by using function pointers (delegate - the closest thing to function pointers) to allow callbacks from native code into managed code.

In this method, you define a delegate for a live callback that matches the signature and pass that into the external method, and the runtime will take care of everything. It’s important to review the signatures of the unmanaged functions before executing the C++ code containing C# functions; for example, the BOOL EnumWindows (WNDENUMPROC lpEnumFunc, LPARAM lParam); is a function call to enumerate all of the windows and the BOOL CALLBACK EnumWindowsProc (HWND hwnd, LPARAM lParam); is a callback signature.

using System.Runtime.InteropServices;

public class _synch_doc {
  public delegate void callback_synch(string str);

  public static void doc_call(string str) {
    System.Console.WriteLine("Managed: " + str);
  }

  public static int Main() {
    caller("Hello World!", 10, new callback_synch(_synch_doc.doc_call));
    return 0;
  }

  [DllImport("nat.dll", CallingConvention = CallingConvention.StdCall)]
  public static extern void caller(string str, int _algebra, callback_synch call);
}
#include <stdio.h>
#include <string.h>

typedef void(__stdcall *callback_synch)(wchar_t *str);
extern "C" __declspec(dllexport) void __stdcall caller(wchar_t *input,
                                                       int _algebra,
                                                       callback_synch call) {
  for (int x = 0; x < _algebra; x++) {
    call(input);
  }
}

Output:

Hello World!

Use the COM System to Call C# Code From C++

The Component Object Model - COM (as a platform-independent, object-oriented, and distributed system for creating interactable binary software components) can help call the C# function when .NET components are exposed to it. A programmer can perform using COM by writing a .NET type and consuming that type from the unmanaged code.

There are some pre-requisites known to be important before using COM, and these include; all the managed types, properties, events, methods, and fields going to expose to COM must be public, custom attributes within managed code must be capable of enhancing the interoperability of a component, and the developer must summarize the steps involved in referencing and deploying the assemblies.

Furthermore, register assemblies, reference .NET types, and call a .NET object with COM to consume a managed type from COM, as well as a strong-named assembly that can be installed in the global assembly cache and requires a signature from its publisher.

The following example will demonstrate the interoperation of a COM Client and a .NET Server that performs mortgage calculations. The client will create and call an instance of a managed class and passes four arguments to the instance to display computations.

COM Client:

// ConLoan.cpp : Defines the entry point for the console application.
#import "..\LoanLib\_azythro_call.tlb" raw_interfaces_only
#include "stdafx.h"
using namespace _azythro_call;

int main(int argc, char* argv[]) {
  HRESULT hr = CoInitialize(NULL);

  ILoanPtr pILoan(__uuidof(_azythro));

  if (argc < 5) {
    printf("Usage: ConLoan Balance Rate Term Payment\n");
    printf("    Either Balance, Rate, Term, or Payment must be 0\n");
    return -1;
  }

  double acc_initial_balance = atof(argv[1]);
  double acc_balance_rate = atof(argv[2]) / 100.0;
  short acc_payment_term = atoi(argv[3]);
  double acc_bal_payment = atof(argv[4]);

  pILoan->put_OpeningBalance(acc_initial_balance);
  pILoan->put_Rate(acc_balance_rate);
  pILoan->put_Term(acc_payment_term);
  pILoan->put_Payment(acc_bal_payment);

  if (acc_initial_balance == 0.00)
    pILoan->calculate_balance_opening(&acc_initial_balance);
  if (acc_balance_rate == 0.00) pILoan->calculate_rate(&acc_balance_rate);
  if (acc_payment_term == 0) pILoan->calculate_term(&acc_payment_term);
  if (acc_bal_payment == 0.00) pILoan->calculate_payment(&acc_bal_payment);

  printf("Balance = %.2f\n", acc_initial_balance);
  printf("Rate    = %.1f%%\n", acc_balance_rate * 100);
  printf("Term    = %.2i\n", acc_payment_term);
  printf("Payment = %.2f\n", acc_bal_payment);

  VARIANT_BOOL extra_PMTs;
  double user_acc_balance = 0.0;
  double Principal = 0.0;
  double user_acc_interest = 0.0;

  printf("%4s%10s%12s%10s%12s\n", "Nbr", "Payment", "Principal", "Interest",
         "Balance");
  printf("%4s%10s%12s%10s%12s\n", "---", "-------", "---------", "--------",
         "-------");

  pILoan->get_1stPMT_dist(acc_bal_payment, &user_acc_balance, &acc_principal,
                          &user_acc_interest, &extra_PMTs);

  for (short PmtNbr = 1; extra_PMTs; PmtNbr++) {
    printf("%4i%10.2f%12.2f%10.2f%12.2f\n", PmtNbr, acc_bal_payment,
           acc_principal, user_acc_interest, user_acc_balance);

    pILoan->get_nextPMT_dist(acc_bal_payment, &user_acc_balance, &acc_principal,
                             &user_acc_interest, &extra_PMTs);
  }

  CoUninitialize();
  return 0;
}

.NET Server:

using System;
using System.Reflection;

[assembly:AssemblyKeyFile("sample.snk")]
namespace _azythro_call {

  public interface loan_call {
    double balance_opening { get; set; }
    double balance_rate { get; set; }
    double user_payment { get; set; }
    short payment_term { get; set; }
    String per_risk_rate { get; set; }

    double calculate_payment();
    double calculate_balance_opening();
    double calculate_rate();
    short calculate_term();
    bool get_1stPMT_dist(double cal_PMT_AMT, ref double user_acc_balance, out double acc_principal,
                         out double user_acc_interest);
    bool get_nextPMT_dist(double cal_PMT_AMT, ref double user_acc_balance, out double acc_principal,
                          out double user_acc_interest);
  }

  public class _azythro : loan_call {
    private double acc_initial_balance;
    private double acc_balance_rate;
    private double acc_bal_payment;
    private short acc_payment_term;
    private String rating_calculatedRisk;

    public double balance_opening {
      get { return acc_initial_balance; }
      set { acc_initial_balance = value; }
    }

    public double balance_rate {
      get { return acc_balance_rate; }
      set { acc_balance_rate = value; }
    }

    public double user_payment {
      get { return acc_bal_payment; }
      set { acc_bal_payment = value; }
    }

    public short payment_term {
      get { return acc_payment_term; }
      set { acc_payment_term = value; }
    }

    public String per_risk_rate {
      get { return rating_calculatedRisk; }
      set { rating_calculatedRisk = value; }
    }

    public double calculate_payment() {
      user_payment = Util.Round(
          balance_opening * (balance_rate / (1  Math.Pow((1 + balance_rate), -payment_term))), 2);
      return user_payment;
    }

    public double calculate_balance_opening() {
      balance_opening = Util.Round(
          user_payment / (balance_rate / (1 - Math.Pow((1 + balance_rate), -payment_term))), 2);
      return balance_opening;
    }

    public double calculate_rate() {
      double acc_payment_calculated = user_payment;

      for (balance_rate = 0.001; balance_rate < 28.0; balance_rate += 0.001) {
        user_payment = Util.Round(
            balance_opening * (balance_rate / (1  Math.Pow((1 + balance_rate), -payment_term))),
            2);

        if (user_payment >= acc_payment_calculated)
          break;
      }
      return balance_rate;
    }

    public short calculate_term() {
      double acc_payment_calculated = user_payment;

      for (payment_term = 1; payment_term < 480; payment_term++) {
        user_payment = Util.Round(
            balance_opening * (balance_rate / (1  Math.Pow((1 + balance_rate), -payment_term))),
            2);

        if (user_payment <= acc_payment_calculated)
          break;
      }

      return payment_term;
    }

    public bool get_1stPMT_dist(double cal_PMT_AMT, ref double user_acc_balance,
                                out double acc_principal, out double user_acc_interest) {
      user_acc_balance = balance_opening;
      return get_nextPMT_dist(cal_PMT_AMT, ref user_acc_balance, out acc_principal,
                              out user_acc_interest);
    }

    public bool get_nextPMT_dist(double cal_PMT_AMT, ref double user_acc_balance,
                                 out double acc_principal, out double user_acc_interest) {
      user_acc_interest = Util.Round(user_acc_balance * balance_rate, 2);
      acc_principal = Util.Round(cal_PMT_AMT - user_acc_interest, 2);
      user_acc_balance = Util.Round(user_acc_balance - acc_principal, 2);

      if (user_acc_balance <= 0.0)
        return false;

      return true;
    }
  }

  internal class Util {
    public static double Round(double value, short digits) {
      double factor = Math.Pow(10, digits);
      return Math.Round(value * factor) / factor;
    }
  }
}

Output:

[load] sample.snk

Balance: 300000
Rate: 24000
Term: 1200
Payment: 175000

Use the ICLRRuntimeHost::ExecuteInDefaultAppDomain() CLR Hosting to Call C# Code From C++

The ICLRRuntimeHost::ExecuteInDefaultAppDomain method calls the specified method of the specified type in the specified managed assembly. Its syntax is something like HRESULT ExecuteInDefaultAppDomain ([in] LPCWSTR pwzAssemblyPath, [in] LPCWSTR pwzTypeName, [in] LPCWSTR pwzMethodName, [in] LPCWSTR pwzArgument, [out] DWORD *pReturnValue ); where each parameter respectively represents the path, type, name, string parameter to pass the method, and the integer value returned by the invoked method.

Furthermore, the invoked method must have the static int pwzMethodName (String pwzArgument) signature where pwzArgument represents a string value passed as a parameter to that invoke method. In general, you can embed any CLR assembly in a native C++ program using the CLR Hosting, and it’s the reason why native programs like SQL Server support .NET code extensions like SQL CLR and can be loaded into the massive processes using CorBindToRuntimeEx() or CLRCreateInstance() depends on the .NET framework version 2.0 or 4 respectively.

SharedMemory.h:

#pragma const_onetime_impl

#ifdef SHAREDMEM_EXPORTS
#define SHAREDMEM_API __declspec(export_dll)
#else
#define SHAREDMEM_API __declspec(import_dll)
#endif

#define conv_call_sharedmemory __cdecl

extern "C" {
SHAREDMEM_API BOOL conv_call_sharedmemory SetSharedMem(ULONGLONG _64bitValue);
SHAREDMEM_API BOOL conv_call_sharedmemory GetSharedMem(ULONGLONG* p64bitValue);
}

SharedMemory.cpp:

#include "SharedMemory.h"
#include "custom_made.h"  // a user created/defined library | customizable according to your preference

HANDLE object_mapfile_handling = NULL;  // an object to handle mapped file
LPVOID ponter_sharedMemory =
    NULL;  // a pointer to the shared memory (SharedMemory)
const int size_type_sharedMemory =
    sizeof(ULONGLONG);  // the size type of shared memory

BOOL sharedMemory_init_create() {
  // named mapped_file object creation
  object_mapfile_handling = CreateFileMapping(
      INVALID_HANDLE_VALUE, NULL, PAGE_READWRITE, 0, size_type_sharedMemory,
      TEXT("file_sharedMemory")  // initialize the name of shared memory file
  );

  if (object_mapfile_handling == NULL) {
    return FALSE;
  }

  BOOL memory_access_information = (ERROR_ALREADY_EXISTS != GetLastError());

  // access / get a pointer to the shared memory
  ponter_sharedMemory =
      MapViewOfFile(object_mapfile_handling, FILE_MAP_WRITE, 0, 0, 0);

  if (ponter_sharedMemory == NULL) {
    return FALSE;
  }

  if (memory_access_information)  // First time the shared memory is accessed?
  {
    ZeroMemory(ponter_sharedMemory, size_type_sharedMemory);
  }

  return TRUE;
}

BOOL set_type_sharedMemory(ULONGLONG _64bitValue) {
  BOOL check_set = sharedMemory_init_create();

  if (check_set) {
    ULONGLONG* new_sharedMemory_pointer = (ULONGLONG*)ponter_sharedMemory;
    *new_sharedMemory_pointer = _64bitValue;
  }

  return check_set;
}

BOOL get_sharedMemory(ULONGLONG* p64bitValue) {
  if (p64bitValue == NULL) return FALSE;

  BOOL check_get = sharedMemory_init_create();

  if (check_get) {
    ULONGLONG* new_sharedMemory_pointer = (ULONGLONG*)ponter_sharedMemory;
    *p64bitValue = *new_sharedMemory_pointer;
  }

  return check_get;
}
namespace shared_memory_csharp {
  delegate void _void_init();

  static public class command_line_interface {
    [import_dll("SharedMemory.dll")]
    static extern bool set_type_sharedMemory(Int64 value);

    static GCHandle delegate_cust_handle;

    public static int prog_point_entry(string ignored) {
      IntPtr primary_function = IntPtr.Zero;
      Delegate new_function_delegate = new VoidDelegate(_custom_method);
      delegate_cust_handle = GCHandle.Alloc(new_function_delegate);
      primary_function = Marshal.GetFunctionPointerForDelegate(new_function_delegate);
      bool bool_set_ok = set_type_sharedMemory(primary_function.ToInt64());
      return bool_set_ok ? 1 : 0;
    }

    public static void primary_method() {
      MessageBox.Show("Hello from C# primary_method!");
      delegate_cust_handle.Free();
    }
  }
}

main.cpp:

#include "SharedMemory.h"
typedef void (*VOID_FUNC_PTR)();

void execute_charp_code() {
  ICLRRuntimeHost *pointer_CLR_host = NULL;
  HRESULT cor_bind_hr =
      CorBindToRuntimeEx(NULL, L"wks", 0, CLSID_CLRRuntimeHost,
                         IID_ICLRRuntimeHost, (PVOID *)&pointer_CLR_host);

  HRESULT process_start_hr = pointer_CLR_host->Start();

  DWORD retVal;
  HRESULT process_execute_hr = pointer_CLR_host->ExecuteInDefaultAppDomain(
      szPathToAssembly, L"shared_memory_csharp.command_line_interface",
      L"prog_point_entry", L"",
      &retVal  // returns `1` for successful execution | returns `0` for failed
               // execution
  );

  if (process_execute_hr == S_OK && retVal == 1) {
    ULONGLONG new_shared_memory_value = 0;
    BOOL bool_shMem_value = get_sharedMemory(&new_shared_memory_value);
    if (bool_shMem_value) {
      VOID_FUNC_PTR function_csharp = (VOID_FUNC_PTR)new_shared_memory_value;
      function_csharp();
    }
  }
}

Output:

Hello from C# primary_method!

Use IPC - Interprocess Communication to Call C# Code From C++

The world of interprocess communication holds many methods to do this task, including; named pipes, RPC, shared memory, etc. In a nutshell, the IPC runs your C++ program inside a process to activate its C# code.

The processes from IPC can communicate with each other and invoke each other’s methods or use each other’s data. During execution, it creates two processes, each holding code from different technology.

In the case of C# and C++, it will create two processes and create a file-sharable connection. Furthermore, you can use sockets on the local machine to connect processes for IPC using named pipes or make duplex communication between a pipe server and one or more pipe clients with a stream that can be used to send/receive messages between processes.

The named pipes have FIFO (First in - First out) behavior, and you will find two in the following example.

using System;       // essential
using System.Text;  // essential

namespace csharp_namedpipes {
  class namedpipe_server {
    static void Main(string[] args) {
      NamedPipeServer pipe_server_one = new NamedPipeServer(@"\\.\pipe\myNamedPipe1", 0);
      NamedPipeServer pipe_server_two = new NamedPipeServer(@"\\.\pipe\myNamedPipe2", 1);

      pipe_server_one.Start();
      pipe_server_two.Start();

      string _message = "Start";
      do {
        Console.WriteLine("Enter the message: ");
        _message = Console.ReadLine();
        pipe_server_two.SendMessage(_message, pipe_server_two.clientse);
      } while (_message != "Quit");

      pipe_server_one.StopServer();
      pipe_server_two.StopServer();
    }
  }
}
void execute_csharp_code() {
  LPTSTR lpsz_pipe_name1 = TEXT("\\\\.\\pipe\\myNamedPipe1");
  LPTSTR lpsz_pipe_name2 = TEXT("\\\\.\\pipe\\myNamedPipe2");
  do {
    printf("Enter your message: ");
    scanf("%s", buf);
    if (strcmp(buf, "Quit") == 0)
      Write_St = FALSE;
    else {
      WriteFile(hPipe1, buf, dwBytesToWrite, &cbWritten, NULL);
      memset(buf, 0xCC, 100);
    }
  } while (Write_St);
}

Output:

[C# app]

Enter your message: this is a test
C++ App: Received 17 bytes: test sending back

[C++ app]

Enter the message:
C# App: Received 5 Bytes: this
C# App: Received 2 bytes: a
test sending back
Enter the message:

Host an HTTP Server to Call C# Code From C++

The curlpp enables C++ programmers to request HTTP by hosting an HTTP server and invoking via HTTP verbs like a REST-style API. In C++, curlpp is natural and helps get the content of an URL and access the C# code.

On the other hand, POCO is another way to host an HTTP server, and unlike curlpp, it has rich documentation and maintained support HTTPs. As the POCO is free and open source, it can be a great start for beginners, and in the following examples, you will learn how to implement these approaches.

#include <string.h>
#include <windows.h>
#include <winsock2.h>

#include <iostream>
#include <locale>
#include <sstream>
#include <vector>

using namespace std;

#pragma comment(lib, "ws2_32.lib")

int main(void) {
  WSADATA data_wsa;
  SOCKET server_socket;
  SOCKADDR_IN server_socket_add;
  int count_line = 0;
  int count_row = 0;
  struct hostent *server_host;
  locale host_local;
  char var_buffer[10000];
  int i = 0;
  int data_length_new;
  string get_website_HTML;

  // website URL
  string website_URL = "www.google.com";

  // HTTP GET
  string get_website_HTTP = "GET / HTTP/1.1\r\nHost: " + website_URL +
                            "\r\nConnection: close\r\n\r\n";

  if (WSAStartup(MAKEWORD(2, 2), &data_wsa) != 0) {
    cout << "WSAStartup failed.\n";
    system("pause");
    // return 1;
  }

  server_socket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
  server_host = gethostbyname(website_URL.c_str());

  server_socket_add.sin_port = htons(80);
  server_socket_add.sin_family = AF_INET;
  server_socket_add.sin_addr.s_addr = *((unsigned long *)server_host->h_addr);

  if (connect(server_socket, (SOCKADDR *)(&server_socket_add),
              sizeof(server_socket_add)) != 0) {
    cout << "Could not connect";
    system("pause");
    // return 1;
  }

  // send GET / HTTP
  send(server_socket, get_website_HTTP.c_str(),
       strlen(get_website_HTTP.c_str()), 0);

  // receive HTML
  while ((data_length_new = recv(server_socket, var_buffer, 10000, 0)) > 0) {
    int x = 0;
    while (var_buffer[x] >= 32 || var_buffer[x] == '\n' ||
           var_buffer[x] == '\r') {
      get_website_HTML += var_buffer[x];
      x += 1;
    }
  }

  closesocket(server_socket);
  WSACleanup();

  // Display HTML source
  cout << get_website_HTML;
  cout << "source displayed!" << endl;

  // pause
  cout << "\n\nPress ANY key to close.\n\n";
  cin.ignore();
  cin.get();

  return 0;
}

Output:

source displayed!
Press ANY key to close.

One characteristic of the .NET languages is that they make use of the managed code where CLR takes responsibility for managing the memory and resources of the program by performing garbage collection, controlling the lifetime of objects, enhancing debugging functionality, and many more.

The runtime systems know little about the memory and resources used by the program in unmanaged code like that of C++ and can provide minimal services; the hard part to calling C# code with C++ is knowing how to go about moving data across the boundary between managed and unmanaged code as this process is known as marshaling.

Syed Hassan Sabeeh Kazmi avatar Syed Hassan Sabeeh Kazmi avatar

Hassan is a Software Engineer with a well-developed set of programming skills. He uses his knowledge and writing capabilities to produce interesting-to-read technical articles.

GitHub