I have a problem generating C# code dynamically when the calling process is a native Win32 C++ application. To narrow down the problem, I have created the following test projects:
DynCodeDll
: .net5 assembly (C#) that dynamically compiles and executes source code via Roslyn compilerCLRLibCaller
: .Net/CLR dll, acts as a bridge so that DynCodeDll
can be called by native codeNativeExeCaller
: Win32 app that calls functions from DynCodeDll
via CLRLibCaller
DynCodeDll
:
Via package manager, Microsoft.CodeAnalysis.CSharp was added as the only reference.
Version used: 3.11, because according to https://github.com/dotnet/roslyn/blob/main/docs/wiki/NuGet-packages.md this is the latest version that works for VS2019 and .net5
public class Class1
{
public Class1()
{
System.Diagnostics.Debug.WriteLine("managed ctor");
}
public void compileAndRunDynCode()
{
System.Diagnostics.Debug.WriteLine(">>> managed compileAndRunDynCode");
DoRoslynDynCodeStuff();
System.Diagnostics.Debug.WriteLine("<<< managed compileAndRunDynCode");
}
private void DoRoslynDynCodeStuff()
{
System.Diagnostics.Debug.WriteLine(">>> managed DoRoslynDynCodeStuff");
// Create a syntax tree for the dynamic code
SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText(code);
//Here follows code compiling and run - not relevant to the problem.
System.Diagnostics.Debug.WriteLine("<<< managed DoRoslynDynCodeStuff");
return;
}
}
CLRLibCaller
:
References DynCodeDll
as a Dll, not as a project reference. CopyLocal
is set to true
#include <vcclr.h>
#define INTEROPBRIDGE_API extern "C" __declspec(dllexport)
gcroot<DynCodeDll::Class1^> flowControlMain;
INTEROPBRIDGE_API void init()
{
System::Diagnostics::Debug::WriteLine(">>> CLR-Init");
flowControlMain = gcnew DynCodeDll::Class1();
System::Diagnostics::Debug::WriteLine("<<< CLR-Init");
}
INTEROPBRIDGE_API void dyncode()
{
System::Diagnostics::Debug::WriteLine(">>> CLR-dyncode");
flowControlMain->compileAndRunDynCode();
System::Diagnostics::Debug::WriteLine("<<< CLR-dyncode");
}
NativeExeCaller
:
BOOL CNativeExeCallerDoc::OnNewDocument()
{
HMODULE hModule = LoadLibrary(_T("S:\\spielwiese\\dotnet\\TestRoslyn\\AsDll\\DynCodeDll\\Debug\\CLRLibCaller.dll"));
typedef void(*RunFlowControlSignature)();
auto initFlowControlMethod = (RunFlowControlSignature)GetProcAddress(hModule, "init");
auto runFlowControlMethod = (RunFlowControlSignature)GetProcAddress(hModule, "dyncode");
initFlowControlMethod(); //calls init method from CLI wrapper and works
runFlowControlMethod(); //calls compileDynCode from CLI wrapper and crashes, see below
return TRUE;
}
Notes:
Calling DynCodeDll
from another .net application works. In this case, the referenced dlls and their package dependencies (Microsoft.CodeAnalysis.dll
, Microsoft.CodeAnalysis.CSharp
, ...) are copied to the output directory of the calling app.
When NativeExeCaller
is executed, I can debug until DoRoslynDynCodeStuff
is called, then only an exception is thrown in the VS output window:
Exception thrown at 0x76EEFA72 in NativeExeCaller.exe: Microsoft C++ exception: EEFileLoadException at memory location 0x003AE20C.
Exception thrown at 0x76EEFA72 in NativeExeCaller.exe: Microsoft C++ exception: [rethrow] at memory location 0x00000000.
Exception thrown at 0x76EEFA72 in NativeExeCaller.exe: Microsoft C++ exception: [rethrow] at memory location 0x00000000.
The thread 0x1b84 has exited with code 0 (0x0).
Exception thrown at 0x76EEFA72 in NativeExeCaller.exe: Microsoft C++ exception: [rethrow] at memory location 0x00000000.
Exception thrown: 'System.IO.FileNotFoundException' in DynCodeDll.dll
An unhandled exception of type 'System.IO.FileNotFoundException' occurred in DynCodeDll.dll
Could not load file or assembly 'Microsoft.CodeAnalysis, Version=3.11.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35'. The system cannot find the specified file.
The DoRoslynDynCodeStuff
method is not entered.
The fact that the package dependencies are not found is not surprising, as they are not present in the output directory of the native app. However, it does not help to copy the files from a directory in which a working .net client is running. The error message remains the same
So the questions now are:
Microsoft.CodeAnalysis.CSharp
, from a native application through a c++/CLR wrapper?Background: The test example is part of a much larger project. Switching to VS2022, newer .net or calls "past" the CLI wrapper are not possible.
Thank you for your advice
Finally I came up with a solution. I did the following steps :
DynCodeDll
- in the projectfile add:
<PropertyGroup>`enter code here`
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
</PropertyGroup>
By doing this you prevent VS from building the output into I subdir .net5.0
. Since the c++/cli-Project isn't created in such a folder, be sure to put both dlls into the same one. Either both or none into .net5.0
add an AssemblyResolve
-Handler that loads missing assemblies from our own output folder. This is important since the calling Win32 app NativeExeCaller
is one folder above our DynCodeDll
and we need to locate the NuGet references in our own folder:
AppDomain currentDomain = AppDomain.CurrentDomain;
currentDomain.AssemblyResolve += new ResolveEventHandler(CurrentDomain_AssemblyResolve);
private Assembly? CurrentDomain_AssemblyResolve(object? sender, ResolveEventArgs args)
{
//This handler is called only when the common language runtime tries to bind to the assembly and fails.
Assembly executingAssembly = Assembly.GetExecutingAssembly(); //This is us
string? applicationDirectory = Path.GetDirectoryName(executingAssembly.Location);
if (applicationDirectory == null)
{
return null;
}
string[] fields = args.Name.Split(',');
string assemblyName = fields[0];
string? assemblyCulture = null;
if (fields.Length >= 2)
{
assemblyCulture = fields[2][(fields[2].IndexOf('=') + 1)..];
}
string assemblyFileName = assemblyName + ".dll";
string assemblyPath;
if (assemblyName.EndsWith(".resources") && assemblyCulture != null)
{
// Specific resources are located in app subdirectories
string resourceDirectory = Path.Combine(applicationDirectory, assemblyCulture);
assemblyPath = Path.Combine(resourceDirectory, assemblyFileName);
}
else
{
assemblyPath = Path.Combine(applicationDirectory, assemblyFileName);
}
if (File.Exists(assemblyPath))
{
// Load the assembly from the specified path.
loadingAssembly = Assembly.LoadFrom(assemblyPath);
}
return loadingAssembly;
}
DependencyCopier
: add another project, which is a .NET .exe
and reference DynCodeDll
from it. It does not has to have any implementation. Just set the output folder to he same as DynCodeDll
. By doing so the VS build process copies the CodeAnalysis NuGet package into that folder.
If you reference DynCodeDll
just from the c++/cli project CLRLibCaller
, these references are not copied, which is a bug I think.
Make sure to also set AppendTargetFrameworkToOutputPath
to false
for this project.
NativeExeCaller
- before calling LoadLibrary
, add the output folder of DynCodeDll
to the DllSearchpath
. Otherwise LoadLibrary
fails because it won't load dependencies of CLRLibCaller.dll
, mainly ijwhost.dll
(which is part of the .net runtime I think)
CString CFileTools::GetPath(const CString& sFileNameWithPathAndName)
{
CString szDrive, szDir, szFilename, szExt;
_tsplitpath_s(sFileNameWithPathAndName,
szDrive.GetBuffer(_MAX_PATH), _MAX_PATH,
szDir.GetBuffer(_MAX_PATH), _MAX_PATH,
szFilename.GetBuffer(_MAX_PATH), _MAX_PATH,
szExt.GetBuffer(_MAX_PATH), _MAX_PATH);
szDrive.ReleaseBuffer();
szDir.ReleaseBuffer();
szFilename.ReleaseBuffer();
szExt.ReleaseBuffer();
szDir = szDrive + szDir;
return szDir;
}
BOOL CNativeExeCallerDoc::OnNewDocument()
{
CString szModuleFilePathname;
GetModuleFileName(nullptr, szModuleFilePathname.GetBuffer(_MAX_PATH), _MAX_PATH);
szModuleFilePathname.ReleaseBuffer();
//Extract the Path without .exe
CString sOurOwnPath = CFileTools::GetPath(szModuleFilePathname);
CString sPathToFlowControl = sOurOwnPath + _T("\\DynCodeDll");
BOOL bDirAdded = SetDllDirectory(static_cast<LPCTSTR>(sPathToFlowControl));
ASSERT(bDirAdded == TRUE);
HMODULE hModule = AfxLoadLibrary(_T("CLRLibCaller.dll"));
ASSERT(hModule != NULL);
typedef void(*RunFlowControlSignature)();
auto initFlowControlMethod = (RunFlowControlSignature)GetProcAddress(hModule, "init");
auto runFlowControlMethod = (RunFlowControlSignature)GetProcAddress(hModule, "dyncode");
initFlowControlMethod(); //calls init method from CLI wrapper and works
runFlowControlMethod(); //calls compileDynCode
return TRUE;
}
Well... doing all this finally made it work for me. Maybe it helps someone having the same issue