为什么使用 CMake 构建时总是编译 Debug 版本?

在使用 CMake 构建项目时,很多开发者可能遇到过一个问题:明明想要生成 Release 版本,却总是默认编译成了 Debug 版本。这篇文章记录了我在使用 CMake 过程中遇到的相关问题及其解决方法。

理解 CMake 生成的本地构建系统

CMake 是一个跨平台的构建工具,它根据 CMakeLists.txt 文件生成特定于平台的本地编译系统。你可以通过 cmake -B 命令指定生成目标。例如,使用 -DCMAKE_BUILD_TYPE=Release 选项可以为 Makefile 或 Ninja 构建系统生成 Release 版本。然而,Visual Studio 和 Xcode 等 IDE 构建系统支持多目标(multi-configuration)编译,CMake 的行为有所不同。

单配置 vs. 多配置构建系统

对于像 Makefile 和 Ninja 这样的单配置构建系统,你可以通过 -DCMAKE_BUILD_TYPE 来指定编译类型。例如,生成 Release 版本时可以这样指定:

1
cmake -B build -DCMAKE_BUILD_TYPE=Release

这个选项会告诉 CMake 为 Release 模式生成适当的编译文件,一旦配置好,直接运行 cmake --build 即可编译出 Release 版本的程序。

Visual Studio 和 Xcode 的多配置模式

然而,在使用 Visual Studio 或 Xcode 这类多配置构建系统时,情况略有不同。因为这些 IDE 支持多个编译目标(如 Debug 和 Release)同时存在,你不能依赖 -DCMAKE_BUILD_TYPE=Release 来指定编译模式。

生成 Visual Studio 或 Xcode 项目时,只需运行如下命令:

1
cmake -B build -G "Visual Studio 17 2022"

此时,CMake 生成的解决方案文件默认包含多个配置。你可以在构建时通过 --config 选项来指定使用哪个配置。例如,要生成 Release 版本的程序,可以这样编译:

1
cmake --build build --config Release

在这种情况下,-DCMAKE_BUILD_TYPE 是不必要的,而且不会影响编译输出。

总结

在使用 CMake 时,需要根据你使用的构建系统决定如何指定编译类型:

  • 对于 Makefile 和 Ninja 等单配置构建系统,使用 -DCMAKE_BUILD_TYPE=Release 即可。
  • 对于 Visual Studio 和 Xcode 这类多配置构建系统,生成时不需要 -DCMAKE_BUILD_TYPE,而是在编译时使用 --config Release

希望这篇文章能帮助大家更好地理解 CMake 中不同构建系统的行为,避免在生成 Release 版本时陷入 Debug 编译的困扰。

Python & C/C++ 混合开发,调试指南

Debug 第三方库:

调试Python第三方库时,通常需要从源码编译库的 Debug 版本,过程可能会非常繁琐,特别是涉及 C/C++ 依赖时。

但也可以用python调试release库,但生成调试信息pdb,这样可以调试源码而不用python_d

常见问题:
  1. 依赖 C++ 库

    • 如果某些三方库依赖大量 C++ 代码 SDK,这可能涉及几个 GB 的开发库,并且需要复杂的编译环境配置。
    • 在面对第三方 Python 库中使用大量 C++ 代码时,建议考虑是否值得继续,尤其是调试时间成本较高的情况下。
  2. 不兼容的 ABI

    • python_d只能加载 Debug 动态链接库, 因此需要从源码重新编译。
    • 需要自行尝试,如果 python_d 运行出现找不到模块的错误通常就是ABI不兼容需要自行编译debug版本
    • 就算你把release库重命名为_d结尾,_d版本python也不能加载,会出现错误(版本不兼容导入失败):
    • <Fault 1: “<class ‘ImportError’>:Module use of python312.dll conflicts with this version of Python.”>
  3. 自定义构建工具

    • 如果库使用 makecmake 等自定义构建工具,而非使用 setuptools,则需要手动编译和安装,按照库的 README, docs 文档进行编译步骤。
适用于使用 setuptools 的库:

对于仅使用 setuptools 构建的库,可以直接使用以下命令安装 Debug 版本:

