创建要在 C++ 可执行文件中使用的 C# DLL

Creating a C# DLL to use in C++ executables

提问人:Apache81 提问时间:10/26/2023 最后编辑:Apache81 更新时间:10/27/2023 访问量:93

问:

我已经在这个话题上苦苦挣扎了 1 周,但我尝试过的似乎都不起作用。 我做了一个非常简单的 C# 类:

namespace SimpleMathLib
{
    public class SimpleMath
    {
        public float SumFloat(float a, float b)
        {
            return a + b;
        }

        public int SumInt(int a, int b)
        {
            return a + b;
        }
    }
}

在配置为生成 DLL 的 Visual Studio 2022 解决方案中:VS C# project settings

我确实遵循了一些在线教程,但给我带来较少问题的是关于创建使用 CLR 的 C++ 包装器并将其编译为静态库 (.lib) 的教程。 以下是我在此包装器中添加的脚本:

// SimpleMathLibWrapper.h
#pragma once

class SimpleMathLibWrapperPrivate;

class __declspec(dllexport) SimpleMathLibWrapper
{
    private:
        SimpleMathLibWrapperPrivate* _private;

    public:
        SimpleMathLibWrapper();
        ~SimpleMathLibWrapper();

        float SumFloat(const float a, const float b);
        int SumInt(const int a, const int b);
};

// SimpleMathLibWrapper.cpp
#include "..\public\SimpleMathLibWrapper.h"

#include <msclr\auto_gcroot.h>
#include <msclr\marshal_cppstd.h>

using namespace System::Runtime::InteropServices;
using namespace SimpleMathLib;

class SimpleMathLibWrapperPrivate
{
    public:
        msclr::auto_gcroot<SimpleMath^> simpleMathCSharp;
};

SimpleMathLibWrapper::SimpleMathLibWrapper()
{
    _private = new SimpleMathLibWrapperPrivate();
    _private->simpleMathCSharp = gcnew SimpleMath();
}

SimpleMathLibWrapper::~SimpleMathLibWrapper()
{
    delete _private;
}

float SimpleMathLibWrapper::SumFloat(const float a, const float b)
{
    return _private->simpleMathCSharp->SumFloat(a, b);
}

int SimpleMathLibWrapper::SumInt(const int a, const int b)
{
    return _private->simpleMathCSharp->SumInt(a, b);
}

这些是我必须更改的 Visual Studio CLR 库项目的设置: C++ CLR Wrapper tab1 C++ CLR Wrapper tab2 此外,我还向该项目添加了已编译的 C# DLL 引用C++ CLR Wrapper references

现在,对于 C++ 可执行项目,它是具有以下脚本的 Visual Studio C++ 控制台项目:

// SimpleMathLibUser.cpp : This file contains the 'main' function. Program execution begins and ends there.
//

//#define LOAD_DLL_MANUALLY

#include <iostream>
#include <Windows.h>

#ifndef LOAD_DLL_MANUALLY
    #include "SimpleMathLibWrapper.h"
#endif // !LOAD_DLL_MANUALLY


#ifdef LOAD_DLL_MANUALLY
    void PrintExecutablePath()
    {
        TCHAR exePath[MAX_PATH];
        GetModuleFileName(NULL, exePath, MAX_PATH);
        char narrowExePath[MAX_PATH];

        // convert the string to a narrow character string
        if (WideCharToMultiByte(CP_ACP, 0, exePath, -1, narrowExePath, MAX_PATH, 0, 0) == 0)
        {
            std::cerr << "Failed to convert the path to a narrow character string." << std::endl;
            return;
        }

        char* lastSlash = strrchr(narrowExePath, '\\');
        if (lastSlash != NULL)
        {
            *lastSlash = '\0';
        }

        std::cout << "Current directory: " << narrowExePath << std::endl << std::endl;
    }
#endif // LOAD_DLL_MANUALLY

int main()
{
#ifdef LOAD_DLL_MANUALLY
    PrintExecutablePath();

    // load the DLL.
    HMODULE mathLib = LoadLibrary(TEXT("..\\Plugins\\SimpleMathLibWrapper.dll"));

    if (mathLib == NULL)
    {
        std::cerr << "Failed to load the DLL." << std::endl;
        return 1;
    }

    // get a pointer to functions from the DLL.
    float (*SumFloat)(float, float) = (float (*)(const float, const float))GetProcAddress(mathLib, "SumFloat");
    int (*SumInt)(int, int) = (int (*)(const int, const int))GetProcAddress(mathLib, "SumInt");

    if (SumFloat == NULL)
    {
        std::cerr << "Failed to find the 'SumFloat' function in the DLL." << std::endl;
        return 1;
    }

    if (SumInt == NULL)
    {
        std::cerr << "Failed to find the 'SumInt' function in the DLL." << std::endl;
        return 1;
    }

    // call the functions.
    float resultFloat = SumFloat(10.f, 5.f);
    std::cout << "Float Sum: " << resultFloat << std::endl;

    int resultInt = SumInt(2, 3);
    std::cout << "Int Sum: " << resultInt << std::endl;

    // unload the DLL.
    FreeLibrary(mathLib);
#else
    SimpleMathLibWrapper wrapper;

    std::cout << "Float Sum: " << wrapper.SumFloat(10.f, 5.f) << std::endl;
    std::cout << "Int Sum: " << wrapper.SumInt(2, 3) << std::endl;
#endif // LOAD_DLL_MANUALLY

    return 0;
}

如您所见,为此,我尝试了 2 种方法:

  1. 手动加载包装器
  2. 使用链接器

这些是我为链接器解决方案添加的设置(该解决方案目前至少可以编译,并且似乎在某个点之前有效):C++ executable project tab1 C++ executable project tab2 C++ executable project tab3 C++ executable project tab4

