C# 与 C++ DLL 的联合调试

C# 与 C++ DLL 的联合调试

创建简单的本机 DLL

为 DLL 项目创建文件:

  1. 打开 Visual Studio 并创建项目。

    Esc 关闭开始窗口。 键入 Ctrl+Q 以打开搜索框,键入"空项目",然后选择"空项目"(C++)。 在出现的"配置新项目"对话框中,键入名称(如 Mixed_Mode_Debugging),并单击"创建"。

    如果未看到 空项目 项目模板,请转到 工具 >获取工具和功能...,这将打开 Visual Studio 安装程序。 Visual Studio 安装程序将启动。 选择"使用 C++ 的桌面开发"工作负载,然后选择"修改"按钮。

    Visual Studio 创建项目。

  2. 解决方案资源管理器 中,选择 源文件 ,然后选择 项目 >添加新项 。 或者,右键单击 源文件 并选择"添加 >新项 。如果未看到所有项模板,请选择 显示所有模板

  3. "新建项 "对话框中,选择 C++文件(.cpp) 。 在"名称"字段中键入"Mixed_Mode.cpp",然后选择"添加"。Visual Studio 将新的C++文件添加到 解决方案资源管理器

  4. 将以下代码复制到 Mixed_Mode.cpp

    cpp 复制代码
    #include "Mixed_Mode.h"
  5. 解决方案资源管理器 中,选择 头文件 ,然后选择 项目 >添加新项 。 或者,右击 头文件 ,并选择 添加 >新项 。如果未看到所有项模板,请选择 显示所有模板

  6. 在"新项"对话框中,选择"头文件(.h)"。 在 名称 字段中键入 Mixed_Mode.h ,然后选择 添加 。Visual Studio 将新的头文件添加到 解决方案资源管理器

  7. 将以下代码复制到 Mixed_Mode.h

    cpp 复制代码
    #ifndef MIXED_MODE_MULTIPLY_HPP
    #define MIXED_MODE_MULTIPLY_HPP
    
    extern "C"
    {
      __declspec(dllexport) int __stdcall mixed_mode_multiply(int a, int b) {
        return a * b;
      }
    }
    #endif
  8. 选择"文件""全部保存",或按 Ctrl>ShiftS 进行保存。++

配置和生成 DLL 项目:

  1. 在 Visual Studio 工具栏中,选择 调试 配置,x86x64 平台。 如果调用应用为 .NET Core(始终以 64 位模式运行),请选择 x64 作为平台。

  2. 解决方案资源管理器 中,选择 Mixed_Mode_Debugging 项目节点,然后选择 属性 图标,或右键单击项目节点并选择 属性

  3. 在"属性 "窗格顶部,确保 配置 设置为 Active(Debug)平台 与在工具栏中设置的内容相同:x64 ,或 win32 x86 平台。

    如果将平台从 x86 切换到 x64,则必须重新配置新平台的属性。

  4. 在左窗格中 配置属性 下,选择 "链接器 >高级 ",然后在 "无入口点" 旁边的下拉列表中,选择 "无"。 如果必须将其更改为"否",请选择"应用"。

  5. 配置属性 下,选择 常规 ,然后在 配置类型 旁边的下拉列表中,选择 动态库(.dll)。 选择"应用",然后选择"确定"。

  6. 解决方案资源管理器 中选择项目,然后选择 生成 >生成解决方案 ,按 F7 ,或右键单击项目并选择 生成

    该项目应顺利生成,没有错误。

