none
Has anything been added to .Net 4.0 to support "Reverse P/Invoke" scenarios?

    Question

  • I am creating a managed plugin for an unmanaged host. This is for a music application, and it uses the well-established native VST spec. I have created a post-build utility that decompiles a managed DLL and looks for methods that have a special attribute added that signals that it should be exported as a native function. Here is an example: 

     

    [DllExport("main", CallingConvention.Cdecl)]
    public static IntPtr VSTChordsPlugin(IntPtr hostCallback)
    {
     ...
    }
    

    So the "DllExport" is an attribute I created that stores the name you want to use for the exported function (default is the name of the managed method), and the calling convention you want to use. My post-build utility scans through the IL and finds these attributes, then tweaks the IL to properly export the method as an unmanaged function call. It then recompiles the DLL from the IL and creates both a 32-bit and 64-bit version, so that native 32-bit and 64-bit hosts can use the managed DLL directly. This eliminates the need for an intermediate C++/CLI DLL.

    Since I did this before .Net 4.0, my question is whether .Net 4.0 adds any new support that makes reverse P/Invoke any easier? To me this syntax is just as clean as using DllImport when you want to P/Invoke. Why can't you do this directly from C# since the .Net framework fully supports exporting methods as unmanaged functions? Why hide it from C#?

    • Edited by BitFlipper Sunday, November 21, 2010 5:20 PM Formatting
    Sunday, November 21, 2010 5:18 PM

