Search code examples
c#nugetc++-cli.net-5roslyn

Package-References to Roslyn not working when calling from native App


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 compiler
  • CLRLibCaller: .Net/CLR dll, acts as a bridge so that DynCodeDll can be called by native code
  • NativeExeCaller: 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:

  • How can I call a .net dll, which in turn uses 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


Solution

  • 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