创建一个简单的托管应用来调用 DLL

  1. 打开 Visual Studio 并创建新项目。

    Esc 关闭开始窗口。 键入 Ctrl + Q 打开搜索框,键入"控制台",然后选择适用于 .NET 或 .NET Framework 的 C# 控制台应用。

    然后,键入名称(如 Mixed_Mode_Calling_App ),单击 "下一步""创建",选择可用的选项。

    对于 .NET Core 或 .NET 5+,请选择建议的目标框架或 .NET 10,然后选择" 创建"。

    如果未看到正确的项目模板,请转到 工具 >获取工具和功能... ,这将打开 Visual Studio 安装程序。 根据先决条件选择正确的 .NET 工作负荷,然后选择 修改

    注意

    还可以将新的托管项目添加到现有C++解决方案。 我们正在新解决方案中创建项目,以使混合模式调试任务更加困难。

    Visual Studio 创建空项目,并将其显示在解决方案资源管理器 中。

  2. Program.cs 中的所有代码替换为以下代码:

    C#

    cs 复制代码
    using System;
    using System.Runtime.InteropServices;
    
    namespace Mixed_Mode_Calling_App
    {
        public class Program
        {
            // Replace the file path shown here with the
            // file path on your computer. For .NET Core, the typical (default) path
            // for a 64-bit DLL might look like this:
            // C:\Users\username\source\repos\Mixed_Mode_Debugging\x64\Debug\Mixed_Mode_Debugging.dll
            // Here, we show a typical path for a DLL targeting the **x86** option.
            [DllImport(@"C:\Users\username\source\repos\Mixed_Mode_Debugging\Debug\Mixed_Mode_Debugging.dll", EntryPoint =
            "mixed_mode_multiply", CallingConvention = CallingConvention.StdCall)]
            public static extern int Multiply(int x, int y);
            public static void Main(string[] args)
            {
                int result = Multiply(7, 7);
                Console.WriteLine("The answer is {0}", result);
                Console.ReadKey();
            }
        }
    }
  3. 在新代码中,将 [DllImport] 中的文件路径替换为刚创建的 Mixed_Mode_Debugging.dll 的文件路径。 有关提示,请参阅代码注释。 确保替换 username 占位符。

  4. 选择"文件 >"以保存"Program.cs" ,或按Ctrl +S 保存文件。

配置混合模式调试

  1. 解决方案资源管理器 中,选择 Mixed_Mode_Calling_App 项目节点,然后选择 属性 图标,或右键单击项目节点并选择 属性

  2. 启用本机代码调试‌:右键 C# 项目 > 属性 > 调试 > 打开启动配置文件 UI(或直接在"调试"页)> 勾选 ‌"启用本机代码调试"‌ 。

  3. 在属性中启用本机代码调试。

    .NET 代码

    在左窗格中选择 调试 ,选择 打开调试启动配置文件 UI ,然后选择 启用本机代码调试 复选框,然后关闭属性页以保存更改。

    在左侧菜单中,选择"调试"。 然后,在 调试器引擎 部分中,选择 启用本机代码调试 属性,然后关闭属性页以保存更改。

  4. 如果要从 .NET Framework 应用定位 x64 DLL,请将平台目标从"任何 CPU"更改为 x64。 为此,可能需要从"调试"工具栏的解决方案平台下拉列表中选择 Configuration Manager 。 然后,如果无法直接切换到 x64,请创建面向 x64 的新 配置

设置断点并开始调试

  1. 在 C# 项目中,打开 Program.cs。 在下列代码行中设置断点,方法是点击最左侧边缘并选择该行再按 F9,或右键单击该行并选择"断点""插入断点">。

    cs 复制代码
    int result = Multiply(7, 7);

    在设置断点的左边距中会出现一个红色圆圈。

  2. F5 ,选择 Visual Studio 工具栏中的绿色箭头,或者选择 调试 >启动调试 来开始调试。

    调试器会在设置的断点上暂停。 黄色箭头指示调试器当前暂停的位置。

单步执行和单步跳出本机代码

  1. 托管应用中的调试暂停时按 F11,或选择"调试""单步执行">。

    "Mixed_Mode.h"本机头文件打开,在调试器暂停位置看到黄色箭头。

  2. 现在,可以设置并命中断点以及检查本机代码或托管代码中的变量。

    • 将鼠标悬停在源代码中的变量上以查看其值。

    • 在"自动"和"局部变量"窗口查看变量和变量值。

    • 在调试器中暂停时,还可以使用"监视"窗口和"调用堆栈"窗口。

  3. 再按 F11,将调试器推进一行。

  4. 按 ShiftF11 或选择"调试"+"单步跳出",在托管应用中继续执行并再次暂停。>

  5. F5 或选择绿色箭头以继续调试应用。

Windows C/C# 使用 DLL 的 4 种主流方式完整讲解

一、隐式链接(静态加载,编译时绑定.lib)

原理