1
2
3
4
# 从 requirements.txt安装所有库的 debug 版本
pip install --no-binary :all: --global-option build --debug -r requirements.txt
# 安装指定的库 debug 版本
pip install --no-binary :all: --global-option build --debug <package-name>

在 VSCode 中混合调试 Python 和 C++ 源码

要在 VSCode 中同时调试 Python 和 C++ 代码,需要以下配置:

必要插件:
  1. C++ 插件:支持 C++ 代码的调试。
  2. Python 插件:支持 Python 代码的调试。
  3. GDB: 调试C\C++代码
  4. Python C++ Debugger 插件(可选):此插件可以自动附加调试进程,或自行使用 GDB 进行手动附加调试。
launch.json 示例配置:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
{
"version": "0.2.0",
"configurations": [
{
"name": "Python C++ Debug",
"type": "pythoncpp",
"request": "launch",
"pythonLaunchName": "Python:Streamlit",
"cppAttachName": "(Windows) Attach"
},
{
"name": "(Windows) Attach",
"type": "cppvsdbg",
"request": "attach",
"processId": "",
"sourceFileMap": {
// 映射 Python 源码, 不是必须
"d:/a/1/s/": "D:/Users/Li_Dong/PythonProject/Python-3.12.6"
}
},
{
"name": "Python:Streamlit",
"type": "debugpy",
"request": "launch",
"python": "${workspaceFolder}/env/Scripts/python_d.exe",
"module": "streamlit",
"justMyCode": false,
"args": ["run", "Chenyme-AAVT.py", "--server.port", "8501"]
}
]
}
注意事项:
  • 使用 _d 版本 Python:确保你安装并使用了 Python 的 Debug 版本(python_d)。
  • **关闭 justMyCode**:关闭此选项以便调试第三方库代码,而不仅仅是你自己的代码。

c_cpp_properties.json 示例配置:

这个文件主要用于 C++ 代码的语法分析,方便代码提示和调试。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
{
"configurations": [
{
"name": "Win32",
"includePath": [
// Python 源码头文件目录, 不是必须
"D:/Program Files/Python312/include",
"${workspaceFolder}/CTranslate2/third_party",
"${workspaceFolder}/CTranslate2/include",
"${workspaceFolder}/env/Lib/site-packages/torch/include"
],
"defines": [
"_DEBUG",
"UNICODE",
"_UNICODE"
],
"windowsSdkVersion": "10.0.26100.0",
"compilerPath": "cl.exe",
"cStandard": "c17",
"cppStandard": "c++17",
"intelliSenseMode": "windows-msvc-x64"
}
],
"version": 4
}
总结:
  • 手动编译第三方库的 Debug 版本可能会耗时,尤其是有复杂依赖时。
  • 对于使用 setuptools 的库,可以通过 pip install --no-binary 安装 Debug 版本。
  • 在 VSCode 中配置好 Python 和 C++ 的调试环境,可以帮助你同时调试 Python 与 C++ 代码。

参考:

https://nadiah.org/2020/03/01/example-debug-mixed-python-c-in-visual-studio-code/

Python 2 与 Python 3 的编码差异导致的`ctypes`输出问题

在使用ctypes库时,由于Python 2和Python 3的编码处理不同,可能会导致输出行为差异。以下代码在Python 2中可以正常输出完整的字符串,但在Python 3中只能输出一个字符T

1
2
3
4
from ctypes import *
msvcrt = cdll.msvcrt
message_string = "Hello world"
msvcrt.printf("Testing: %s", message_string)

原因分析

  • Python 2:默认使用ASCII编码处理字符串,因此ctypes能够正常处理该字符串,并将其传递给C函数,如msvcrt.printf

  • Python 3:默认使用Unicode编码处理字符串。由于msvcrt.printf期望接收ASCII编码的字节数据,而Python 3传递的却是Unicode字符串,这导致函数只能正确处理字符串的第一个字节(即T),其余部分被忽略。

解决方法

为了解决Python 3中的编码问题,可以使用以下几种方法:

  1. 使用宽字符函数
    使用wprintf来处理Unicode字符串:

    1
    msvcrt.wprintf("Testing: %s", message_string)
  2. 手动声明为字节串
    将字符串明确声明为字节串(ASCII编码):

    1
    2
    message_string = b"Hello world"
    msvcrt.printf(b"Testing: %s", message_string)
  3. 使用encode()方法
    通过encode()方法将Unicode字符串转换为字节串(默认使用UTF-8编码):

    1
    2
    message_string = "Hello world".encode()
    msvcrt.printf("Testing: %s".encode(), message_string)

