I have the following C++ API
#ifndef AGUR_H
#define AGUR_H
#define AGUR_EXPORT __attribute__((visibility("default")))
#include <cstdint>
extern "C"
{
AGUR_EXPORT void sphericalRelativePoseSolveFromFile(
const char* filePathStatic,
const char* filePathDynamic,
const char* solverConfigJson,
char** solverSolutionJson);
AGUR_EXPORT void sphericalRelativePoseSolve(
const char* rawImageStatic,
const char* rawImageDynamic,
int imageWidth,
int imageHeight,
const char* solverConfigJson,
char** solverSolutionJson);
}
#endif // AGUR_H
My implementation for the first method above (and the one I am concerned with) is
void sphericalRelativePoseSolveFromFile(
const char* filePathStatic,
const char* filePathDynamic,
const char* solverConfigJson,
char** solverSolutionJson)
{
json cj = json::parse(solverConfigJson);
types::SolverConfiguration solverConfig = cj.get<types::SolverConfiguration>();
std::string sfp = std::string(filePathStatic);
std::string dfp = std::string(filePathDynamic);
agur::sfm::SphericalRelativePoseSolver sphericalRelativePoseSolver(solverConfig);
types::SolverSolution solverSolution = sphericalRelativePoseSolver.solve(sfp, dfp);
const json sj{ solverSolution };
std::string jsonString = sj[0].dump();
char* tmp = (char*)jsonString.c_str();
size_t len = std::strlen(tmp) + 1;
*solverSolutionJson = (char*)malloc(len);
std::memcpy((void*)*solverSolutionJson, tmp, len);
}
I call this API from C++ as followws and the results are what I expect and want.
std::string ss = "/Alignment/Theta_R0010732_2048.jpg";
const char* staticFilePath = ss.c_str();
std::string ds = "/Alignment/Theta_R0010733_2048.jpg";
const char* dynamicFilePath = ds.c_str();
const char* solverConfigJson =
"{"
"\"acRansacMaxThreshold\": 4.0,"
"\"outputEssentialGeometry\": true,"
"\"outputFeatures\": true,"
"\"outputInliers\": true,"
"\"outputMatches\": false,"
"\"unstableSolutionInlierThreshold\": 60,"
"\"useUprightRelativePoseSolver\": true"
"}";
char solverSolutionJson[] = "";
char* ptrSolverSolutionJson = solverSolutionJson;
sphericalRelativePoseSolveFromFile(staticFilePath, dynamicFilePath, solverConfigJson, &ptrSolverSolutionJson);
std::cout << ptrSolverSolutionJson << std::endl;
This prints out the expected solver results as jsomn - great. However, I now want to call the API from a Unity project. The Unity code so far is
using System;
using System.IO;
using System.Runtime.InteropServices;
using UnityEngine;
using UnityEngine.Networking;
public class AgurIvocation : MonoBehaviour
{
private string _status = string.Empty;
#if UNITY_IOS || UNITY_TVOS
// On iOS plugins are statically linked into
// the executable, so we have to use __Internal as the
// library name.
private const string Import = "__Internal";
#else
// Other platforms load plugins dynamically, so pass the
private const string Import = "agur";
#endif
[DllImport(Import)]
private static unsafe extern void sphericalRelativePoseSolveFromFile(
string filePathStatic,
string filePathDynamic,
string solverConfigJson,
out string[] solverSolutionJson);
[DllImport(Import)]
private static unsafe extern void sphericalRelativePoseSolve(
string rawImageStatic,
string rawImageDynamic,
int imageWidth,
int imageHeight,
string solverConfigJson,
out string[] solverSolutionJson);
void Start()
{
try
{
var filePathStatic = "/Alignment/Theta_R0010732_2048.jpg";
var filePathDynamic = "/Alignment/Theta_R0010733_2048.jpg";
var solverConfigJson =
"{" +
"\"acRansacMaxThreshold\": 4.0," +
"\"outputEssentialGeometry\": true," +
"\"outputFeatures\": true," +
"\"outputInliers\": true," +
"\"outputMatches\": false," +
"\"unstableSolutionInlierThreshold\": 60," +
"\"useUprightRelativePoseSolver\": true" +
"}";
var solverSolutionJson = new string[] { };
sphericalRelativePoseSolveFromFile(filePathStatic, filePathDynamic, solverConfigJson, out solverSolutionJson);
Console.WriteLine(solverSolutionJson[0]);
}
catch (Exception e)
{
_status = e.ToString();
}
}
// Update is called once per frame
void Update()
{
}
private string UnpackAsset(string path)
{
#if !UNITY_ANDROID
return Path.Combine(Application.streamingAssetsPath, path);
#endif
var inputFilePath = Path.Combine(Application.streamingAssetsPath, path);
var outputFilePath = Path.Combine(Application.persistentDataPath, path);
var loadingRequest = UnityWebRequest.Get(inputFilePath);
loadingRequest.SendWebRequest();
while (!loadingRequest.isDone)
{
if (loadingRequest.isNetworkError || loadingRequest.isHttpError)
{
break;
}
}
if (!loadingRequest.isNetworkError && !loadingRequest.isHttpError)
{
File.WriteAllBytes(outputFilePath, loadingRequest.downloadHandler.data);
return outputFilePath;
}
string errorMessage = $"Unpacking asset failed: {path}, reason: {loadingRequest.error}";
throw new Exception(errorMessage);
}
}
But I am getting nothing out of the solverSolutionJson variable. Simply, this is not working - can someone advise are to how this should be done from Unity/Mono please?
Related
I am relatively new to C++, and have copied some code from a live stream to get started with embedding C# mono into my game engine.
I have created a C# script:
using System;
namespace Neutron {
public class Main {
public float FloatVar { get; set; }
public Main() {
Console.WriteLine("Main constructor");
}
public void PrintMessage() {
Console.WriteLine("Hello World from C#!");
}
public void PrintCustomMessage(string message) {
Console.WriteLine($"C# says: {message}");
}
}
}
and built it to
../Sandbox/Sandbox/bin/Debug/Sandbox.dll (which i have verified within the c++ program that I am embedding it into)
when i call mono_runtime_object_init passing the MonoObject* created from calling mono_object_new (I have verified that it doesnt return a nullptr)
The following error occurs:
* Assertion at object.c:116, condition `is_ok (error)' not met, function:mono_runtime_object_init, (null) assembly:/usr/lib/mono/4.5/mscorlib.dll type:TypeInitializationException member:(null)
Followed by a long stack trace.
Here are my relavent files:
ScriptingEngine.cpp (See the InitMono function towards the bottom)
//
// Created by aw1lt on 05/12/22.
//
#include "ScriptingEngine.h"
#include "Logger.h"
#include <mono/jit/jit.h>
#include <mono/metadata/assembly.h>
#include <fstream>
namespace Neutron {
struct ScriptEngineData {
MonoDomain* RootDomain = nullptr;
MonoDomain* AppDomain = nullptr;
MonoAssembly* CoreAssembly = nullptr;
};
char* ReadBytes(const std::string& filepath, uint32_t* outSize) {
std::ifstream stream(filepath, std::ios::binary | std::ios::ate);
if (!stream) {
// Failed to open the file
return nullptr;
}
std::streampos end = stream.tellg();
stream.seekg(0, std::ios::beg);
uint32_t size = end - stream.tellg();
if (size == 0) {
// File is empty
return nullptr;
}
char* buffer = new char[size];
stream.read((char*)buffer, size);
stream.close();
*outSize = size;
return buffer;
}
MonoAssembly* LoadCSharpAssembly(const std::string& assemblyPath) {
uint32_t fileSize = 0;
char* fileData = ReadBytes(assemblyPath, &fileSize);
// NOTE: We can't use this image for anything other than loading the assembly because this image doesn't have a reference to the assembly
MonoImageOpenStatus status;
MonoImage* image = mono_image_open_from_data_full(fileData, fileSize, 1, &status, 0);
if (status != MONO_IMAGE_OK) {
const char* errorMessage = mono_image_strerror(status);
Logger::Crit(errorMessage);
return nullptr;
}
MonoAssembly* assembly = mono_assembly_load_from_full(image, assemblyPath.c_str(), &status, 0);
mono_image_close(image);
// Don't forget to free the file data
delete[] fileData;
return assembly;
}
void PrintAssemblyTypes(MonoAssembly* assembly) {
MonoImage* image = mono_assembly_get_image(assembly);
const MonoTableInfo* typeDefinitionsTable = mono_image_get_table_info(image, MONO_TABLE_TYPEDEF);
int32_t numTypes = mono_table_info_get_rows(typeDefinitionsTable);
for (int32_t i = 0; i < numTypes; i++) {
uint32_t cols[MONO_TYPEDEF_SIZE];
mono_metadata_decode_row(typeDefinitionsTable, i, cols, MONO_TYPEDEF_SIZE);
const char* nameSpace = mono_metadata_string_heap(image, cols[MONO_TYPEDEF_NAMESPACE]);
const char* name = mono_metadata_string_heap(image, cols[MONO_TYPEDEF_NAME]);
Logger::Log(std::string(nameSpace) + "." + name);
}
}
static ScriptEngineData* s_Data;
void ScriptingEngine::Init() {
s_Data = new ScriptEngineData();
InitMono();
}
void ScriptingEngine::Shutdown() {
delete s_Data;
}
void ScriptingEngine::InitMono(std::string path) {
MonoDomain* rootDomain = mono_jit_init("NeutronJITRuntime");
Logger::Assert(rootDomain != nullptr);
//system(("cat " + path).c_str());
// Store the root domain pointer
s_Data->RootDomain = rootDomain;
// Create an App Domain
s_Data->AppDomain = mono_domain_create_appdomain("NeutronScriptRuntime", nullptr);
mono_domain_set(s_Data->AppDomain, true);
s_Data->CoreAssembly = LoadCSharpAssembly(path);
Logger::Assert(s_Data->CoreAssembly != nullptr);
MonoImage* assemblyImage = mono_assembly_get_image(s_Data->CoreAssembly);
MonoClass* monoClass = mono_class_from_name(assemblyImage, "Neutron", "Main");
Logger::Assert(monoClass != nullptr);
PrintAssemblyTypes(s_Data->CoreAssembly);
MonoObject* instance = mono_object_new(s_Data->AppDomain, monoClass);
Logger::Assert(instance != nullptr);
mono_runtime_object_init(instance); // << ERROR HAPPENS HERE
}
void ScriptingEngine::ShutdownMono() {
}
} // Neutron
my header file, ScriptingEngine.h
//
// Created by aw1lt on 05/12/22.
//
#ifndef NEUTRONENGINE_SCRIPTINGENGINE_H
#define NEUTRONENGINE_SCRIPTINGENGINE_H
#include <string>
namespace Neutron {
class ScriptingEngine {
public:
static void Init();
static void Shutdown();
private:
static void InitMono(std::string path = "../Sandbox/Sandbox/bin/Debug/Sandbox.dll");
static void ShutdownMono();
};
} // Neutron
#endif //NEUTRONENGINE_SCRIPTINGENGINE_H
I also tried the answers on this page.
Thank you, and sorry if this answer is a mess.
I am trying to send a message from c++ to csharp but some of my accents are lost in the way ( not all of them?? ) ps: writing from italian
Here is what I do :
c++:
#ifdef DLL_EXPORTS
#define DLL_API __declspec(dllexport)
#else
#define DLL_API __declspec(dllimport)
#endif
extern "C" {
DLL_API void __cdecl getResults(char* entry, wchar_t* result);
}
[...]
void getResults(char* entry,wchar_t* result)
{
std::string str(entry);
std::string Stringresult= "héà" ;
std::wstring wsTmp(Stringresult.begin(), Stringresult.end());
const wchar_t* constChar = wsTmp.c_str();
swprintf(result, Stringresult.length(), constChar);
c# :
[DllImport("libface.dll", CharSet = CharSet.Unicode, CallingConvention = CallingConvention.Cdecl)]
public static extern void getResults([MarshalAs(UnmanagedType.LPStr)] string entry, StringBuilder res);
static void Main()
{
StringBuilder result = new StringBuilder(2000);
string entry = Console.ReadLine();
getResults( entry,result);
Console.WriteLine(result);
Solved the problem thanks to this link (that state that the problem is much more complicated that some persons may think .... ) :
http://blog.kutulu.org/2012/04/marshaling-utf-8-harder-than-it-ought.html
c++ code:
extern "C" {
DLL_API char* __cdecl getResults(char* entry);
}
char* getResults(char* entry)
{
std::string Stringresult= "hàé";
char *cstr = new char[Stringresult.length() + 1];
strcpy(cstr, Stringresult.c_str());
return cstr;
}
c# code:
[DllImport("libface.dll" , EntryPoint = "getResults")]
private static extern IntPtr getResults([MarshalAs(UnmanagedType.LPStr)] string entry);
static void Main()
{
var data = new List<byte>();
var ptr = getResults("p");
var off = 0;
while (true)
{
var ch = Marshal.ReadByte(ptr, off++);
if (ch == 0)
{
break;
}
data.Add(ch);
}
string sptr = Encoding.UTF8.GetString(data.ToArray());
Console.WriteLine(sptr);
I have a c++ function like this:
myExport.h
extern "C" { __declspec(dllexport) const int Run(char *input, char *output, int *length); }
myExport.cpp
const int Run(char *input, char *output, int *length) {
std::ostringstream value;
value
<< "FOO" << "|"
<< "BAR" << "|";
auto str = value.str();
auto i = stdext::checked_array_iterator<char*>(output, str.length());
std::copy(str.begin(), str.end(), i);
output[str.length()] = '\0';
return 1;
}
And in C# I have:
myImport.cs
[DllImport("MyExport.dll", CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]
private static extern int Run(
[MarshalAs(UnmanagedType.LPStr)]string input,
StringBuilder output,
ref int length);
public static string Execute(string input)
{
var length = 1024;
var output = new StringBuilder(1024);
var result = Run(input, output, ref length);
return output.ToString();
}
However, the output buffer is always empty. What am I doing wrong?
Since the type is char * for the second parameter, and the DLL function will fill in the buffer that's passed, the C# declaration should be as follows:
[MarshalAs(UnmanagedType.LPStr)]System.Text.StringBuilder output
I've already written this piece of code which works fine:
C++ code
extern "C"
{
const MYLIBRARY_EXPORT char* giefStrPlx(char* addon)
{
return addon;
}
}
C# code
[DllImport("ClassLibrary1")]
private static extern IntPtr giefStrPlx(string x);
void Start()
{
IntPtr stringPtr = giefStrPlx("Huntsman");
string huntsman = Marshal.PtrToStringAnsi(echoedStringPtr);
}
After this huntsman contains "Huntsman".
My problem is the step of doing something similar for an array of strings. I wrote the following function
extern "C"
{
const MYLIBRARY_EXPORT bool fillStrArray(char** lizt, int* length)
{
char* one = "one";
char* two = "two";
char* three = "three";
lizt[0] = one;
lizt[1] = two;
lizt[2] = three;
*length = 3;
}
}
I then tried to write the following piece of code in C#
[DllImport("ClassLibrary1")]
private static extern bool fillStrArray(ref IntPtr array, ref int length);
void Start()
{
IntPtr charArray = IntPtr.Zero;
int charArraySize = 0;
fillStrArray(ref charArray, ref charArraySize);
IntPtr[] results = new IntPtr[charArraySize];
Marshal.Copy(charArray, results, 0, charArraySize);
foreach (IntPtr ptr in results)
{
string str = Marshal.PtrToStringAnsi(ptr);
}
}
Which does not work. So now I'm a bit lost on how to accomplish this.
Here are the two helper functions I have from CLR to std::string and from std::string to string CLR
std::string CLROperations::ClrStringToStdString(String^ str)
{
if (String::IsNullOrEmpty(str))
return "";
std::string outStr;
IntPtr ansiStr = System::Runtime::InteropServices::Marshal::StringToHGlobalAnsi(str);
outStr = (const char*)ansiStr.ToPointer();
System::Runtime::InteropServices::Marshal::FreeHGlobal(ansiStr);
return outStr;
}
String ^ CLROperations::StdStringToClr(std::string str)
{
return gcnew String(str.c_str());
}
for using a List of strings you will need to use List<String^>^ mind the capital String. for a list of std::string use std::vector<std::string>
I'm using Robert Giesecke's Unmanaged Exports package to be able to call from C++ to C#.
This has to use the C interface from within C++. I have managed to get most things working, by scouring the web and picking up bits here and there....
extern "C"
{
// Simple
__declspec(dllimport) int IntTest(int input);
__declspec(dllimport) double DoubleTest(double input);
// Array of simple types in
__declspec(dllimport) int passArray(int t[], int i, int xx);
// String in and out
__declspec(dllimport) int PassStringIn(wchar_t* str);
__declspec(dllimport) int PassStringOut(wchar_t** str);
__declspec(dllimport) wchar_t* PassStringInOut(wchar_t* str);
// Array of strings types in
//__declspec(dllimport) int passArrayStrings(char** t, int i);
}
....
// Int in and out
int aa = IntTest(4);
// Double in and out
double bb = DoubleTest(4.3);
// Pass array in
int arr[4] = { 1,2,3,4 };
int cc = passArray(arr, 4, 0);
// String in
wchar_t* a_str = L"input string from C++";
int dd = PassStringIn(a_str);
// String out
wchar_t* b_str = L"not used";
int ee = PassStringOut(&b_str);
// String in & out
wchar_t* d_str = L"bob";
wchar_t* result = PassStringInOut(d_str);
corresponding C#
[DllExport( CallingConvention = CallingConvention.Cdecl)]
static int IntTest(int input)
{
return input + 1;
}
[DllExport(CallingConvention = CallingConvention.Cdecl)]
static double DoubleTest(double input)
{
return input + 1;
}
[DllExport(CallingConvention = CallingConvention.Cdecl)]
public static int passArray([In, Out, MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 1)] int[] tab, int i, int x)
{
return tab[x];
}
[DllExport(CallingConvention = CallingConvention.Cdecl)]
public static int PassStringIn( [MarshalAs(UnmanagedType.LPWStr)] string inputString)
{
Console.WriteLine("Hi, the string passed in was :" + inputString);
return 1;
}
[DllExport(CallingConvention = CallingConvention.Cdecl)]
static int PassStringOut([MarshalAs(UnmanagedType.BStr)] out string outputString)
{
Console.WriteLine("Hi, I will return the time from c#");
outputString = DateTime.Now.ToLongTimeString();
return 0; // indicates success
}
[DllExport(CallingConvention = CallingConvention.Cdecl)]
[return: MarshalAs(UnmanagedType.LPTStr)]
public static string PassStringInOut([MarshalAs(UnmanagedType.LPTStr)]string name)
{
return string.Format("Hello from .NET assembly, {0}!", name);
}
Which was nice! Anyway would anybody be able to help with passing arrays of strings in and out. I am pretty sure the C# section should look like this:
[DllExport(CallingConvention = CallingConvention.Cdecl)]
public static int passArrayStrings( [In, Out, MarshalAs(UnmanagedType.LPArray, ArraySubType = UnmanagedType.LPStr, SizeParamIndex = 1)] string[] tab, int i)
{
return 1;
}
I need some help on the C++(C) side on how to structure the array of strings in, such that they can be marshaled correctly. The mixed mode assembly created has both C# and and a C interface. As it is C and not C++ the arguments types of the exposed functions are not visible.
Thanks
You can use an IntPtr parameter.
You'll have to allocate unmanaged memory and copy the array into that blob anyway. Otherwise the GC will eat your array at some point.
Unmanaged Exports with Arrays
ok so after a lot of messing about I came to a solution:
// Array of strings types in
__declspec(dllimport) int passArrayStrings(BSTR* bstrArray, int i);
BSTR bstrArray[10] = { 0 };
for (int i = 0; i < 10; i++)
{
bstrArray[i] = ::SysAllocString(L"My String.");
}
int ff = passArrayStrings(bstrArray, 10);
for (int i = 0; i < 10; i++)
{
::SysFreeString(bstrArray[i]);
}
and on the c# side:
[DllExport(CallingConvention = CallingConvention.Cdecl)]
public static int passArrayStrings([In, Out, MarshalAs(UnmanagedType.LPArray, ArraySubType = UnmanagedType.BStr, SizeParamIndex = 1)] string[] tab, int iSize)
{
return 1;
}