编译时链接器读取 DLL 配套的导入库 .lib,程序启动时操作系统自动加载对应 DLL,无需手动调用加载 API。

适用场景

  1. 自有 DLL 工程,同一解决方案开发;
  2. DLL 随程序一同发布,不会动态切换不同版本 dll;
  3. C++、C#(仅 C++ 支持原生隐式链接,C# 无.lib 隐式链接,只能 DllImport)。

C++ 完整使用步骤

  1. DLL 工程导出函数,生成 xxx.dll + xxx.lib
  2. 调用方项目配置:
    • 附加包含目录:DLL 头文件目录;
    • 附加库目录:lib 文件所在文件夹;
    • 附加依赖项:xxx.lib
  3. 代码引入头文件直接调用:
cpp 复制代码
#include "Calculator.h"
int main()
{
    Calculator* calc = CreateCalculator();
    DeleteCalculator(calc);
    return 0;
}
  1. 发布时必须把 xxx.dll 和 exe 放在同一目录,否则启动直接报错找不到模块。

优缺点

✅ 优点:代码简洁,无需手动加载释放,启动即用; ❌ 缺点:

  1. 缺少 dll 程序直接无法启动;
  2. 不能运行时切换 dll 版本;
  3. C# 不支持该方式。

二、显式动态加载(LoadLibrary / FreeLibrary,运行时手动加载)

原理

程序运行后,代码主动调用 Win32 API 加载 DLL,通过函数名获取导出地址,用完手动释放。

适用场景

  1. 可选插件架构,不存在对应 dll 时程序也能正常运行;
  2. 运行时切换不同版本 DLL;
  3. 只使用 DLL 内少量函数,不想全局链接 lib;
  4. C++、C#(P/Invoke 调用 LoadLibrary)通用。

C++ 标准示例

cpp 复制代码
#include <Windows.h>
typedef Calculator* (__cdecl *pfnCreateCalc)();
typedef void (__cdecl *pfnDeleteCalc)(Calculator*);

void TestDll()
{
    // 加载DLL
    HMODULE hDll = LoadLibraryW(L"Calculator.dll");
    if (hDll == NULL) return;

    // 获取导出函数地址
    pfnCreateCalc CreateCalc = (pfnCreateCalc)GetProcAddress(hDll, "CreateCalculator");
    pfnDeleteCalc DeleteCalc = (pfnDeleteCalc)GetProcAddress(hDll, "DeleteCalculator");

    if (CreateCalc && DeleteCalc)
    {
        Calculator* p = CreateCalc();
        DeleteCalc(p);
    }
    // 释放DLL模块
    FreeLibrary(hDll);
}

C# 动态加载封装示例

cs 复制代码
using System.Runtime.InteropServices;

public class DllLoader
{
    [DllImport("kernel32.dll")]
    public static extern IntPtr LoadLibrary(string dllPath);
    [DllImport("kernel32.dll")]
    public static extern IntPtr GetProcAddress(IntPtr hModule, string procName);
    [DllImport("kernel32.dll")]
    public static extern bool FreeLibrary(IntPtr hModule);

    delegate IntPtr CreateCalc();
    delegate void DeleteCalc(IntPtr ptr);

    static void Test()
    {
        IntPtr hDll = LoadLibrary("Calculator.dll");
        IntPtr pCreate = GetProcAddress(hDll, "CreateCalculator");
        CreateCalc create = Marshal.GetDelegateForFunctionPointer<CreateCalc>(pCreate);
        IntPtr calc = create();
        // ...业务逻辑
        FreeLibrary(hDll);
    }
}

优缺点

✅ 优点:

  1. 无 dll 时可做降级逻辑,程序不会崩溃;
  2. 运行时指定 dll 路径、切换版本;
  3. 无需.lib 导入库,只需要 dll; ❌ 缺点:代码繁琐,需要手动管理模块句柄与函数指针。

三、C# 专用:DllImport 特性加载(托管 P/Invoke)

原理

C# 独有的托管调用原生 DLL 方式,本质底层是封装后的动态加载,无需手动LoadLibrary,第一次调用函数时自动加载 dll。

适用场景

C# WinForms/WPF/ 控制台调用原生 C++ DLL(你当前计算器项目场景)。