无论 C# dll 是用什么 .NET 版本编译的,当我运行可执行文件时,我总是收到以下错误:

Unhandled Exception: System.IO.FileNotFoundException: Could not load file or assembly 'netstandard, Version=2.1.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51' or one of its dependencies. Impossible to find the scpecified file.
   in SimpleMathLibWrapper.{ctor}(SimpleMathLibWrapper* )
   in mainCRTStartup()

我所知道的是,运行时和 SDK for the .NET 版本都已安装并在系统中正确引用。 无论我在 C# VS 项目中选择什么版本的目标框架,结果都是相同的;只有程序集名称和版本会更改。

我也在互联网上寻找这个特定问题,但大多数解决方案都尝试直接从 .sln 文件而不是从 VS 打开项目,这对我不起作用。

手动加载DLL会导致一系列我无法弄清楚的不同问题,因此,我保留了代码以供参考,但试图修复它。

我知道这是一篇很长的文章,我鼓励你向我询问更多细节,以防我错过什么。 希望在这里找到解决方案将来能够帮助更多的人。

谢谢!!

C# C++ DLL CLR

评论

0赞 shingo 10/26/2023
本文说:C++/CLI 项目不能面向 .NET Standard
2赞 JonasH 10/26/2023
C++/CLI 似乎总是事后才想到的,多年来的改进非常有限。我会从使用 .net 4.8 开始,因为我至少知道它有效,并在您找到工作时进行升级。我没有在 .Net 5+ 中尝试过 C++/CLI,但我预计会有一些问题和限制。我也不确定如果应用程序不是 .net 应用程序,这是否有效。需要加载 CLR 才能运行任何托管代码,我不会假设这是自动完成的。
2赞 Hans Passant 10/26/2023
错误消息是准确的,netstandard.dll v2.1 不适用于 .NET 4.x。这是你通过选择 /clr 而不是 /clr:netcore 而不选择特定的 .NET 版本来实现的目标。在这种情况下,您确实希望最大程度地减少拖入的依赖项数量,请赞成 C# 。面向 4.x 的 NETFramework 项目
2赞 Ben Voigt 10/26/2023
看起来您使用 C++/CLI 让它工作;我只是想解释为什么你的尝试失败了。 只能查找 DLL 的导出表中列出的函数。导出表与 .NET 元数据截然不同,后者使其他 .NET 项目能够使用 DLL(以及运行时反射)。.NET DLL 可以具有导出,但需要使用 MSIL 来创建它们。有一些后期生成工具可以与 C# 编译器生成的 DLL 一起使用,但仅靠 C# 无法创建导出。LoadLibraryGetProcAddressGetProcAddress
1赞 Ben Voigt 10/27/2023
@Apache81:我想到了这一点,但你使用的是 C# 代码中的名称。C++ 包装器代码没有命名的全局函数,它具有非静态成员函数,这些函数在名称上不匹配,在签名上不匹配,在调用约定上不匹配。SumFloatSumInt

答:

2赞 Apache81 10/26/2023 #1

在阅读了我收到的一些评论后,我终于能够让一切正常!!

首先,由于 CLR,C# DLL 需要使用小于或等于 4.7.2 的 .NET Framework 版本进行编译。

你应该怎么做才能实现这一目标? 编辑 .csproj 文件,如下所示:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>net472</TargetFramework>
    <ImplicitUsings>disable</ImplicitUsings>
    <Nullable>disable</Nullable>
    <SignAssembly>False</SignAssembly>
    <AssemblyOriginatorKeyFile>SimpleMathManaged</AssemblyOriginatorKeyFile>
  </PropertyGroup>

</Project>

确保指向正确的版本,则禁用,因此项目将不会使用不适用于该框架的 GlobalUse,并且您还需要禁用 ,也不受支持。<TargetFramework><ImplicitUsing><Nullable>

之后,重新编译 DLL、包装器(我没有在那里更改任何内容)以及 C++ 可执行文件以更新到最新的包装器 LIB。 确保将 C# DLL 复制到 .exe 文件所在的位置(我忘了在原来的帖子中提到这一点),运行项目,一切都应该正常工作!!:)enter image description here 最后。。。

基本上,我最初的错误是使用 .NET 6.0(使用 Visual Studio 创建新的 C# 项目时的默认设置)编译 DLL,它高于 4.7.2 并且不受支持。 包装器编译过程没有启动任何警告或错误,但是当从可执行文件使用时,它不起作用。

第二个错误是尝试使用 .Net Standard 2.1,同样,它不受支持,也没有发出警告。

当我按照 JonasH 和 Hans Passant(谢谢你们)在评论中的建议尝试 .NET 4.8 时,我终于收到了包装器的警告,告诉我不支持目标 .NET Framework,因为高于 4.7.2,然后,最终以正确的版本为目标,一切正常。

我真的希望这个解决方案可以帮助每个有同样问题的人,因为找到解决方案并不好玩。

更新!!!

为了完整起见,我想补充另一件事。 我将 C++ 包装器编译为静态库 (.lib),因为当我尝试在 C++ 可执行 Visual Studio 项目中链接 DLL 时: 我收到此错误: enter image description here

Error LNK1302 only support linking safe .netmodules; unable to link ijw/native .netmodule
    SimpleMathLibUser D:\dev\C++\SimpleMathLibWrapper\x64\Debug\SimpleMathLibWrapper.dll

如果要将 C++ 包装器作为动态库 (.dll),则 C++ 可执行 Visual Studio 项目的链接器应链接 .obj 文件,而不是直接指定 .dll 文件:enter image description here

评论

0赞 nick 10/31/2023
非常感谢您回来提供您的解决方案!很高兴你让它工作:)