创建一个C# DLL并从非托管的C++中使用它

14

我有一个本地(非托管)的C++应用程序(使用wxWidgets)。我考虑编写一个用C#编写的单独工具应用程序,其中包含基于Winform的对话框。将其中一些对话框放在单独的DLL中将很有用,因为我希望能够从我的C++应用程序中使用它们。

但是我不知道需要多少操作才能完成这个目标,这个过程会特别容易吗?

编辑

我不需要直接调用对话框函数。我只需要一种方法,让我的C++应用程序调用C# DLL中的API以传递数据,以及让C# DLL调用C++应用程序中某种观察者/后端对象的方法。

例如,从C++:

CSharpManager *pCSM = SomehowGetPointerToCSharpObject();
CSharpObserver pCSO = new CSharpObserver;

pCSM->RegisterCPlusPlusObserver(pCSO);
pCSM->LaunchDialog();

用户在C#对话框中执行操作时,会调用pCSO方法将数据传递回C++。

因此,我认为这基本上是一个关于C++/C#通信的问题。虽然我知道C++和C#,但我不知道.net本身是如何工作的。我知道COM,但真的很想避免它,我怀疑与我合作的其他开发人员是否都知道它。


1
从C#类库中导出函数: https://dev59.com/E0zSa4cB1Zd3GeqPnIGx - Andras Vass
如果您不想涉足 COM,我建议像其他人一样使用托管 C++ 将托管类包装起来。另一个线程专门讨论了这个问题:https://dev59.com/6mLVa4cB1Zd3GeqPtiVr - Peter Ritchie
8个回答

7

在非托管代码中,互操作的通用语言是COM。在C#方面很容易开始,只需使用[ComVisible] 属性。您需要在C++程序中编写COM代码来使用它,如果您从未这样做过,那么这并不容易。如果您使用MSVC编译器,请从#import指令开始。

您的下一个选择是自己在非托管代码中托管CLR,而不是依赖于COM互操作层来处理它。它允许您直接创建托管类型。这也需要使用COM,但仅用于加载和初始化CLR。此项目展示了这种方法。