代码示例

cs 复制代码
using System.Runtime.InteropServices;

class Program
{
    // 声明DLL导出函数,指定调用约定匹配C++ __cdecl
    [DllImport("Calculator.dll", CallingConvention = CallingConvention.Cdecl)]
    public static extern IntPtr CreateCalculator();

    [DllImport("Calculator.dll", CallingConvention = CallingConvention.Cdecl)]
    public static extern void DeleteCalculator(IntPtr calcPtr);

    static void Main()
    {
        IntPtr calc = CreateCalculator();
        DeleteCalculator(calc);
    }
}

关键约束

  1. dll 必须放在 exe 同级目录、系统 system32 目录;
  2. 调用约定必须和 C++ 完全匹配(__cdecl / __stdcall);
  3. 无法直接传递 C++ 类对象,只能用指针交互。

优缺点

✅ 优点:语法简洁,C# 官方标准调用方式,混合调试友好; ❌ 缺点:dll 路径搜索规则固定,不能动态指定 dll 完整路径;缺失 dll 启动后调用函数才报错。


四、C++/CLI 托管混合 DLL(托管 + 原生互通)

原理

创建CLR C++ 项目,编译生成混合模式 dll,同时包含原生 C++ 代码与.NET 托管接口,C# 直接引用该 dll,像调用 C# 类一样使用原生逻辑,无需 DllImport。

适用场景

大量 C++ 逻辑需要高频给 C# 调用,不想手动写 DllImport 指针转换。

示例逻辑

  1. C++/CLI 封装原生 Calculator 类,输出托管包装类;
  2. C# 项目直接添加引用该混合 dll,new 包装类即可使用;
cpp 复制代码
// C++/CLI 封装代码
public ref class CalcWrapper
{
private:
    Calculator* m_native;
public:
    CalcWrapper()
    {
        m_native = CreateCalculator();
    }
    ~CalcWrapper()
    {
        DeleteCalculator(m_native);
    }
    int Add(int a, int b)
    {
        return m_native->Add(a,b);
    }
};

C# 调用:

cs 复制代码
CalcWrapper calc = new CalcWrapper();
int res = calc.Add(1,2);

优缺点

✅ 优点:托管原生无缝互通,不用处理函数指针、调用约定; ❌ 缺点:

  1. 依赖.NET 运行时;
  2. 生成的 dll 体积更大;
  3. 不同 VS 版本、平台目标容易出现兼容性问题。

四种方式对比汇总表

使用方式 支持语言 是否需要.lib 能否动态切换 DLL 开发复杂度 典型场景
隐式链接 (lib) C++ 需要 固定配套 dll,内部项目
LoadLibrary 显式加载 C++/C# 不需要 插件、可选组件
C# DllImport C# 不需要 C# 调用固定原生 DLL(你的计算器)
C++/CLI 混合 DLL C+++C# 互通 不需要 大量原生逻辑给 C# 调用