总结

Python 2和Python 3在处理字符串时默认的编码方式不同,因此在与C函数交互时需要注意编码问题。Python 2使用ASCII编码,而Python 3使用Unicode编码。为确保在Python 3中正常输出,可以使用宽字符函数、声明字节串或通过encode()方法转换为合适的编码。

fastcall, thiscall, cdecl, stdcall 调用约定

调用约定规定了函数参数如何传递、栈如何管理以及函数调用后栈的清理方式。常见的调用约定包括fastcallthiscallcdeclstdcall,它们在不同平台和架构下的实现方式有所不同。
特别说明,不同cpu,编译器架构调用约定不一样,下面只适用 x86_64 CPU架构 和 MSVC编译器


fastcall

fastcall 是一种优化的调用约定,旨在减少栈操作,加快函数调用的速度。不同平台下的实现差异主要体现在寄存器的使用上。

符号名

  • C: @funcName@paramSize
  • C++: 较为复杂, 一般和C相比funcName变为_Z后面跟函数名长度和函数名, 遍历命名空间和类也是长度加名字, 再跟参数类型, 不需要特别记忆, 导出一般用extern C

32位系统(x86架构)

  • 参数传递: 前两个函数参数通过ECXEDX寄存器传递。
    • 第一个参数传递到ECX
    • 第二个参数传递到EDX
  • 栈传递: 第三个及后续参数通过栈从右到左依次传递。
  • 栈清理: 调用者负责清理栈。

这种方式减少了栈操作,提升了性能,特别适用于传递少量参数的函数调用。

64位系统(x64架构)

在x64架构下,fastcall已成为默认调用约定,优化寄存器的使用:

  • 整数和指针参数传递: 前四个参数分别通过RCXRDXR8R9寄存器传递。
  • 浮点参数传递: 浮点数类型参数通过XMM0XMM3寄存器传递。
  • 栈传递: 超过四个参数则通过栈传递。
  • 栈清理: 被调用者负责清理栈,栈帧必须保持16字节对齐,以支持SSE和AVX指令集。

区别总结

  • 32位: 使用ECXEDX传递前两个参数,栈清理由调用者完成。
  • 64位: 使用RCXRDX等寄存器传递前四个参数,栈清理由被调用者完成,且需要保持16字节对齐。

thiscall

thiscall 是 C++ 类的非静态成员函数的专用调用约定,它用于传递类的this指针。根据不同的系统架构,thiscall的实现有所不同。

符号名

  • C: _funcName
  • C++: 略…

32位系统

  • this传递: this指针通过ECX寄存器传递。
  • 其余参数: 通过栈传递,类似于其他调用约定。

64位系统

  • 标准调用约定: 在64位系统中,不再有单独的thiscall调用约定。this指针会使用标准的寄存器传递,通常是RCX,根据默认的64位调用规则处理。

特性总结

thiscall 是用于非静态成员函数的调用约定,this指针被隐式传递给成员函数。32位系统中使用ECX寄存器传递,而64位系统中则采用统一的调用规则,不再单独定义thiscall


cdecl

cdecl 是C语言和C++中最常见的调用约定,几乎所有函数默认都会使用此约定。

符号名

  • C: _funcName
  • C++: 略…

特性

  • 参数传递: 所有参数通过栈从右到左依次入栈。
  • 栈清理: 调用者负责清理栈。

这是最常见的调用约定,适用于大多数平台和编译器。调用者清理栈使得它在支持可变参数函数(如printf)时表现良好。

示例

1
int __cdecl python_rocks(int one, int two, int three);

对应的汇编代码:
1
2
3
4
5
push three
push two
push one
call python_rocks
add esp, 12

stdcall

stdcall 是 Windows API 的默认调用约定,专为减少函数调用的复杂性设计。

符号名

  • C: _funcName@paramSize
  • C++: 略…

特性

  • 参数传递: 所有参数通过栈从右到左传递。
  • 栈清理: 由被调用者负责清理栈,而非调用者。

