DNNE
DNNE copied to clipboard
Provide example for strings
It would be nice to have an example for strings as function parameters or returned value.
Thanks, amazing project!
@david-bouyssie Like this one?:
[UnmanagedCallersOnlyAttribute(EntryPoint = "GetData")] //functions name for C interface
public static int GetData([DNNE.C99Type("const char**")] System.IntPtr StringText, [DNNE.C99Type("int*")] System.IntPtr intNumber, [DNNE.C99Type("double*")] System.IntPtr doubleNumber)
{
var returnValue = CSharpLibrary.GetData(out string strText, out int iNumber, out double dNumber);
Marshal.WriteIntPtr(StringText, Marshal.StringToHGlobalAnsi(strText));
Marshal.WriteInt32(intNumber, iNumber);
double[] val = new double[1];
val[0] = dNumber;
Marshal.Copy(val, 0, doubleNumber, 1);
return returnValue;
}
[UnmanagedCallersOnlyAttribute(EntryPoint = "SetData")] //functions name for C interface
public static int SetData([DNNE.C99Type("const char*")] System.IntPtr StringText, int intNumber, double doubleNumber)
{
var returnValue = CSharpLibrary.SetData(Marshal.PtrToStringAnsi(StringText), intNumber, doubleNumber);
return returnValue;
}
@AlexGer123 That is a solid example - thanks! It would be great to see the project that is a part of. Any chance you can share it?
@AaronRobinsonMSFT I have built some simply examples to provide how it works. I can share the examples tomorrow. :)
Great, thank you @AlexGer123
Small examples Interoperability C <-> C#
And this one for StringList from ANSI-C To C#: https://stackoverflow.com/questions/66932663/marshalling-string-array-from-ansi-c-to-string-in-c-sharp/66933877#66933877
SDK 5.0.202 x86 is needed and the build also must be build as x86. Some settings in "InteroperabilityTest.csproj" you should check...
Maybe that is enough. :)
@AlexGer123 thanks!
Two questions:
- what is the purpose of
char sResultText[OPTO_GLOBAL_MAX_PROCESSING_STRING_DATA_SIZE] = {""};
? - what is preventing from targeting x64?
what is the purpose of char sResultText[OPTO_GLOBAL_MAX_PROCESSING_STRING_DATA_SIZE] = {""};?
To copy the result like "strcpy(sResultText, strTextOut);" size for "OPTO_GLOBAL_MAX_PROCESSING_STRING_DATA_SIZE" freely selectable.
what is preventing from targeting x64?
For the build you must select.
I can contribute the opposite example -- calling a function in C/C++/D etc that expect a const char*
or equivalent 😃
Repo Here: https://github.com/GavinRay97/ReaperDNNE
The scenario for me is that the managed C# .dll
is being loaded by an external, closed-source C++ program, which calls extension/addon .dll
entrypoint functions with the following struct:
struct reaper_plugin_info_t
{
int caller_version;
void* hwnd_main;
int (*Register)(const char* name, void* infostruct);
void* (*GetFunc)(const char* name);
};
Where reaper_plugin_info_t->GetFunc()
takes a function name, looks up the function's C++ address, and returns a function pointer.
Here's a little C stub loader which simulates the 3rd party program loading my .dll
:
https://github.com/GavinRay97/ReaperDNNE/blob/master/include/example_loader.c
#include <stdio.h>
#include <string.h>
#include <windows.h>
#define EXPORTED_ENTRYPOINT_FN_NAME "ReaperPluginEntry"
#define DLL_NAME "../bin/Debug/net6.0/win-x64/ReaperDNNENE.dll"
typedef struct {
int caller_version;
HWND hwnd_main;
int (*Register)(const char *name, void *infostruct);
void *(*GetFunc)(const char *name);
} reaper_plugin_info_t;
void ShowConsoleMsg(const char *msg) {
printf("[ShowConsoleMsg]: %s \n", msg);
return;
}
void *GetFunc(const char *name) {
printf("GetFunc called with function name: %s \n", name);
if (strcmp(name, "ShowConsoleMsg") == 0)
return &ShowConsoleMsg;
return NULL;
}
int main() {
HMODULE hTargetDll = LoadLibraryA(DLL_NAME);
if (!hTargetDll) {
printf("[!] LoadLibraryA failed. Error: %lu\n", GetLastError());
return 0;
}
printf("[+] Target DLL base address: 0x%p\n", hTargetDll);
FARPROC entrypointFuncPtr =
GetProcAddress(hTargetDll, EXPORTED_ENTRYPOINT_FN_NAME);
void (*ReaperPluginEntry)(void *, reaper_plugin_info_t *) =
(void (*)(void *, reaper_plugin_info_t *))entrypointFuncPtr;
reaper_plugin_info_t rec;
rec.caller_version = 1;
rec.hwnd_main = NULL;
rec.GetFunc = &GetFunc;
rec.Register = NULL;
ReaperPluginEntry(NULL, &rec);
printf("[+] done\n");
return 1;
}
Now to write a C# .dll
which gets loaded by this C++ program, and use reaper_plugin_info_t->GetFunc()
to ask for the pointer to ShowConsoleMsg
then call it with a string, is like this:
https://github.com/GavinRay97/ReaperDNNE/blob/master/Program.cs
using System;
using System.Runtime.InteropServices;
namespace ReaperDNNE
{
public unsafe static class MyReaperPlugin
{
private const string ReaperPluginInfoStructString = @"
struct reaper_plugin_info_t
{
int caller_version;
void* hwnd_main;
int (*Register)(const char* name, void* infostruct);
void* (*GetFunc)(const char* name);
};
";
public struct ReaperPluginInfo
{
public int caller_version;
public IntPtr hwnd_main;
public delegate* unmanaged[Cdecl]<sbyte*, void*, int> Register;
public delegate* unmanaged[Cdecl]<sbyte*, void*> GetFunc;
}
[UnmanagedCallersOnly]
[DNNE.C99DeclCode(ReaperPluginInfoStructString)]
public static int ReaperPluginEntry(IntPtr hInstance, [DNNE.C99Type("struct reaper_plugin_info_t*")] ReaperPluginInfo* rec)
{
var showConsoleMsgStrPtr = Marshal.StringToHGlobalAnsi("ShowConsoleMsg");
var ShowConsoleMsg = (delegate* unmanaged[Cdecl]<sbyte*, void>)
rec->GetFunc((sbyte*)showConsoleMsgStrPtr);
Marshal.FreeHGlobal(showConsoleMsgStrPtr);
var helloMsgStrPtr = Marshal.StringToHGlobalAnsi("Hello From C#");
ShowConsoleMsg((sbyte*)helloMsgStrPtr);
Marshal.FreeHGlobal(helloMsgStrPtr);
return 1;
}
}
}
If you run dotnet build
on the C# project, with the following attributes (or similar):
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<EnableDynamicLoading>true</EnableDynamicLoading>
<DnneAddGeneratedBinaryToProject>true</DnneAddGeneratedBinaryToProject>
<DnneMSBuildLogging>high</DnneMSBuildLogging>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="DNNE" Version="1.*" />
</ItemGroup>
</Project>
And then change #define DLL_NAME
to be the path to your DNNE generated .dll
(with the NE
suffix), then compile the C .dll
loader/symbol caller program and execute it, you should get:
I hope this can be helpful to someone =) This work on DNNE is awesome, coolest thing I've seen in a while:
Credits/Acknowledgements:
- Proper conversion of the C struct (particularly the function pointers to delegates) to C# code
- Tanner Gooding et al's https://github.com/microsoft/ClangSharp tool and his support on Discord
- DLL Loading in C
- https://blog.whtaguy.com/2020/04/calling-arbitrary-functions-in-exes.html
- Using
Marshal.StringToHGlobalAnsi()
and freeing afterwards to properly convert managed string tosbyte*
- User
alexrp
from Dotnet Foundation, on the DotNetEvolution Discord server
- User
- Being made aware that DNNE exists
- Some super cool guy on a Reverse Engineering Discord server
- This library
- Aaron Robinson
I added libnethost.lib and InteroperabilityTestNE.lib to the C++ project -> Linker -> Input, however I'm getting a link error: Unresolved external symbol "int __stdcall SetData(Char cost *, int, double)". What am I missing?
@hsnmck We need more information. Maybe you upload two (C++,C#) projects....
Hey, I was able to work based on the examples. Thanks for providing them. I am just wondering who manages the memory of the created IntPtrs.
For me
Marshal.WriteIntPtr(StringText, Marshal.StringToHGlobalAnsi(strText));
would require a
Marshal.FreeHGlobal(StringText);
at some point which we do not know when it should happen