呵呵,“通用语言”。确实如此。+1 - user2189331
1
不需要使用COM,因此如果C++应用程序尚未使用它,则不建议开始使用。当然,在这种情况下,回调到C++代码变成了传递状态对象指针作为参数的全局函数的调用,而不是状态对象的成员函数的调用,但这样编写(和调试)起来要容易得多 - 如果由于p/invoke不匹配而导致数据被覆盖,至少您仍然在调用正确的代码,即使数据已损坏,也不像使用COM那样依赖于有效的v-table指针进行函数调用。 - Ben Voigt
1
是的,还有呢?因为.NET程序集依赖于mscoree.dll或其相关文件,所以CLR将自动加载。当我第一次发现这一点时感到非常奇怪,但纯管理代码确实可以具有本地导出函数。同时,本地EXE文件也可以为其DLL文件导出函数供其使用。 - Ben Voigt
2
@Ben:请在本帖中发布您自己的示例代码,我无法理解您的评论。托管程序集包含数据,而不是代码。IL和元数据。您可以让C++/CLI编译器生成具有DLL导出的thunk,这是我知道的唯一技巧。这距离导出类(特别是C#类)还有很长的路要走。 - Hans Passant
1
@nobugz:发布了各种示例代码(包括IL和元数据),这些代码都有本地导出。当然,它们导出的是函数而不是类,所有本地导出都是函数或全局数据。但导出类绝对不是实现OP目标所必需的。当.NET程序集被加载时,.NET运行时会运行,纯粹通过静态依赖项加载过程,因为.NET程序集从mscorlib.dll(抱歉不是mscoree)中导入。因此,mscorlib的DllMain首先运行,并JITs所有导出,因此在LoadLibrary返回时就有本机代码了。 - Ben Voigt
显示剩余2条评论

2

您可以使用COM,或编写一个C++/CLI包装器来调用您的C#对话框,然后从您的非托管C++代码中调用此C++/CLI包装器。


1

我知道这里有一些答案,但是它们都没有指向一个可用的示例。当我遇到这个问题时,我能够通过这个示例找出解决方法。

http://support.microsoft.com/kb/828736


1

这取决于你所说的“我希望能够从我的C++应用程序中使用它们”的意思。

在本地环境中,对话框具有对话框模板结构,您可以将其“烹制”到您的可执行文件中,无论是DLL还是EXE。很好。但在托管世界中,情况有些不同。Winforms应用程序没有“对话框模板”资源类型。相反,表单只是代码。

然而:

  • 您始终可以从非托管代码调用托管DLL。这很简单。该托管DLL可以显示您的Winforms对话框。因此,您的应用程序的本机C++部分可以调用这些对话框。但是,如果没有一些额外的工作,它不能直接实例化它们。

  • 您始终可以在本机C++代码和托管DLL之间插入一个C++/CLI“shim DLL”。在C++/CLI中,您可以透明地加载托管和.NET资源/对话框。

  • 就此而言,您可以直接从本机代码调用.NET方法,而无需中介的C++/CLI shim DLL,尽管有点混乱。

但是,关于直接使用“.NET/Winforms对话框资源”……不行。不能像在Winforms和本地C++中同时使用相同的对话框模板。


我不需要直接调用对话框函数。 我只需要一种方法让我的C ++应用程序调用C#DLL中的API以传递数据,以及一种让C#DLL调用C ++应用程序中某种观察者/后台对象的方法的方式。 - Mr. Boy


0

在从C++调用的C# DLL中使用表单并不容易,但是一旦编写了一些实用程序代码,它就可以相当健壮。回调到C++代码非常容易。

要使用表单(或WPF),NativeWindow类是您的朋友。您希望获得比NativeWindow更多的功能,因此需要派生。下面的代码显示了一个从NativeWindow派生并提供BeginInvoke()调用和窗口消息事件处理程序的实现。

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Runtime.InteropServices;
using System.Windows.Forms;

/// <summary>
/// A <see cref="NativeWindow"/> for the main application window. Used
/// to be able to run things on the UI thread and manage window message
/// callbacks.
/// </summary>
public class NativeWindowWithCallbacks : NativeWindow, IDisposable
{
    /// <summary>
    /// Used to synchronize access to <see cref="NativeWindow.Handle"/>.
    /// </summary>
    private readonly object handleLock = new object();

    /// <summary>
    /// Queue of methods to run on the UI thread.
    /// </summary>
    private readonly Queue<MethodArgs> queue = new Queue<MethodArgs>();

    /// <summary>
    /// The message handlers.
    /// </summary>
    private readonly Dictionary<int, MessageHandler> messageHandlers = 
        new Dictionary<int, MessageHandler>();

    /// <summary>
    /// Windows message number to prompt running methods on the UI thread.
    /// </summary>
    private readonly int runOnUiThreadWindowsMessageNumber =
        Win32.RegisterWindowMessage(
                "NativeWindowWithCallbacksInvokeOnGuiThread");

    /// <summary>
    /// Handles the message.
    /// </summary>
    /// <param name="sender">
    /// The this.
    /// </param>
    /// <param name="m">
    /// The message.
    /// </param>
    /// <returns>
    /// True if done processing; false otherwise. Normally, returning
    /// true will stop other handlers from being called, but, for
    /// some messages (like WM_DESTROY), the return value has no effect.
    /// </returns>
    public delegate bool MessageHandler(object sender, ref Message m);

    /// <summary>
    /// Gets a value indicating whether the caller must call BeginInvoke
    /// when making UI calls (like <see cref="Control.InvokeRequired"/>).
    /// </summary>
    /// <returns>
    /// True if not running on the UI thread.
    /// </returns>
    /// <remarks>
    /// This can get called prior to detecting the main window (likely if 
    /// the main window has yet to be created). In this case, this method
    /// will return true even if the main window subsequently gets
    /// created on the current thread. This behavior works for queuing up
    /// methods that will update the main window which is likely the only 
    /// reason for invoking methods on the UI thread anyway.
    /// </remarks>
    public bool InvokeRequired
    {
        get
        {
            int pid;
            return this.Handle != IntPtr.Zero
                && Win32.GetWindowThreadProcessId(
                        new HandleRef(this, this.Handle), out pid)
                != Win32.GetCurrentThreadId();
        }
    }

    /// <summary>
    /// Like <see cref="Control.BeginInvoke(Delegate,Object[])"/> but
    /// probably not as good.
    /// </summary>
    /// <param name="method">
    /// The method.
    /// </param>
    /// <param name="args">
    /// The arguments.
    /// </param>
    /// <remarks>
    /// This can get called prior to finding the main window (likely if 
    /// the main window has yet to be created). In this case, the method 
    /// will get queued and called upon detection of the main window.
    /// </remarks>
    public void BeginInvoke(Delegate method, params object[] args)
    {
        // TODO: ExecutionContext ec = ExecutionContext.Capture();
        // TODO: then ExecutionContext.Run(ec, ...) 
        // TODO: in WndProc for more accurate security
        lock (this.queue)
        {
            this.queue.Enqueue(
                new MethodArgs { Method = method, Args = args });
        }

        if (this.Handle != IntPtr.Zero)
        {
            Win32.PostMessage(
                    new HandleRef(this, this.Handle),
                    this.runOnUiThreadWindowsMessageNumber,
                    IntPtr.Zero,
                    IntPtr.Zero);
        }
    }

    /// <summary>
    /// Returns the handle of the main window menu.
    /// </summary>
    /// <returns>
    /// The handle of the main window menu; Handle <see cref="IntPtr.Zero"/>
    /// on failure.
    /// </returns>
    public HandleRef MenuHandle()
    {
        return new HandleRef(
                this,
                this.Handle != IntPtr.Zero
                    ? Win32.GetMenu(new HandleRef(this, this.Handle))
                    : IntPtr.Zero);
    }

    /// <summary>
    /// When the instance gets disposed.
    /// </summary>
    public void Dispose()
    {
        this.ReleaseHandle();
    }

    /// <summary>
    /// Sets the handle.
    /// </summary>
    /// <param name="handle">
    ///   The handle.
    /// </param>
    /// <param name="onlyIfNotSet">
    /// If true, will not assign to an already assigned handle.
    /// </param>
    public void AssignHandle(IntPtr handle, bool onlyIfNotSet)
    {
        bool emptyBacklog = false;
        lock (this.handleLock)
        {
            if (this.Handle != handle
                    && (!onlyIfNotSet || this.Handle != IntPtr.Zero))
            {
                base.AssignHandle(handle);
                emptyBacklog = true;
            }
        }

        if (emptyBacklog)
        {
            this.EmptyUiBacklog();
        }
    }

    /// <summary>
    /// Adds a message handler for the given message number.
    /// </summary>
    /// <param name="messageNumber">
    /// The message number.
    /// </param>
    /// <param name="messageHandler">
    /// The message handler.
    /// </param>
    public void AddMessageHandler(
        int messageNumber,
        MessageHandler messageHandler)
    {
        lock (this.messageHandlers)
        {
            if (this.messageHandlers.ContainsKey(messageNumber))
            {
                this.messageHandlers[messageNumber] += messageHandler;
            }
            else
            {
                this.messageHandlers.Add(
                        messageNumber, (MessageHandler)messageHandler.Clone());
            }
        }
    }

    /// <summary>
    /// Processes the window messages.
    /// </summary>
    /// <param name="m">
    /// The m.
    /// </param>
    protected override void WndProc(ref Message m)
    {
        if (m.Msg == this.runOnUiThreadWindowsMessageNumber && m.Msg != 0)
        {
            for (;;)
            {
                MethodArgs ma;
                lock (this.queue)
                {
                    if (!this.queue.Any())
                    {
                        break;
                    }

                    ma = this.queue.Dequeue();
                }

                ma.Method.DynamicInvoke(ma.Args);
            }

            return;
        }

        int messageNumber = m.Msg;
        MessageHandler mh;
        if (this.messageHandlers.TryGetValue(messageNumber, out mh))
        {
            if (mh != null)
            {
                foreach (MessageHandler cb in mh.GetInvocationList())
                {
                    try
                    {
                        // if WM_DESTROY (messageNumber == 2),
                        // ignore return value
                        if (cb(this, ref m) && messageNumber != 2)
                        {
                            return; // done processing
                        }
                    }
                    catch (Exception ex)
                    {
                        Debug.WriteLine(string.Format("{0}", ex));
                    }
                }
            }
        }

        base.WndProc(ref m);
    }

    /// <summary>
    /// Empty any existing backlog of things to run on the user interface
    /// thread.
    /// </summary>
    private void EmptyUiBacklog()
    {
        // Check to see if there is a backlog of
        // methods to run on the UI thread. If there
        // is than notify the UI thread about them.
        bool haveBacklog;
        lock (this.queue)
        {
            haveBacklog = this.queue.Any();
        }

        if (haveBacklog)
        {
            Win32.PostMessage(
                    new HandleRef(this, this.Handle),
                    this.runOnUiThreadWindowsMessageNumber,
                    IntPtr.Zero,
                    IntPtr.Zero);
        }
    }

    /// <summary>
    /// Holds a method and its arguments.
    /// </summary>
    private class MethodArgs
    {
        /// <summary>
        /// Gets or sets the method arguments.
        /// </summary>
        public object[] Args { get; set; }

        /// <summary>
        /// Gets or sets Method.
        /// </summary>
        public Delegate Method { get; set; }
    }
}

以上代码的主要目的是获取在内部实现的BeginInvoke()调用 - 您需要该调用来在GUI线程上创建自己的窗体。但是,在您可以在GUI线程上回调之前,您需要拥有一个窗口句柄。最简单的方法是让C++代码传递窗口句柄(作为IntPtr到达),但您也可以使用类似以下内容的东西:

Process.GetCurrentProcess().MainWindowHandle;

即使在从C++调用C#时,也可以获取到主窗口的句柄。请注意,C++代码可能会更改主窗口句柄并留下无效的C#代码(当然,可以通过监听原始句柄上的适当窗口消息来捕获此问题 - 您也可以使用上面的代码执行此操作)。

很抱歉,上面的Win32调用声明未显示。您可以通过搜索网络获取它们的P/Invoke声明。(我的Win32类非常庞大。)

至于回调到C++代码-只要使回调相当简单,您就可以使用Marshal.GetDelegateForFunctionPointer将传递的函数指针(转换为IntPtr)转换为常规的C#委托。

因此,至少回调到C++非常容易(只要正确定义委托声明)。例如,如果您有一个C++函数,该函数接受char const *并返回void,则委托声明将如下所示:

public delegate void MyCallback([MarshalAs(UnmanagedType.LPStr)] string myText);

这涵盖了基础知识。使用上述类和传入的窗口句柄,在NativeWindowWithCallbacks.BeginInvoke()调用中创建自己的基于表单的窗口。现在,如果您想玩C++窗口代码,例如在C++代码管理的窗口中添加菜单项条目,事情会变得更加复杂。 .Net控件代码不喜欢与其未创建的任何窗口进行接口交互。因此,要添加菜单项,您最终需要编写具有大量Win32 P / Invokes的代码,以执行与编写C代码时相同的调用。上述的NativeWindowWithCallbacks类将再次派上用场。


0

如果您想在C++应用程序中加载任何.NET DLL,则必须在C++应用程序中托管.NET。

您可以在此处找到Microsoft的示例: https://code.msdn.microsoft.com/CppHostCLR-e6581ee0 该示例还包括一些必需的头文件。

简而言之,您需要执行以下操作:

  1. 使用LoadLibrary命令加载mscoree.dll(否则,您可以将mscoree.dll静态链接到项目中)
  2. 调用由mscoree.dll导出的CLRCreateInstance函数,以创建ICLRMetaHost对象
  3. 调用ICLRMetaHost对象的GetRuntime方法,以获取首选.NET版本的ICLRRuntimeInfo对象。
  4. 调用ICLRRuntimeInfo.IsLoadable检查版本是否可加载
  5. 从ICLRRuntimeInfo调用GetInterface方法以获取ICorRuntimeHost
  6. 调用ICorRuntimeHost对象的Start方法
  7. 从ICorRuntimeHost对象调用GetDefaultDomain方法以获取IAppDomain对象

然后,您可以使用IAppDomain.Load_2加载库。如果您想从网络共享加载.NET DLL,则更加复杂,因为您需要调用UnsafeLoadFrom,而这在IAppDomain中不可用。但这也是可能的。


-1

我本来打算把这个作为评论发到早些时候的帖子里,不过由于你还没有接受任何答案,也许这可能是你在找的答案。

你原来的帖子只有一个问题:“它是否特别容易?” 答案是坚决的“不”,从你得到的答案就可以看出。

如果其他建议(如本地导出/COM等)“超出了你的能力范围”(你的话!),而你又无法深入学习,我的建议是你需要重新考虑你提出的架构。

为什么不将共享函数写在一个C++库中,然后更容易地被你现有的C++应用程序使用呢?一般来说,从托管代码中使用本机组件要比相反方向更容易 - 所以编写你的C#应用程序来使用共享的C++ DLL会更容易一些。

我知道这个回答并不解答你最初的技术问题,但或许对你面临的问题来说,这是一个更实际的答案。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接