stdcall 在 Windows 平台上广泛使用,简化了调用者的责任,使代码更加简洁。

示例

1
int __stdcall python_rocks(int one, int two, int three);

对应的汇编代码:
1
2
3
4
5
6
push three
push two
push one
call python_rocks
;; 被调用者清理栈
ret 12

MSDN x64 ABI 约定概述

具体可以参考MSDN:
在64位系统上,MSDN ABI 调用约定规定了参数通过寄存器传递的方式和栈帧对齐规则。遵循这些规则可提高代码的跨平台兼容性和性能。


总结

调用约定定义了函数调用过程中参数传递和栈管理的方式。在32位和64位系统之间,这些约定存在显著差异。fastcallthiscall在不同平台下的寄存器利用有所不同,cdeclstdcall 则在栈清理方式上有所差异。了解这些约定对编写高效、跨平台的代码至关重要。

Win32 使用 C 调用汇编函数

使用 C 调用汇编函数

在上一章中,我们已经配置好了 MSYS2 环境,安装了 GCC 和 NASM。本章将介绍如何通过 C 调用汇编函数。我们将使用 C 的 fastcall 调用约定来调用一个简单的汇编函数,并且演示如何进行编译和链接。

第一步:创建 C 源文件

首先,创建一个名为 main.c 的 C 文件,并写入以下代码:

1
2
3
4
5
6
7
8
9
#include <stdio.h>

// 声明导入的 add 符号,使用 fastcall 调用约定
extern int __attribute__((fastcall)) add(int a, int b);

int main(void) {
printf("%d", add(20, 50)); // 调用汇编函数并打印结果
return 0;
}

这里我们使用了 __attribute__((fastcall)) 来指定调用约定,这样函数参数可以通过寄存器传递,并且我们不需要处理堆栈平衡。

第二步:编写汇编代码

接下来,创建一个名为 add.asm 的汇编文件,内容如下:

1
2
3
4
5
6
7
section .text           ; 声明 .text 段/节,表示代码段
global @add@8 ; 导出符号,使用 Windows 下的符号格式,@开头,@结尾,8表示参数的总字节数

@add@8: ; 汇编代码,声明 @add@8 标签
add ecx, edx ; 将传入的两个参数相加
mov eax, ecx ; 将结果放入 eax 中(这是返回值)
ret ; 返回

在这个汇编文件中,我们定义了一个名为 @add@8 的函数,它将两个整数相加,并将结果返回。ecxedx 寄存器用于传递 fastcall 调用约定下的参数。

第三步:编译和链接

在 MSYS2 的 mingw32 环境下,运行以下命令来编译和链接汇编文件和 C 文件:

1
2
$ nasm -f win32 -o add.o add.asm    # 编译汇编代码为目标文件
$ gcc -o main.exe add.o main.c # 使用 GCC 链接目标文件和 C 文件为可执行文件

这里我们使用 NASM 将汇编文件编译为 .o 目标文件,然后使用 GCC 链接 C 和汇编的目标文件生成最终的可执行文件。

第四步:运行程序

执行生成的可执行文件 main.exe,并查看输出:

1
$ ./main.exe

输出结果应为:
70

总结

通过以上步骤,我们成功地在 C 中调用了汇编函数,并演示了如何使用 fastcall 调用约定进行参数传递。MSYS2 提供了一个简便的开发环境,可以让我们轻松地进行 C 和汇编的混合编程,并且能够跨平台编译和链接。

Win32 MSYS2 NASM GCC 开发

Win32 在 MSYS2 中使用 NASM 进行汇编开发与 GCC 链接

在 Windows 系统中进行跨平台开发,MSYS2 提供了一个强大的工具链,特别是对于需要进行汇编开发的用户,MSYS2 提供了灵活的环境来处理 NASM 和 GCC 的集成。下面是一个简单的教程,展示如何使用 MSYS2 环境进行 NASM 汇编开发并通过 GCC 进行链接。

第一步:安装 MSYS2

首先,下载并安装 MSYS2。MSYS2 提供了一个轻量级的 POSIX 仿真环境,并且集成了大量的工具包,包括编译器、库和脚本工具。