All replies

  • LOL, I guess that is a big fat NO then. Thanks for clearing it up!
    Monday, November 22, 2010 8:10 PM
  • No, you still cannot export standalone functions.  However, you can develop COM objects in .NET which can then be consumed by native code.

    >  exporting methods as unmanaged functions?

    I don't believe so.  While the CLR does have metadata for a top-level method (forget the right name for this...) as might occur when languages like C are built on the CLR, specifying this metadata does not cause there to be a DLL export that can be consumed by Windows dynamic linking.  It can consumed by .NET managed code that references the assembly.

     

     

     

    Monday, November 22, 2010 11:52 PM
  • Thanks for the info. As I said I'm able to already do this via a special post-build utility that decompiles the DLL into IL and then tweaks the IL to properly export the methods I tagged with the custom DllExport attribute. I'm just wondering why this isn't exposed all the way up to C#. It would make these kinds of scenarios much easier and completely eliminate the intermediate C++/CLI DLL that is otherwise required. In addition, doing it this way prevents Edit-and-Continue from working properly. Fortunately all other debugging functionality works properly, so it isn't too bad.

    I can't use COM because the DLL I'm writing is being called from an unmanaged application that is following a well-established unmanaged API, and expects the plugin to do the same. There is no way for me to modify this 3rd-party application to use COM.

    Tuesday, November 23, 2010 12:31 AM
  • > tweaks the IL to properly export the methods I tagged with the custom DllExport attribute.

    Could you please point me to the exact name/documentation of the metadata you changed to the IL?  I didn't think it was possible to use metadata to instruct the assembler to add entries to the PE export header so that they could be consumed by unmanaged code.  Or is this merely a managed code thing (that I believe is possible with techniques like you described).

     

     

     

    Tuesday, November 23, 2010 12:45 AM
  • > tweaks the IL to properly export the methods I tagged with the custom DllExport attribute.

    Could you please point me to the exact name/documentation of the metadata you changed to the IL?  I didn't think it was possible to use metadata to instruct the assembler to add entries to the PE export header so that they could be consumed by unmanaged code.  Or is this merely a managed code thing (that I believe is possible with techniques like you described).


    Well, the end result is that for an unmanaged application, it can load the DLL and see the exported functions, just as if it was a true unmanaged DLL. The very first time it calls into any of the exported functions, the DLL will automatically load the .Net framework, and the call will end up going into your managed method. This is just like it happens when you call into a managed C++/CLI DLL. The only thing I am doing is exposing this functionality all the way up to C# by adding my own custom DllExport attribute.

    It has been some time since I worked on my IL parser. Below are some comments that are at the top of one of my source files (copied from somewhere on the web a long time ago). I believe things have changed a bit since then (see last comment). That should at least give you something to start from.

    Here is also a thread where I posted about this a long time ago. There is some source code you can download from there although I have since completely rewritten the IL parser to make it cleaner and more reliable. If you are really interested I could post my latest code.

      // It is well known fact that .NET assembly could be tweaked to export native method, 
      // similar way how normal DLLs do it. There is good description and tool from Selvin 
      // for 32bit native function.
      //
      // My problem was how to do it for 64bits. Here you go.
      //
      // 1) you IlDAsm your assembly into il code.
      // 2) Edit the IL and change header to look like this:
      //
      // For 32bit:
      //   .corflags 0x00000002
      //   .vtfixup [1] int32 fromunmanaged at VT_01
      //   .data VT_01 = int32[1]
      //
      // For 64bit 
      //   .corflags 0x00000008 
      //   .vtfixup [1] int64 fromunmanaged at VT_01
      //   .data VT_01 = int64[1]
      //
      // 3) Header of your exported method would look similar to this. This is same for 32bit version. 
      //   .vtentry 1 : 1
      //   .export [1] as Java_net_sf_jni4net_Bridge_initDotNet
      //
      // 4) You IlAsm the file back into DLL. For x64 you use /x64 flag.
      //
      // 5) Update: It looks like none of the .vtfixup, .data or .vtentry changes are required any more to make this work.
      //  This simplifies the parser quite a lot. We only need to change .corflags and modify the method signature
    

     

    Tuesday, November 23, 2010 1:07 AM
  • Thanks, I didn't realize that there was an MSIL syntax for this.  (Probably because of my lack of knowledge of using the CLI from C++.)  There are a number of things you can do from C++/CLI for which C# could provide syntax but does not.

     

     

     

    Tuesday, November 23, 2010 1:29 AM
  • Not sure whether it is ok to post long lists of code here, but here is my exporter class. Simply instantiate and call "Execute()". It will pick up the command line args and create one or two new executables, depending on whether you chose 32 and/or 64 bit versions.

    In addition, in your project that contains the C# methods that you want to tag as unmanaged functions, you need to add the DllExport attribute class (see below). The DllExport class doesn't need to exist in the IL parser project, since the parser only looks for the signature in the decompiled IL and doesn't use the DllExport class directly. See first post for an example of how to tag a method to be exported.

    In my main project, I have a post-build step that looks like this:

     

    if "$(OutDir)"=="bin\Debug\" (set EXPORTARGS=/debug /name32:" x86" /name64:" x64") ELSE (set EXPORTARGS=/name32:" x86" /name64:" x64")
    "$(SolutionDir)Utilities\DllExport.exe" %EXPORTARGS% /input:"$(TargetPath)"

     

    Here is the exporter class:

     

    using System;
    using System.Collections.Generic;
    using System.Text;
    using System.IO;
    using System.Reflection;
    using System.Diagnostics;
    using Microsoft.Win32;
    using System.ComponentModel;
    using System.Threading;
    using System.Runtime.InteropServices;
    using System.Runtime.CompilerServices;
    
    namespace DllExport
    {
      // Export native 64bit method from .NET assembly
      // =============================================
      // It is well known fact that .NET assembly could be tweaked to export native method, 
      // similar way how normal DLLs do it. There is good description and tool from Selvin 
      // for 32bit native function.
      //
      // My problem was how to do it for 64bits. Here you go.
      //
      // 1) you IlDAsm your assembly into il code.
      // 2) Edit the IL and change header to look like this:
      //
      // For 32bit:
      //   .corflags 0x00000002
      //   .vtfixup [1] int32 fromunmanaged at VT_01
      //   .data VT_01 = int32[1]
      //
      // For 64bit 
      //   .corflags 0x00000008 
      //   .vtfixup [1] int64 fromunmanaged at VT_01
      //   .data VT_01 = int64[1]
      //
      // 3) Header of your exported method would look similar to this. This is same for 32bit version. 
      //   .vtentry 1 : 1
      //   .export [1] as Java_net_sf_jni4net_Bridge_initDotNet
      //
      // 4) You IlAsm the file back into DLL. For x64 you use /x64 flag.
      //
      // 5) Update: It looks like none of the .vtfixup, .data or .vtentry changes are required any more to make this work.
      //  This simplifies the parser quite a lot. We only need to change .corflags and modify the method signature 
    
      /// <summary>
      /// Exports the specified methods as C callable functions
      /// </summary>
      class Exporter
      {
        #region Enums
    
        private enum Platform
        {
          x86,
          x64,
        }
    
        #endregion Enums
    
        #region Fields
    
        private string m_filePathNameExt;
        private string m_outPathName;
        private bool m_debug;
        private bool m_verbose;
        private bool m_argsValid;
        private List<string> m_lines = new List<string>();
        private int m_exportIdx;
        private string m_x86NameSuffix;
        private string m_x64NameSuffix;
        private bool m_exportX86;
        private bool m_exportX64;
    
        #endregion Fields
    
        #region Initialization
    
        /// <summary>
        /// Constructor
        /// </summary>
        public Exporter()
        {
        }
    
        #endregion Initialization
    
        #region Properties
    
        /// <summary>
        /// Get just the file name without extension
        /// </summary>
        private string FileName
        {
          get { return Path.GetFileNameWithoutExtension(m_filePathNameExt); }
        }
    
        /// <summary>
        /// Get just the file extension
        /// </summary>
        private string FileExtention
        {
          get { return Path.GetExtension(m_filePathNameExt); }
        }
    
        /// <summary>
        /// Get the file name and extension
        /// </summary>
        private string FileNameAndExtention
        {
          get { return Path.GetFileName(m_filePathNameExt); }
        }
    
        /// <summary>
        /// Get the folder that contains the file
        /// </summary>
        private string FileFolder
        {
          get { return Path.GetDirectoryName(m_filePathNameExt); }
        }
    
        /// <summary>
        /// Get the path to the disassembler
        /// </summary>
        private string DisassemblerPath
        {
          get
          {
            var registryPath = @"SOFTWARE\Microsoft\Microsoft SDKs\Windows";
            var registryValue = "CurrentInstallFolder";
            var key = Registry.LocalMachine.OpenSubKey(registryPath) ?? Registry.CurrentUser.OpenSubKey(registryPath);
    
            if (key == null)
              throw new Exception("Cannot locate ildasm.exe.");
    
            var path = key.GetValue(registryValue) as string;
    
            if (path == null)
              throw new Exception("Cannot locate ildasm.exe.");
    
            path = Path.Combine(path, @"Bin\ildasm.exe");
    
            if (!File.Exists(path))
              throw new Exception("Cannot locate ildasm.exe.");
    
            return path;
          }
        }
    
        /// <summary>
        /// Get the path to the assembler
        /// </summary>
        private string AssemblerPath
        {
          get
          {
            var version = Environment.Version.Major.ToString() + "." +
                   Environment.Version.Minor.ToString() + "." +
                   Environment.Version.Build.ToString();
    
            var path = Environment.ExpandEnvironmentVariables(@"%SystemRoot%\Microsoft.NET\Framework\v" + version + @"\ilasm.exe");
    
            if (!File.Exists(path))
              throw new Exception("Cannot locate ilasm.exe.");
    
            return path;
          }
        }
    
        #endregion Properties
    
        #region Public Methods
    
        /// <summary>
        /// Run the conversion
        /// </summary>
        public int Execute()
        {
          // This will cause an Assert dialog to be shown, at which point the debugger
          // can be attached in order to debug this code
          //Debug.Assert(false, "Exporter", "Exporter");
    
          ProcessArguments();
    
          if (!m_argsValid)
          {
            // Show usage
            Console.WriteLine("usage: DllExport.exe assembly [/Release|/Debug] [/Verbose] [/Out:new_assembly]");
            return 1;
          }
    
          if (!File.Exists(m_filePathNameExt))
            throw new Exception("The input file does not exist: '" + m_filePathNameExt + "'");
    
          WriteInfo("DllExport Tool");
          WriteInfo("Debug: " + m_debug);
          WriteInfo("Input: '" + m_filePathNameExt + "'");
          WriteInfo("Output: '" + m_outPathName + "'");
    
          Console.WriteLine("");
    
          Disassemble();
          ReadLines();
          //SaveLines(@"C:\Temp\DllExport\Disassembled Original.il");
          ParseAllDllExport();
    
          // 32-bit
          if (m_exportX86)
          {
            FixCorFlags(Platform.x86);
            Assemble(Platform.x86);
            //SaveLines(@"C:\Temp\DllExport\Disassembled x86.il");
          }
    
          // 64-bit
          if (m_exportX64)
          {
            FixCorFlags(Platform.x64);
            Assemble(Platform.x64);
            //SaveLines(@"C:\Temp\DllExport\Disassembled x64.il");
          }
    
          int exportCount = m_exportIdx - 1;
    
          Console.WriteLine("DllExport: Exported " + exportCount +
            (exportCount == 1 ? " function" : " functions"));
    
          Console.WriteLine();
    
          return 0;
        }
    
        #endregion Public Methods
    
        #region Private, Protected Methods
    
        /// <summary>
        /// Parse the arguments
        /// </summary>
        private void ProcessArguments()
        {
          m_debug = false;
          m_verbose = false;
          m_filePathNameExt = null;
          m_outPathName = null;
          m_x86NameSuffix = null;
          m_x64NameSuffix = null;
          m_exportX86 = false;
          m_exportX64 = false;
    
          string[] args = Environment.GetCommandLineArgs();
    
          for (int idx = 1; idx < args.Length; idx++)
          {
            string argLower = args[idx].ToLower();
    
            if (argLower.StartsWith("/name32:"))
            {
              m_exportX86 = true;
              m_x86NameSuffix = args[idx].Substring(8).Trim("\"".ToCharArray());
            }
            else if (argLower.StartsWith("/name64:"))
            {
              m_exportX64 = true;
              m_x64NameSuffix = args[idx].Substring(8).Trim("\"".ToCharArray());
            }
            else if (argLower == "/debug")
            {
              m_debug = true;
            }
            else if (argLower == "/verbose")
            {
              m_verbose = true;
            }
            else if (argLower.StartsWith("/input:"))
            {
              m_filePathNameExt = args[idx].Substring(7).Trim();
            }
            else if (argLower.StartsWith("/output:"))
            {
              m_outPathName = args[idx].Substring(8).Trim();
            }
          }
    
          if (!m_exportX86 && !m_exportX64)
            m_exportX86 = true;
    
          string path = Path.GetDirectoryName(m_filePathNameExt);
    
          if (!File.Exists(m_filePathNameExt) || this.FileFolder == string.Empty)
          {
            m_filePathNameExt = Path.Combine(Directory.GetCurrentDirectory(), m_filePathNameExt);
    
            if (!File.Exists(m_filePathNameExt))
              throw new Exception("The input file does not exist: '" + m_filePathNameExt + "'");
          }
    
          if (string.IsNullOrEmpty(m_outPathName))
            m_outPathName = m_filePathNameExt;
    
          m_argsValid = !string.IsNullOrEmpty(m_filePathNameExt);
        }
    
        /// <summary>
        /// Disassemble the input file
        /// </summary>
        private void Disassemble()
        {
          m_exportIdx = 1;
          System.IO.Directory.SetCurrentDirectory(this.FileFolder);
          Process proc = new Process();
    
          // Must specify the /caverbal switch in order to get the custom attribute 
          // values as text and not as binary blobs
          string arguments = string.Format("/nobar{1}/out:\"{0}.il\" \"{0}.dll\"", this.FileName, " /linenum /caverbal ");
    
          WriteInfo("Disassemble file with arguments '" + arguments + "'");
    
          ProcessStartInfo info = new ProcessStartInfo(this.DisassemblerPath, arguments);
    
          info.UseShellExecute = false;
          info.CreateNoWindow = false;
          info.RedirectStandardOutput = true;
          proc.StartInfo = info;
    
          try
          {
            proc.Start();
          }
          catch (Win32Exception e)
          {
            bool handled = false;
    
            if (e.NativeErrorCode == 3)
            {
              // try to check wow64 program files
              string fn = info.FileName;
    
              if (fn.Substring(1, 16).ToLower() == @":\program files\")
              {
                info.FileName = fn.Insert(16, " (x86)");
                handled = true;
                proc.Start();
              }
            }
            if (!handled)
              throw (e);
          }
    
          proc.WaitForExit();
    
          if (proc.ExitCode != 0)
          {
            WriteError(proc.StandardOutput.ReadToEnd());
            throw new Exception("Could not Disassemble: Error code '" + proc.ExitCode + "'");
          }
        }
    
        /// <summary>
        /// Read all the lines from the disassembled IL file
        /// </summary>
        private void ReadLines()
        {
          m_lines.Clear();
    
          if (string.IsNullOrEmpty(m_filePathNameExt))
            throw new Exception("The input file could not be found");
    
          string ilFile = Path.Combine(this.FileFolder, this.FileName + ".il");
    
          if (!File.Exists(ilFile))
            throw new Exception("The disassembled IL file could not be found");
    
          StreamReader sr = File.OpenText(ilFile);
    
          while (!sr.EndOfStream)
          {
            string line = sr.ReadLine();
            m_lines.Add(line);
          }
    
          sr.Close();
          sr.Dispose();
        }
    
        /// <summary>
        /// Save the current lines to the specified file
        /// </summary>
        /// <param name="p"></param>
        private void SaveLines(string fileName)
        {
          try
          {
            var folder = Path.GetDirectoryName(fileName);
    
            if (!Directory.Exists(folder))
              Directory.CreateDirectory(folder);
    
            var fileStream = File.CreateText(fileName);
    
            foreach (string line in m_lines)
              fileStream.WriteLine(line);
    
            fileStream.Close();
          }
          catch (System.Exception ex)
          {
          }
        }
    
        /// <summary>
        /// Fix the Cor flags
        /// </summary>
        private void FixCorFlags(Platform platform)
        {
          for (int idx = 0; idx < m_lines.Count; idx++)
          {
            if (m_lines[idx].StartsWith(".corflags"))
            {
              switch (platform)
              {
                case Platform.x86:
                  m_lines[idx] = ".corflags 0x00000002  // 32BITREQUIRED";
                  break;
    
                case Platform.x64:
                  m_lines[idx] = ".corflags 0x00000008  // 64BITREQUIRED";
                  break;
              }
              break;
            }
          }
        }
    
        /// <summary>
        /// Parse all DllExport entries
        /// </summary>
        private void ParseAllDllExport()
        {
          string findString = ".custom instance void DllExport";
    
          int dllExportIdx = FindLineStartsWith(findString, true, -1, -1);
    
          while (dllExportIdx >= 0)
          {
            ParseDllExport(dllExportIdx);
            dllExportIdx = FindLineStartsWith(findString, true, dllExportIdx + 1, -1);
          }
        }
    
        /// <summary>
        /// Parse the DllExport entry
        /// </summary>
        /// <param name="dllExportIdx"></param>
        private void ParseDllExport(int dllExportIdx)
        {
          int exportNameIdx = FindLineContains("string('", true, dllExportIdx, dllExportIdx + 5);
          int calConvIdx = FindLineContains("int32(", true, dllExportIdx, dllExportIdx + 5);
          string exportName = null;
          int startIdx = 0;
          int endIdx = 0;
    
          if (calConvIdx < 0)
            throw new Exception("Could not find Calling Convention for line " + dllExportIdx.ToString());
    
          if (exportNameIdx >= 0)
          {
            startIdx = m_lines[exportNameIdx].IndexOf("('");
            endIdx = m_lines[exportNameIdx].IndexOf("')");
    
            if (startIdx >= 0 && endIdx >= 0)
              exportName = m_lines[exportNameIdx].Substring(startIdx + 2, endIdx - startIdx - 2);
          }
    
          startIdx = m_lines[calConvIdx].IndexOf("int32(");
          endIdx = m_lines[calConvIdx].IndexOf(")");
    
          if (startIdx < 0 || endIdx < 0)
            throw new Exception("Could not find Calling Convention for line " + dllExportIdx.ToString());
    
          string calConvText = m_lines[calConvIdx].Substring(startIdx + 6, endIdx - startIdx - 6);
          int calConvValue = 0;
    
          if (!int.TryParse(calConvText, out calConvValue))
            throw new Exception("Could not parse Calling Convention for line " + dllExportIdx.ToString());
    
          CallingConvention callConv = (CallingConvention)calConvValue;
    
          int endDllExport = FindLineContains("}", true, calConvIdx, calConvIdx + 10);
    
          if (endDllExport < 0)
            throw new Exception("Could not find end of Calling Convention for line " + dllExportIdx.ToString());
    
          // Remove the DllExport lines
          while (endDllExport >= dllExportIdx)
            m_lines.RemoveAt(endDllExport--);
    
          int insertIdx = FindLineStartsWith(".maxstack", true, dllExportIdx, dllExportIdx + 20);
    
          if (insertIdx < 0)
            throw new Exception("Could not find '.maxstack' insert location for line " + dllExportIdx.ToString());
    
          int tabs = m_lines[insertIdx].IndexOf(".");
    
          string exportText = TabString(tabs) + ".export [" + (m_exportIdx++).ToString() + "]";
    
          if (!string.IsNullOrEmpty(exportName))
            exportText += " as " + exportName;
    
          m_lines.Insert(insertIdx, exportText);
    
          string methodName = UpdateMethodCalConv(FindLineStartsWith(".method", false, insertIdx - 1, -1), callConv);
    
          if (!string.IsNullOrEmpty(methodName))
          {
            if (!string.IsNullOrEmpty(exportName))
              Console.WriteLine("Exported '" + methodName + "' as '" + exportName + "'");
            else
              Console.WriteLine("Exported '" + methodName + "'");
          }
        }
    
        /// <summary>
        /// Update the method's calling convention
        /// </summary>
        /// <param name="methodIdx"></param>
        /// <param name="callConv"></param>
        private string UpdateMethodCalConv(int methodIdx, CallingConvention callConv)
        {
          if (methodIdx < 0 || FindLineStartsWith(".method", true, methodIdx, methodIdx) != methodIdx)
            throw new Exception("Invalid method index: " + methodIdx.ToString());
    
          int endIdx = FindLineStartsWith("{", true, methodIdx, -1);
    
          if (endIdx < 0)
            throw new Exception("Could not find method open brace location for line " + methodIdx.ToString());
    
          endIdx--;
          int insertLine = -1;
          int insertCol = -1;
          string methodName = null;
    
          for (int idx = methodIdx; idx <= endIdx; idx++)
          {
            int marshalIdx = m_lines[idx].IndexOf("marshal(");
    
            if (marshalIdx >= 0)
            {
              // Must be inserted before the "marshal(" entry
              insertLine = idx;
              insertCol = marshalIdx;
              break;
            }
            else
            {
              int openBraceIdx = m_lines[idx].IndexOf('(');
    
              while (openBraceIdx >= 0 && insertLine < 0 && insertCol < 0)
              {
                int spaceIdx = m_lines[idx].LastIndexOf(' ', openBraceIdx);
    
                if (spaceIdx >= 0)
                {
                  string findMethodName = m_lines[idx].Substring(spaceIdx + 1, openBraceIdx - spaceIdx - 1);
    
                  // The method name is anything but "marshal"
                  if (findMethodName != "marshal")
                  {
                    insertLine = idx;
                    insertCol = spaceIdx + 1;
                    methodName = findMethodName;
                    break;
                  }
    
                  openBraceIdx = m_lines[idx].IndexOf('(', openBraceIdx + 1);
                }
              }
            }
    
            if (methodIdx >= 0 && insertCol >= 0)
              break;
          }
    
          if (insertLine < 0 || insertCol < 0)
            throw new Exception("Could not find method name for line " + methodIdx.ToString());
    
          string leftText = m_lines[insertLine].Substring(0, insertCol);
          string rightText = m_lines[insertLine].Substring(insertCol);
          string callConvText = "modopt([mscorlib]";
    
          switch (callConv)
          {
            case System.Runtime.InteropServices.CallingConvention.Cdecl:
              callConvText += typeof(CallConvCdecl).FullName + ") ";
              break;
    
            case System.Runtime.InteropServices.CallingConvention.FastCall:
              callConvText += typeof(CallConvFastcall).FullName + ") ";
              break;
    
            case System.Runtime.InteropServices.CallingConvention.StdCall:
              callConvText += typeof(CallConvStdcall).FullName + ") ";
              break;
    
            case System.Runtime.InteropServices.CallingConvention.ThisCall:
              callConvText += typeof(CallConvThiscall).FullName + ") ";
              break;
    
            case System.Runtime.InteropServices.CallingConvention.Winapi:
              callConvText += typeof(CallConvStdcall).FullName + ") ";
              break;
    
            default:
              throw new Exception("Invalid calling convention specified: '" + callConv.ToString() + "'");
          }
    
          m_lines[insertLine] = leftText + callConvText + rightText;
          return methodName;
        }
    
        /// <summary>
        /// Assemble the destination file
        /// </summary>
        private void Assemble(Platform platform)
        {
          StreamWriter sw = File.CreateText(Path.Combine(this.FileFolder, this.FileName + ".il"));
    
          foreach (string line in m_lines)
            sw.WriteLine(line);
    
          sw.Close();
          sw.Dispose();
    
          string resFile = Path.Combine(this.FileFolder, this.FileName + ".res");
          string res = "\"" + resFile + "\"";
    
          if (File.Exists(resFile))
            res = " /resource=" + res;
          else
            res = "";
    
          Process proc = new Process();
          string extension = Path.GetExtension(m_filePathNameExt);
          string outFile = Path.Combine(Path.GetDirectoryName(m_outPathName),
            Path.GetFileNameWithoutExtension(m_outPathName));
    
          switch (platform)
          {
            case Platform.x86:
              if (!string.IsNullOrEmpty(m_x86NameSuffix))
                outFile += m_x86NameSuffix;
              break;
    
            case Platform.x64:
              if (!string.IsNullOrEmpty(m_x64NameSuffix))
                outFile += m_x64NameSuffix;
    
              break;
          }
    
          if (extension == string.Empty)
            extension = ".dll";
    
          outFile += extension;
    
          string argOptions = "/nologo /quiet /DLL";
          string argIl = "\"" + Path.Combine(this.FileFolder, this.FileName) + ".il\"";
          string argOut = "/out:\"" + outFile + "\"";
    
          if (m_debug)
            argOptions += " /debug /pdb";
          else
            argOptions += " /optimize";
    
          if (platform == Platform.x64)
            argOptions += " /x64";
    
          string arguments = argOptions + " " + argIl + " " + res + " " + argOut;
    
          WriteInfo("Compiling file with arguments '" + arguments + "'");
    
          ProcessStartInfo info = new ProcessStartInfo(this.AssemblerPath, arguments);
          info.UseShellExecute = false;
          info.CreateNoWindow = false;
          info.RedirectStandardOutput = true;
          proc.StartInfo = info;
          proc.Start();
          proc.WaitForExit();
    
          WriteInfo(proc.StandardOutput.ReadToEnd());
    
          if (proc.ExitCode != 0)
            throw new Exception("Could not Assemble: Error code '" + proc.ExitCode + "'");
        }
    
        /// <summary>
        /// Find the next line that starts with the specified text, ignoring leading whitespace
        /// </summary>
        /// <param name="findText"></param>
        /// <param name="startIdx"></param>
        /// <param name="endIdx"></param>
        /// <returns></returns>
        private int FindLineStartsWith(string findText, bool forward, int startIdx, int endIdx)
        {
          if (forward)
          {
            if (startIdx < 0)
              startIdx = 0;
    
            if (endIdx < 0)
              endIdx = m_lines.Count - 1;
            else
              endIdx = Math.Min(endIdx, m_lines.Count - 1);
    
            for (int idx = startIdx; idx <= endIdx; idx++)
            {
              if (m_lines[idx].Contains(findText) && m_lines[idx].Trim().StartsWith(findText))
                return idx;
            }
          }
          else
          {
            if (startIdx < 0)
              startIdx = m_lines.Count - 1;
    
            if (endIdx < 0)
              endIdx = 0;
    
            for (int idx = startIdx; idx >= endIdx; idx--)
            {
              if (m_lines[idx].Contains(findText) && m_lines[idx].Trim().StartsWith(findText))
                return idx;
            }
          }
    
          return -1;
        }
    
        /// <summary>
        /// Find the line that contains the specified text
        /// </summary>
        /// <param name="findText"></param>
        /// <param name="startIdx"></param>
        /// <param name="endIdx"></param>
        /// <returns></returns>
        private int FindLineContains(string findText, bool forward, int startIdx, int endIdx)
        {
          if (forward)
          {
            if (startIdx < 0)
              startIdx = 0;
    
            if (endIdx < 0)
              endIdx = m_lines.Count - 1;
            else
              endIdx = Math.Min(endIdx, m_lines.Count - 1);
    
            for (int idx = startIdx; idx < endIdx; idx++)
            {
              if (m_lines[idx].Contains(findText))
                return idx;
            }
          }
          else
          {
            if (startIdx < 0)
              startIdx = m_lines.Count - 1;
    
            if (endIdx < 0)
              endIdx = 0;
    
            for (int idx = startIdx; idx >= endIdx; idx--)
            {
              if (m_lines[idx].Contains(findText))
                return idx;
            }
          }
    
          return -1;
        }
    
        /// <summary>
        /// Get a string padded with the number of spaces
        /// </summary>
        /// <param name="tabCount"></param>
        /// <returns></returns>
        private string TabString(int tabCount)
        {
          if (tabCount <= 0)
            return string.Empty;
    
          StringBuilder sb = new StringBuilder();
    
          sb.Append(' ', tabCount);
          return sb.ToString();
        }
    
        /// <summary>
        /// Write an informational message
        /// </summary>
        /// <param name="info"></param>
        private void WriteInfo(string info)
        {
          if (m_verbose)
            Console.WriteLine(info);
        }
    
        /// <summary>
        /// Write an informational message
        /// </summary>
        /// <param name="info"></param>
        private void WriteError(string error)
        {
          Console.WriteLine(error);
        }
    
        #endregion Private, Protected Methods
      }
    }
    

     

    Here is the DllExport attribute that you tag a method with:

     

    using System;
    using System.Collections.Generic;
    using System.Text;
    using System.Runtime.CompilerServices;
    using System.Runtime.InteropServices;
    
    namespace DllExport
    {
      /// <summary>
      /// Attribute added to a static C# method to export it
      /// </summary>
      [AttributeUsage(AttributeTargets.Method)]
      public class DllExportAttribute : Attribute
      {
        private string m_exportName;
        private CallingConvention m_callingConvention;
    
        /// <summary>
        /// Constructor 1
        /// </summary>
        /// <param name="exportName"></param>
        public DllExportAttribute(string exportName)
          : this(exportName, System.Runtime.InteropServices.CallingConvention.StdCall)
        {
        }
    
        /// <summary>
        /// Constructor 2
        /// </summary>
        /// <param name="exportName"></param>
        /// <param name="callingConvention"></param>
        public DllExportAttribute(string exportName, CallingConvention callingConvention)
        {
          m_exportName = exportName;
          m_callingConvention = callingConvention;
        }
    
        /// <summary>
        /// Get the export name, or null to use the method name
        /// </summary>
        public string ExportName
        {
          get { return m_exportName; }
        }
    
        /// <summary>
        /// Get the calling convention
        /// </summary>
        public string CallingConvention
        {
          get
          {
            switch (m_callingConvention)
            {
              case System.Runtime.InteropServices.CallingConvention.Cdecl:
                return typeof(CallConvCdecl).FullName;
    
              case System.Runtime.InteropServices.CallingConvention.FastCall:
                return typeof(CallConvFastcall).FullName;
    
              case System.Runtime.InteropServices.CallingConvention.StdCall:
                return typeof(CallConvStdcall).FullName;
    
              case System.Runtime.InteropServices.CallingConvention.ThisCall:
                return typeof(CallConvThiscall).FullName;
    
              case System.Runtime.InteropServices.CallingConvention.Winapi:
                return typeof(CallConvStdcall).FullName;
    
              default:
                return "";
            }
          }
        }
      }
    }
    
    Tuesday, November 23, 2010 8:19 AM
  • The correct question is "has anything been added to C# 4.0 to support C-style exports?" Because the .NET platform always had this feature. It's only exposed via C++/CLI and ilasm though. From ilasm 2.0 onwards you can have it auto-compute the export vtables, and it even determines the corflags correctly depending on whether you compile for x86 or x64. All you need in the source is something like

    // Compile with : ilasm unm.il /dll
    .assembly extern mscorlib { auto }
    .assembly Unm {}
    .module Unm.dll
    .method public static void foo()
    {
        .export [1] as foo
        ldstr "Hello from managed world"
        call void [mscorlib]System.Console::WriteLine(string)
        ret
    }

    The keyword here is ".export". It exposes "foo" as native C-style dll export and the thunking is automatically generated! Unfortunately, C# 4.0 still doesn't have any language-level support for this type of exports. C++/CLI unfortunately generates a lot of bloat because it exposes all your C hearders as .NET types, and even its built-in interop is about 20Kb in size. Just compile something like "class {} x;" with cl /clr and be amazed at the amount of gunk that goes in. Unfortunently clean and simple interops are not the Microsoft way.

     

     

    • Proposed as answer by Barton_C Tuesday, January 08, 2013 6:59 AM
    Thursday, December 22, 2011 12:26 PM

  • Thanks, I didn't realize that there was an MSIL syntax for this.  (Probably because of my lack of knowledge of using the CLI from C++.)  There are a number of things you can do from C++/CLI for which C# could provide syntax but does not.

     

     

     


    It's not part of MSIL! It's ilasm-specific syntax using the same core mechansim that C++/CLI for C++ interop generating native thunking code.
    Thursday, December 22, 2011 12:33 PM
  • Indeed, since security has de facto been removed from the CLR (what users or administrators do really verify an application for unmanaged DLLs and set security permissions accordingly?) it is rather overdue to introduce an DllExportAttribute anyway. We are looking forward to this for some time now. However, security could return to the CLR if unmanaged functions referenced from managed code could be strongly linked (similarly to strong named assemblies), for example by a hash of a DLL or by auto-checking a DLL's digital signature. Sure, one can already do it manually, but that would be a homemade solution and not future-proof.

    By the way, not only the C++/CLI compiler produces unneccessary bloat, the C# compiler does this as well only less. Though, the most annyoing thing about the C# compiler is that even after 10 years (!) and its fourth incarnation the compiler's optimization strength at IL level is still rather marginal. The optimizations are focused more on space than speed. There is virtually no optimization for such a simple thing as counting or indexing variables. And, the JIT is not able to optimize them either. Even straightforward optimizations done to the IL code manually regarding counting and index variables provide 20%+ speed. Well, but this is another story.

    Monday, January 02, 2012 5:20 PM
  • And here we are in 2013 and still no DllExportAttribute in the class libraries.

    At this rate Mono (via Cecil) will support [DllExport]

    Discussed around the same time as this post

    before Visual Studio does.

    • Proposed as answer by Barton_C Tuesday, January 08, 2013 6:58 AM
    • Unproposed as answer by Barton_C Tuesday, January 08, 2013 6:58 AM
    Tuesday, January 08, 2013 6:47 AM
  • Well Miguel de Icaza did say "...we love C# and .Net more than Microsoft does".

    Just more proof of this.

    Tuesday, January 08, 2013 6:30 PM