补充开发建议(适配你当前 C#+C++ 计算器项目)

  1. 日常调试开发优先使用 DllImport,配合后期生成事件自动复制 dll+pdb,支持混合调试;
  2. 如果后续要做插件功能,改用 LoadLibrary 动态加载;
  3. 不推荐隐式链接,C# 无法使用;
  4. 大量复杂类交互可考虑 C++/CLI 包装层,省去指针转换。

ib 与 dll 完整区别详解(分静态库、导入库两种 lib)

一、先分清两种完全不同的 .lib

很多人混淆,lib 分两类,作用天差地别:

  1. 静态库 Static Library(静态 lib):代码 / 数据完整打包进 lib,编译时直接复制到 exe
  2. 导入库 Import Library(DLL 配套 lib):仅记录 DLL 导出函数地址索引,不包含实现,配合 dll 隐式链接

二、核心概念

Windows 动态链接文件,运行时加载

  • 内部包含完整函数实现、资源、DllMain 入口;
  • exe 运行后才载入内存,多个程序可共用同一份 dll;
  • 单独文件,发布必须随 exe 一起分发。

2. 静态 lib(Static Lib)

编译期静态打包库

  • 存放完整函数二进制代码、全局数据;
  • 链接时把用到的代码直接复制进 EXE
  • 最终 exe 独立运行,不需要附带 lib 文件。

3. DLL 配套导入 lib(Import lib)

编译 DLL 时 VS 自动生成的小型 lib 文件

  • 没有任何函数实现代码,只存导出函数名、地址偏移;
  • 仅给编译器做 "索引表",用来隐式链接 DLL;
  • 运行完全不需要这个 lib,只需要 dll。

三、核心对比表

对比维度 静态 .lib(静态库) 导入 .lib(DLL 配套) .dll 动态库
内部内容 完整机器码、全局变量、函数实现 仅导出函数符号、地址索引表,无实现 完整函数实现、资源、DllMain
链接时机 编译链接阶段 编译链接阶段 程序运行时加载
运行依赖 不需要任何外部文件,exe 独立 运行不需要 lib,只需要 dll 程序启动 / 调用时必须存在 dll
内存占用 每个 exe 单独拷贝一份代码,多开重复占用 代码在 dll,多程序共享同一份内存 多进程共享 dll 内存,节省资源
更新方式 必须重新编译所有依赖 exe 只替换 dll 即可,不用重编 exe 直接替换 dll,程序无需重新编译
代码共享 不支持,多程序重复加载 依托 dll 实现共享 天然支持多进程内存共享
崩溃影响 代码内置 exe,崩溃等同于主程序 代码在 dll,dll 出错只影响加载它的进程 dll 内部异常、崩溃会导致调用进程崩溃
C# 可用性 无法直接使用 C# 不支持隐式链接,完全不用 C# 通过 DllImport/LoadLibrary 调用

四、优缺点拆解

静态 lib 优缺点

✅ 优点:

  1. 发布简单,只需要 exe,不用附带一堆库文件;
  2. 无 DLL 路径缺失、版本冲突问题;
  3. 编译时即可发现所有符号缺失错误。

❌ 缺点:

  1. exe 体积大,所有库代码全部打包进去;
  2. 多程序使用同一库会重复占用内存;
  3. 库升级必须重新编译所有使用它的程序;
  4. 无法实现插件动态加载。

DLL + 导入 lib 组合优缺点

✅ 优点:

  1. exe 体积更小,代码剥离到 dll;
  2. 多个程序共用一个 dll,节省内存;
  3. 单独更新 dll,主程序无需重新编译;
  4. 支持运行时动态加载 / 卸载,实现插件系统;
  5. 可单独对 dll 加密、加壳保护核心算法。

❌ 缺点:

  1. 发布必须同步分发 dll,丢失 dll 程序直接报错;
  2. 容易出现 dll 版本冲突(DLL Hell);
  3. 运行时错误(dll 缺失、函数导出变更)只能启动后才能发现;
  4. 跨模块堆内存操作易崩溃(new 在 dll、delete 在 exe)。

五、关键场景区分

场景 1:只给 C++ 小程序用,不想分发额外文件

静态 lib,打包进 exe,单文件发布。

场景 2:多程序共用一套工具库、需要单独更新算法

DLL + 导入 lib,隐式链接开发,发布只带 dll。

场景 3:C# WinForms/WPF 调用 C++ 原生代码(你当前计算器项目)

只用 DLL,导入 lib 完全用不上;C# 通过DllImportLoadLibrary加载 dll。

场景 4:插件架构,程序无 dll 也能正常运行

只用 DLL,运行时LoadLibrary动态加载,不需要导入 lib。


六、高频易错点澄清

  1. 混淆两种 lib
    • 静态 lib:有完整代码,脱离库也能运行;
    • DLL 导入 lib:只是一张 "对照表",删掉 lib 不影响程序运行,只影响编译链接。
  2. 修改 dll 后不用重编 exe 只要导出函数名、参数不变,直接替换 dll 即可生效,这是 DLL 最大优势;静态库必须全部重编。
  3. 内存崩溃经典坑 DLL 内new创建对象,EXE 中直接delete会崩溃;因为两个模块独立 CRT 堆管理器,必须配套导出DeleteCalculator在 DLL 内部释放。
  4. 编译产物区分
    • 新建「静态库项目」→ 输出静态.lib
    • 新建「动态链接库 (DLL) 项目」→ 输出.dll + 配套导入.lib