第二步:安装软件包

启动 MSYS2 并选择运行 mingw32 环境(32 位开发环境)。为了更新系统和安装需要的软件包,依次输入以下命令:

1
pacman -Syu

该命令将更新系统的软件包。接着,安装 NASM 和 32 位工具链:

1
2
pacman -S nasm
pacman -S mingw-w64-i686-toolchain

这将安装 NASM 汇编器和 mingw-w64 32 位编译工具链。

第三步:编写汇编代码

在 MSYS2 中创建一个名为 main.asm 的文件,并写入以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
section .text
extern _printf
global _main

_main:
push message
call _printf
mov eax, 0
ret

message:
db 'Hello, World', 0

这段代码定义了一个简单的汇编程序,它调用了 C 标准库中的 printf 函数来打印 “Hello, World”。

第四步:编译汇编代码

使用以下命令编译 main.asm 文件为目标文件:

1
nasm -f win32 -o main.o main.asm

-f win32 指定目标文件格式为 Windows 32 位格式。

第五步:链接目标文件

接下来,使用 GCC 将目标文件链接为可执行文件。运行以下命令:

1
gcc -m32 -o main.exe main.o

也可以使用 ld 命令手动配置连接
但是使用 gcc 命令会自动处理库文件和链接依赖,省去了手动配置链接的复杂性。

注意事项

  • 在 Windows 下,你可以选择在系统中全局安装 NASM,并让 MSYS2 继承 Windows 环境。但这种做法可能会导致环境污染,并且出现文件名冲突问题,例如 link.exe 的冲突。为了避免这种情况,建议直接在 MSYS2 内安装 NASM。
  • 手动使用 ld 进行链接虽然更灵活,但需要更多的配置工作。如果你不熟悉库文件和链接规则,建议使用 GCC 来简化链接过程。

通过以上步骤,你可以在 MSYS2 中轻松进行汇编开发并编译、链接成可执行文件。MSYS2 提供的工具链对于 Windows 用户来说是一个极具实用性的跨平台开发环境。

c & c++ 中 main 函数形参

main函数参数的扩展,特别是从无参数到三个参数的变化,源于C和C++语言的发展及不同操作系统对程序启动方式的需求。

传统的main函数签名

在早期的C标准中,main函数的签名通常有两种形式:

  1. int main(void) - 无参数。
  2. int main(int argc, char *argv[]) - 有两个参数,分别为:
    • argc: 参数个数,类型为int
    • argv: 参数列表,类型为指向字符串数组的指针,即char *argv[]char **argv

这两种形式是经典的main函数签名,用于接收命令行参数。

第三个参数的引入

第三个参数——char *envp[],是为了提供对环境变量的访问,某些平台支持main函数的第三个参数。这种形式的main函数通常如下:

1
int main(int argc, char *argv[], char *envp[]);
  • envp[]: 这是一个指向字符串数组的指针,其中每个字符串表示一个环境变量。环境变量通常以"NAME=VALUE"的形式表示。

引入背景

  • 环境变量支持: 某些操作系统(例如Unix和某些Linux发行版)支持通过envp[]参数来传递环境变量。这为程序提供了一种直接访问环境变量的方式,尽管并不是所有的平台都强制要求使用这个参数。
  • POSIX标准: 虽然POSIX标准中main函数的签名并没有强制规定必须包含envp[]参数,但它在许多Unix和Unix-like系统中是可用的。

三个参数形式的应用

这种带有第三个参数的main函数形式主要应用于以下情况:

  • 程序需要直接访问环境变量而不通过标准库函数(如getenv())。
  • 某些嵌入式系统或特殊环境下,操作系统会以这种方式启动程序。
  • 某些历史上使用特定C编译器或平台,可能默认提供了这第三个参数。

标准化和使用现状

现代的标准C和C++规范并没有明确规定main函数必须使用第三个参数。标准的main签名依然是int main(void)int main(int argc, char *argv[])。但在实现上,许多编译器依然支持带有envp[]的第三个参数作为一种扩展。

总结

第三个参数envp[]在Unix和类似操作系统中开始流行,主要用于程序对环境变量的访问。虽然现代标准中不强制使用这个第三个参数,但它在某些特定场合仍然可用。