I created a simple, clean, complete example how to wrap a native C++ DLL in CLI, interface it in C#, and build the whole thing with CMAKE. https://github.com/hepcatjk/CsharpCLICplusplusDLLExample . You build it with CMAKE and Visual Studio (due to the whole CLR thing).
This was in response to this post (Generate C# project using CMake) which provided a way to do it, but did not have a complete answer to test. This is based heavily on (Passing a vector/array from unmanaged C++ to C#) this example and the one from Code Project cited in the Readme.
This might be important to do if you love the performance (or just have legacy code) and wanted to build some DLL to interface with a C# GUI or Unity Game Engine or other plug-in engine. I see a great example you had some matrix code that utilzed Eigen, and wanted to get that in Unity or something else. So you need to mix projects since you don't want to constantly create something every time Microsoft releases a new version of Visual Studio.
So this project is a toy example, but complete in that in moves things from Native Code to Managed CLR code, and tests in both C++ and C#.
My question is : is this the best way to move data like this around? Would you do it another way, if so how would you modify the example?
Is there any interest is more complex type examples?
Native DLL
#pragma once
#include <vector>
#ifdef EXAMPLEUNMANAGEDDLL_EXPORTS
#define EXAMPLEUNMANAGEDDLL_API __declspec(dllexport)
#else
#define EXAMPLEUNMANAGEDDLL_API __declspec(dllimport)
#endif
class EXAMPLEUNMANAGEDDLL_API NativeEntity {
public:
NativeEntity();
//gets
std::string GetString();
std::vector<double> GetVector();
//sets
void SetString(std::string nString);
void SetVector(std::vector<double> &nVec);
private:
std::string mString;
std::vector<double> mVec;
}
Native.cpp
#include "NativeEntity.h"
#include <string>
#include <iostream>
#define WIN32_LEAN_AND_MEAN // Exclude rarely-used stuff from Windows headers
#include <windows.h>
#ifdef _MANAGED
#pragma managed(push, off)
#endif
BOOL APIENTRY DllMain(
HMODULE /*hModule*/,
DWORD ul_reason_for_call,
LPVOID /*lpReserved*/)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}
#ifdef _MANAGED
#pragma managed(pop)
#endif
NativeEntity::NativeEntity() {
mString = "blank_string";
mVec = {0.0, 1.1, 2.2};
}
std::string NativeEntity::GetString()
{
std::cout << "Unmanaged NativeEnity - GetString() was called: " << mString << std::endl;
return mString;
}
std::vector<double> NativeEntity::GetVector()
{
std::cout << "Unmanaged NativeEnity - GetVector() was called: ";
for (const auto& i : mVec)
{
std::cout << i;
if (i != mVec.back()) std::cout << ", ";
}
std::cout<< std::endl;
return mVec;
}
void NativeEntity::SetString(std::string nString)
{
std::cout << "Unmanaged NativeEnity - SetString() was called with old value: " << mString << " new value: " << nString << std::endl;
mString = nString;
}
void NativeEntity::SetVector(std::vector<double> &nVec)
{
std::cout << "Unmanaged NativeEnity - SetVector() was called with old value: ";
for (const auto& i : mVec)
{
std::cout << i;
if (i != mVec.back()) std::cout << ", ";
}
std::cout << " new value: ";
for (const auto& j : nVec)
{
std::cout << j;
if (j != nVec.back()) std::cout << ", ";
}
std::cout << std::endl;
mVec = nVec;
}
Test Caller.h
#define WIN32_LEAN_AND_MEAN // Exclude rarely-used stuff from Windows headers
#include <windows.h>
#include "TestClassCallers.h"
extern "C" EXAMPLEUNMANAGEDDLL_API NativeEntity* CreateTestClass()
{
return new NativeEntity();
}
extern "C" EXAMPLEUNMANAGEDDLL_API void DisposeTestClass(NativeEntity* pObject)
{
if(pObject != NULL)
{
delete pObject;
pObject = NULL;
}
}
extern "C" EXAMPLEUNMANAGEDDLL_API std::string CallGetString(NativeEntity* pObject)
{
if (pObject != NULL)
{
return pObject->GetString();
}
return NULL;
}
extern "C" EXAMPLEUNMANAGEDDLL_API std::vector<double> CallGetVector(NativeEntity* pObject)
{
if (pObject != NULL)
{
return pObject->GetVector();
}
return std::vector<double>();
}
extern "C" EXAMPLEUNMANAGEDDLL_API void CallSetString(NativeEntity* pObject, std::string nString)
{
if (pObject != NULL)
{
pObject->SetString(nString);
}
}
extern "C" EXAMPLEUNMANAGEDDLL_API void CallSetVector(NativeEntity* pObject, std::vector<double> &nVec)
{
if (pObject != NULL)
{
pObject->SetVector(nVec);
}
}
Mananged.h
#include "ManagedEntity.h"
using namespace EntityLibrary;
using namespace System;
using namespace System::Runtime::InteropServices; // needed for Marshal
ManagedEntity::ManagedEntity() {
nativeObj = new NativeEntity();
}
ManagedEntity::~ManagedEntity() {
delete nativeObj;
}
String^ ManagedEntity::GetString()
{
String^ tempString = gcnew String( nativeObj->GetString().c_str());
return tempString;
}
array<double>^ ManagedEntity::GetVector()
{
std::vector<double> tempVec = nativeObj->GetVector();
const int SIZE = tempVec.size();
array<double> ^tempArr = gcnew array<double>(SIZE);
for (int i = 0; i < SIZE; i++)
{
tempArr[i] = tempVec[i];
}
return tempArr;
}
void ManagedEntity::SetString(String^ nString)
{
IntPtr pString = Marshal::StringToHGlobalAnsi(nString);
try
{
char* pchString = static_cast<char *>(pString.ToPointer());
nativeObj->SetString(pchString);
}
finally
{
Marshal::FreeHGlobal(pString);
}
}
void ManagedEntity::SetVector(array<double>^ nVec)
{
std::vector<double> tempVec;
for each (double elem in nVec)
tempVec.push_back(elem);
nativeObj->SetVector(tempVec);
}
Assembly.cpp
using namespace System;
using namespace System::Reflection;
using namespace System::Runtime::CompilerServices;
using namespace System::Runtime::InteropServices;
using namespace System::Security::Permissions;
//
// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
//
[assembly:AssemblyTitleAttribute("EntityLibrary")];
[assembly:AssemblyDescriptionAttribute("")];
[assembly:AssemblyConfigurationAttribute("")];
[assembly:AssemblyCompanyAttribute("EMC")];
[assembly:AssemblyProductAttribute("EntityLibrary")];
[assembly:AssemblyCopyrightAttribute("Copyright (c) EMC 2007")];
[assembly:AssemblyTrademarkAttribute("")];
[assembly:AssemblyCultureAttribute("")];
//
// Version information for an assembly consists of the following four values:
//
// Major Version
// Minor Version
// Build Number
// Revision
//
// You can specify all the value or you can default the Revision and Build Numbers
// by using the '*' as shown below:
[assembly:AssemblyVersionAttribute("1.0.*")];
[assembly:ComVisible(false)];
[assembly:CLSCompliantAttribute(true)];
C# Program.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using EntityLibrary;
namespace SimpleClient
{
class Program
{
static void Main(string[] args)
{
var entity = new ManagedEntity();
string myString = "c# empty string";
double[] myArray = { -1.1, -2.1, -3.1 };
//Test String
System.Console.WriteLine("C# - Value for string: {0}", myString);
System.Console.WriteLine("C# - C++ Value stored for the string: {0}", entity.GetString());
myString = entity.GetString();
System.Console.WriteLine("C# - Value for string after GetString: {0}", myString);
entity.SetString("one-hundred point two");
System.Console.WriteLine("C# - C++ Value stored for the string: {0} \n", entity.GetString());
//Test Array
System.Console.WriteLine("C# - Value for array: {0}", string.Join(", ", myArray));
System.Console.WriteLine("C# - C++ Value stored for the string: {0}", string.Join(", ", entity.GetVector()));
myArray = entity.GetVector();
System.Console.WriteLine("C# - Value for array after GetVector: {0}", string.Join(", ", myArray));
entity.SetVector(new double[] { 100.1, 100.2, 100.3, 100.4 });
System.Console.WriteLine("C# - C++ Value stored for the string: {0}", string.Join(", ", entity.GetVector()));
//array,comma trick needs .NET 4+ if it does not work use code below to print array to console
// foreach (var item in myArray)
// {
// Console.Write("{0}", item);
// if (item != myArray.Last())
// Console.Write(",");
// }
}
}
}
Now this is where it gets hacky so I added a special template Csharp_Test_Application.csproj.template
For the CMAKE main CMakeLists.txt below to read....
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">x64</Platform>
<ProjectGuid>{349769A5-261C-4234-BC48-F558817438F0}</ProjectGuid>
<OutputType>Exe</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>Csharp_Test_Application</RootNamespace>
<AssemblyName>Csharp_Test_Application</AssemblyName>
<TargetFrameworkVersion>v4.5.2</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|x64' ">
<PlatformTarget>x64</PlatformTarget>
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>${CMAKE_RUNTIME_OUTPUT_DIRECTORY}\Debug</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|x64' ">
<PlatformTarget>x64</PlatformTarget>
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>${CMAKE_RUNTIME_OUTPUT_DIRECTORY}\Release</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<ItemGroup>
<Reference Include="CLI_Bridge, Version=1.0.6179.18199, Culture=neutral, processorArchitecture=AMD64">
<SpecificVersion>False</SpecificVersion>
<HintPath>${CMAKE_RUNTIME_OUTPUT_DIRECTORY}\Release\CLI_Bridge.dll</HintPath>
</Reference>
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Data" />
<Reference Include="System.Net.Http" />
<Reference Include="System.Xml" />
</ItemGroup>
<ItemGroup>
<Compile Include="${DOS_STYLE_SOURCE_DIR}\**\*.cs" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Other similar extension points exist, see Microsoft.Common.targets.
<Target Name="BeforeBuild">
</Target>
<Target Name="AfterBuild">
</Target>
-->
</Project>
This is the main CMakeLists.txt which utilizes the C# configure_file and include_external_msproject to help create the c# project inside CMAKE from the template csproj file above
See the github link if this is too much for more types and even more detail.
cmake_minimum_required(VERSION 2.8.9)
project("CLI_DLL_Bridge_Test")
##############################################################
# Output paths
##############################################################
SET(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}")
SET(CMAKE_ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}")
SET(CMAKE_LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}")
set(NATIVEENTITY_INCLUDE_DIR
${CMAKE_CURRENT_SOURCE_DIR}/UnmanagedNativeDLL)
add_subdirectory(UnmanagedNativeDLL)
add_subdirectory(CLI_DLL_Bridge)
add_subdirectory(UnmanagedNativeTest_Application)
# C# Project MumboJumbo Hack
SET(CSHARP_PROJECT "Csharp_Test_Application")
SET(CSHARP_PROJECT_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/Csharp_Test_Application")
MESSAGE( STATUS "Copying in C# Project: " ${CSHARP_PROJECT} )
MESSAGE( STATUS "from directory: " ${CSHARP_PROJECT_DIRECTORY} )
FILE(TO_NATIVE_PATH "${CSHARP_PROJECT_DIRECTORY}" DOS_STYLE_SOURCE_DIR)
include_external_msproject(
${CSHARP_PROJECT} ${CSHARP_PROJECT}.csproj
TYPE FAE04EC0-301F-11D3-BF4B-00C04F79EFBC CLI_Bridge NativeEntity)
CONFIGURE_FILE(${CSHARP_PROJECT_DIRECTORY}/${CSHARP_PROJECT}.csproj.template ${CSHARP_PROJECT}.csproj)