虽然没有上一节的难但是内容也很多
关于实现和使用脚本语言
以下是详细复述:
许多人经常问一个问题,反复问过好几次,那就是:是否会在项目中实现脚本语言。这个问题的具体形式通常是:你们会使用脚本语言吗?或者更具体地问,是否会自己实现一套脚本语言。
对于这个问题,答案是否定的。明确表示,不打算在项目中使用现成的脚本语言。但是,是否要自己构建一套脚本语言,依然是一个值得讨论的问题。因为这意味着从零开始构建,可能最终决定自己开发一套脚本语言,但也必须反思,创建脚本语言是否真的是一个好主意。
为了评估这个问题,需要停下来思考:如果决定使用脚本语言,主要目的是做什么?即,脚本语言能提供哪些好处?与它可能带来的缺点相比,这些好处是否值得。脚本语言有一些明显的缺点,最重要的是性能问题。若游戏中的大量内容都由脚本语言编写,这可能需要使用即时编译等技术,这就会影响性能。因此,开发者必须特别关注脚本语言带来的性能挑战。
进一步来说,使用脚本语言意味着要承担优化和调试的责任。如果有很多游戏内容是通过脚本编写的,开发者可能需要花费更多时间在调试和性能优化上。这不是一件简单的事情,使用脚本语言时,开发者会面临各种困难,因为它通常不像传统编程语言那样有现成的工具和支持。
例如,在调试方面,开发者需要考虑是使用现有的第三方工具,还是自己开发调试工具。并且,如果使用第三方工具,必须确保这些工具能够与所使用的开发平台兼容,否则调试过程可能会变得复杂,甚至需要在不同的工具之间不断切换。这些都是可能出现的问题,涉及到大量的成本和风险。
因此,在讨论是否要实现脚本语言时,需要考虑这些潜在的成本和复杂性。虽然这些问题并不意味着绝对不能使用脚本语言,但它们确实要求开发者深思熟虑,确保这是一个值得的决定,尤其是在从头开始做一切的情况下。因为整个项目已经是从零开始进行开发,团队的精力和时间是有限的,因此在考虑是否实现脚本语言时,必须评估这些额外的工作是否能带来足够的回报。
一个显而易见的好处是,许多人认为脚本语言比传统的编程语言更容易使用。虽然这种看法并不完全明确,但很多人认为,使用脚本语言编写代码更为简便。然而,实际情况可能并非如此复杂。写代码并不一定比使用传统语言更困难,尤其是在开发过程中,使用合适的工具和框架,开发者能够高效地编写和测试代码。
另外,脚本语言的一个明显优点是,它允许在游戏运行时进行编辑。这个功能在很多游戏开发场景中被视为非常有用。通过在游戏运行时编辑脚本,开发者可以实时调整和测试游戏中的变化,这为调试和快速开发提供了巨大的便利。如果能够在游戏运行的同时编辑脚本语言,这无疑是一个显著的优势。
总的来说,尽管脚本语言提供了某些便利,但它也带来了许多额外的成本和技术挑战。开发团队需要权衡这些利弊,决定是否值得为项目构建和维护脚本语言。
动态加载游戏代码的好处
在游戏开发过程中,通常需要不断调整和测试代码,尤其是在调试阶段。有时,开发者会遇到需要调试特定游戏行为(例如一个boss怪物)的情况。在传统的调试流程中,开发者可能需要退出游戏,修改代码,然后重新编译再回到游戏中查看效果。这个过程往往繁琐且时间消耗大。
为了提高效率,一种常见的技术是"编辑和继续"(Edit and Continue)。这种技术允许开发者在调试时直接编辑代码,无需退出游戏或重新编译。例如,开发者可以在调试会话中设置断点,修改某些代码,然后继续游戏运行而不需要中断。这样就能节省时间,直接查看修改后的效果。
然而,这种技术在实际使用中有局限性。尽管它对小规模的修改有效,但在面对复杂的新功能开发时,它并不总是可靠。特别是当开发者尝试添加大量新功能时,这种技术可能会失败,导致游戏崩溃。因此,虽然"编辑和继续"是一种便利的功能,但它并不是一种万能的解决方案。
为了克服这些问题,开发者希望能够实现自己的"编辑和继续"功能,特别是在C语言代码的开发环境中。理想情况下,他们希望能够直接修改代码并让游戏在修改后继续运行,而无需重启。这意味着开发者可以在游戏运行的同时修改其C代码,并立刻看到更改后的效果。
为了解决这个问题,开发者考虑开发一种简单的、自定义的"编辑和继续"功能,避免依赖现有的编译工具和平台(如Visual C++)的局限性。通过这种方式,开发者能够在游戏运行时修改代码,并实时查看修改的效果,而不必担心常规编译和重启步骤。
因此,接下来的目标是实现这一功能:让游戏在运行过程中能够动态修改代码,而不需要重新启动游戏。这将大大提高开发效率,尤其是在调试和试验阶段。开发者打算从实现这个目标开始,逐步推进,看看能够达到什么效果,并在过程中调整和优化实现方法。
开始动态代码加载
在游戏开发过程中,特别是在使用Windows操作系统时,有时会需要动态加载和修改代码,而不必退出游戏进行重新编译。为了实现这一点,可以利用Windows提供的"加载库"(LoadLibrary)功能。该功能允许开发者在运行时动态加载外部库,并通过调用GetProcAddress
获取函数指针,从而执行库中的函数。
这种方法的关键在于能够分离平台相关的代码和平台独立的代码。开发者可以将平台独立的部分和平台相关的部分分开编译。通过这种分离,平台相关代码和平台独立代码可以分别进行独立编译和加载,从而提高灵活性。
具体来说,开发者可以把代码分成两个独立的翻译单元(translation units):一个用于平台相关代码,另一个用于平台独立的游戏代码。平台独立代码通过动态加载的方式与平台相关代码进行交互。当需要修改平台独立代码时,开发者可以重新编译这个部分,并通过重新加载库来实现代码更新,而无需重启游戏。这样,游戏可以在不中断的情况下继续运行。
在这个过程中,所有游戏内存基本上是作为一个整体传递的,游戏代码并不直接操作这些内存,因此开发者可以在修改和重新加载代码时保留原有的内存内容,确保游戏不会丢失状态。通过这种方式,游戏代码的更新变得更加灵活高效。
虽然这种方法简单易行,但也有一些限制,取决于修改的代码和所使用的操作系统。在某些情况下,修改可能无法完全支持动态加载和重新加载功能,但对于大多数情境来说,这是一种有效且可靠的解决方案。
这种方法使得开发者能够在不重启游戏的情况下动态修改代码,并实时查看修改效果,极大提升了开发过程中的效率和灵活性。
将平台代码和游戏代码分到不同的翻译单元
以下是对上面内容的详细复述:
在构建和编译过程中,代码被拆分成多个部分,以便提高灵活性和模块化。在这种情况下,开发者将代码分为两个编译单元,分别包含不同的功能部分。首先,开发者决定编译两个不同的文件------一个是"game"的源文件(game.cpp),另一个是平台相关的源文件。通过将这些部分分开,开发者能够单独编译每个模块,而不必每次都重新编译整个程序。
其中,game的文件会单独编译,作为一个独立的单元。在构建过程中,开发者会确保某些代码只在特定的文件中包含,而不再包含在其他地方。具体来说,game的源文件将只包含与其相关的代码,而不再通过传统的"#include"方式引入其他不必要的部分。这种方法有助于减少代码的复杂性,并让编译过程更加高效。
在构建过程中,开发者使用了"链接器"来将这些分开编译的代码进行连接。链接器的作用是将独立的编译单元合并成一个可执行文件,而这也要求开发者解决一些外部符号和链接问题。当拆分代码后,开发者注意到一些外部调用无法解析,这些"未解决的外部"问题提示开发者在链接时需要解决这些依赖项。
为了处理这些问题,开发者开始检查并修正这些链接错误。通过调整链接器的设置,开发者可以确保各个模块正确地引用彼此的代码,并解决因模块拆分而出现的调用问题。
这种方法的核心是将代码拆分成更小的单元,使得每个单元都可以独立编译和更新。尽管这种拆分可能会导致一些初始的链接错误,但最终它有助于提高程序的灵活性和可维护性。当开发者解决了所有的链接问题后,程序就能够正确地执行,并且通过动态加载和重新编译的方式实现代码更新,而无需停止程序的运行。
总的来说,这种做法优化了代码的组织结构,并使得开发过程更加灵活。通过合理地利用链接器和编译单元,开发者能够在开发和调试过程中更高效地管理代码的修改。
从游戏 DLL 获取函数到平台可执行文件
下面是对上面内容的详细复述,重点关注过程的解释和实现方法:
问题和目标
我们需要实现一个机制来动态加载游戏代码的动态链接库(DLL),并通过加载的库函数更新游戏逻辑和渲染画面,同时处理获取声音样本的需求。这个实现应允许我们在运行时切换游戏逻辑,以支持模块化开发和动态更新。
解决方案概述
- 动态加载库 :使用
LoadLibrary
函数动态加载游戏代码的 DLL 文件。 - 获取函数指针 :通过
GetProcAddress
获取导出的函数地址,这些函数用于处理游戏逻辑和渲染。 - 设置函数存根:在没有有效 DLL 时,使用默认的存根函数作为占位,防止程序崩溃。
- 设计指针存储结构:创建一个结构体来存储这些函数的指针,以便在程序中方便地调用。
- 确保有效性:在加载库和获取函数时验证其有效性。
具体实现步骤
1. 定义游戏代码函数指针
首先,为我们期望从 DLL 中加载的函数定义指针类型。例如,以下两个函数:
GameUpdateAndRender
:用于更新游戏状态和渲染画面。GameGetSoundSamples
:用于获取声音样本。
c
typedef void GameUpdateAndRender();
typedef void GameGetSoundSamples();
2. 创建存储结构
接着,我们定义一个结构来存储这些函数指针,并为它们提供默认存根。
c
typedef struct {
GameUpdateAndRender* UpdateAndRender;
GameGetSoundSamples* GetSoundSamples;
} GameCode;
GameCode Game;
3. 定义存根函数
存根函数用于在库未加载时占位,它们不会执行实际操作。
c
void StubUpdateAndRender() {
// 空实现
}
void StubGetSoundSamples() {
// 空实现
}
在初始化时,将这些存根函数分配给 GameCode
结构的指针。
c
void InitializeGameCode(GameCode* Game) {
Game->UpdateAndRender = StubUpdateAndRender;
Game->GetSoundSamples = StubGetSoundSamples;
}
4. 加载库和函数
实现一个加载库的函数,利用 LoadLibrary
和 GetProcAddress
获取库和函数指针。
c
bool LoadGameCode(const char* DLLPath, GameCode* Game) {
HMODULE GameDLL = LoadLibrary(DLLPath);
if (GameDLL) {
Game->UpdateAndRender = (GameUpdateAndRender*)GetProcAddress(GameDLL, "GameUpdateAndRender");
Game->GetSoundSamples = (GameGetSoundSamples*)GetProcAddress(GameDLL, "GameGetSoundSamples");
// 验证函数是否有效
if (!Game->UpdateAndRender || !Game->GetSoundSamples) {
FreeLibrary(GameDLL);
InitializeGameCode(Game);
return false;
}
return true;
}
// 如果加载失败,则使用存根函数
InitializeGameCode(Game);
return false;
}
5. 使用动态加载的函数
在主程序中,通过调用 LoadGameCode
函数加载 DLL。加载成功后,调用实际函数;否则调用存根函数。
c
void MainLoop() {
// 初始化游戏代码结构
InitializeGameCode(&Game);
// 尝试加载游戏代码
if (!LoadGameCode("GameCode.dll", &Game)) {
printf("Failed to load game code, using stubs.\n");
}
// 主循环
while (true) {
Game.UpdateAndRender();
Game.GetSoundSamples();
}
}
6. 处理库的释放和重新加载
支持在运行时切换库,例如用于热更新,可以在每一帧的开始重新加载库。
c
void ReloadGameCodeIfNeeded(GameCode* Game, const char* DLLPath) {
static FILETIME LastWriteTime = {0};
// 检查 DLL 文件是否被修改
FILETIME CurrentWriteTime = GetFileWriteTime(DLLPath);
if (CompareFileTime(&CurrentWriteTime, &LastWriteTime) != 0) {
// 更新写入时间
LastWriteTime = CurrentWriteTime;
// 释放旧的 DLL 并加载新的
LoadGameCode(DLLPath, Game);
}
}
在主循环中调用此函数以实现热加载。
代码工作流程
- 初始化
GameCode
并将存根函数分配给其指针。 - 使用
LoadGameCode
尝试加载 DLL,并获取函数地址。 - 在运行时通过调用
Game.UpdateAndRender
和Game.GetSoundSamples
来执行操作。 - 通过
ReloadGameCodeIfNeeded
实现热加载功能。
总结
上述方法使得程序能够在运行时动态加载和切换游戏逻辑,具备模块化和灵活性。这种设计特别适用于开发过程中需要频繁修改和测试的场景,同时提高了程序的稳定性,即使在 DLL 缺失时也能正常运行。
从平台可执行文件获取函数到游戏DLL
问题的背景和初步分析
我们面临的问题是无法直接从可执行文件中获取所需的 proc address
。虽然可以通过加载可执行文件并在交易中以较低的级别进行操作来处理调用,但这会浪费资源,也没有必要。考虑到我们传递给游戏的结构中已经包含了所有必要的内容,我们可以通过更高效的方式来解决问题。
解决思路
当我们调用游戏的更新和渲染功能时,所有相关的指针和内存结构(如游戏内存结构、调试服务的指针)已经被传递。这些并不需要实际的函数调用,而是可以通过函数指针直接调用。通过这种方式,我们可以在代码中以一种简单而直观的方式实现反向调用。
调试服务的实现方法
我们通过以下几个步骤实现调试服务:
- 定义功能指针: 将调试服务的功能定义为函数指针,并在内存结构中声明。
- 简化接口: 将原本可能需要显式函数调用的操作变为通过指针访问的方式。例如:
- 平台读取整个文件的操作。
- 平台释放文件内存的操作。
- 初始化指针: 在初始化游戏内存结构时,将这些指针分配到对应的功能。
- 通过内存直接调用: 在需要使用这些功能的地方,直接从内存结构中调用对应的指针,而不是显式地声明额外的函数。
具体代码逻辑
- 定义函数指针: 使用一个宏来统一定义这些功能指针的类型,以减少重复代码和维护成本。
- 分配指针: 初始化时将所有需要的功能指针赋值到游戏内存结构中,确保在调用时不会出现空指针错误。
- 按顺序整理: 为了便于维护,所有指针初始化按照固定顺序排列。
- 避免崩溃: 直接在初始化阶段设置指针,而不是在运行时因未初始化导致崩溃后再修复。
实现小型调度系统
通过将功能封装成指针传递到游戏内存结构中,我们实际上实现了一个类似于虚表(vtable)的机制。这种机制灵活而直观,可以根据需求任意扩展功能,同时避免了传统虚表中复杂且不透明的实现。
调试和验证
- 编译代码,检查是否有未初始化的指针或其他错误。
- 确保构建目录中生成的输出与预期一致。
- 避免在代码中包含不必要的出口或多余的内容。
注意事项
- 初始化过程应该与功能指针的定义保持一致,避免在多个地方重复声明。
- 宏的使用需合理设计,确保改动宏后相关代码依然保持正确性。
总结
通过这种方法,我们实现了一个轻量级、高效的功能调度系统,避免了显式函数调用的开销,并提高了代码的可维护性和灵活性。这种设计方法对于需要频繁扩展功能的项目尤为适用。
从 DLL 导出函数
这段内容详细描述了在开发动态链接库(DLL)时遇到的问题,特别是在定义和实现入口点以及处理 Windows 特定行为时的疑问和尝试。以下是逐步分析和归纳:
1. 关于动态链接库(DLL)的行为与需求
-
DLL 的行为
Windows 在加载 DLL 时会调用指定的入口点函数,例如
DllMain
,用于执行初始化或清理操作。- 即使不需要特定的初始化逻辑,Windows 也会调用 DLL 的入口点。
- 提到了一种假设:是否可以避免 Windows 跳转到入口点。
-
DLL 的用途
当前的目标是创建一个简单的 DLL,它的行为与可执行文件类似,但仍需适应 DLL 的特性:
- 例如:支持被其他进程加载时的响应逻辑。
- 开发者认为目前只需要提供一个"存根"(stub)作为占位代码,可能不需要复杂的实现。
2. 入口点的实现与疑问
-
DLL 的入口点 (
DllMain
)提到在 DLL 中实现入口点函数(如
DllMain
)是必须的,用于响应加载和卸载事件:cppBOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) { switch (ul_reason_for_call) { case DLL_PROCESS_ATTACH: case DLL_THREAD_ATTACH: case DLL_THREAD_DETACH: case DLL_PROCESS_DETACH: break; } return TRUE; }
- Windows 会调用
DllMain
,例如当进程加载 DLL 时触发DLL_PROCESS_ATTACH
。 - 当前假设:如果不导出任何函数或入口点是否会导致构建失败。
- Windows 会调用
-
入口点参数与顺序的疑问
- 提到链接器可能会因为某些参数的位置或内容不正确而无法识别入口点。
- 例如,
/link
参数后的配置是否会影响行为。 - 开发者尝试了不同的参数组合以测试这个问题。
3. 导出函数的设置与问题
-
函数导出是否必要
一个疑问是:如果没有明确导出函数,是否会影响 DLL 的构建和加载。Windows DLL 通常需要明确哪些函数需要导出,可以通过以下方式实现:
cpp__declspec(dllexport) void MyFunction() { // Function implementation }
- 如果未导出任何函数,链接器可能会认为 DLL 的实现不完整,从而报错。
-
存根代码的实现
当前计划在
handmade.cpp
中实现一个基本的存根函数,作为入口点或导出的占位代码。
4. 调试与排查过程
-
编译与运行的问题
- 编译时报错:"必须定义入口点",说明链接器未正确识别 DLL 的入口点配置。
- 开发者推测问题可能是:
- 未正确设置导出函数或入口点。
- 链接参数顺序错误。
-
排查过程
- 尝试创建最小化的 DLL 示例以验证基础配置是否正确。
- 检查
handmade.cpp
和DllMain
的实现是否符合要求。 - 尝试通过不同的编译器标志和链接器选项解决问题,例如调整
/link
参数的位置。
5. 动态链接库的核心逻辑
-
DLL 的加载和卸载
- 提到 DLL 会被加载到进程内存中,并可能被多个线程或进程使用。
DllMain
会响应加载事件,如DLL_PROCESS_ATTACH
和DLL_PROCESS_DETACH
。
-
资源管理与平台代码
- 当前实现计划避免复杂的资源管理逻辑,只需实现最基本的 DLL 加载响应。
- 平台代码的作用仅限于支持核心功能,无需额外逻辑。
6. 问题未解的困惑
-
对链接器报错的疑问
- 开发者表示难以理解为何当前的配置不能正确生成 DLL。
- 强调这是一个标准问题,但可能因为某个细节配置遗漏导致问题未解。
-
简单示例的需求
- 希望找到一个最小化的 DLL 示例,以确认基本配置和逻辑的正确性。
- 当前尝试参考其他代码片段和社区的实现,但未能完全解决问题。
关键总结
这段文字描述了在动态链接库开发中遇到的常见问题,包括:
- 入口点实现 :
DllMain
的配置与响应机制。 - 函数导出与链接参数:导出函数和参数顺序对 DLL 的影响。
- 调试与排查:通过最小化代码示例和调整配置进行测试。
整体反映了开发者在复杂环境下逐步摸索和验证的过程,同时体现了对基础实现和配置的深入思考与测试逻辑。
当前的cmake
cpp
# CMakeList.txt : CMake project for game, include source and define
# project specific logic here.
#
# 创建静态库
# add_library(game STATIC "game.cpp") # 生成game.lib
add_library(game SHARED "game.cpp") # 生成game.dll
# Add source to this project's executable.
add_executable(win32_game WIN32 "win32_game.cpp")
# add_executable(test "test.cpp")
# 定义全局宏
add_compile_definitions(GAME_SLOW=1 GAME_INTERNAL=1)
# 为特定目标定义宏
# target_compile_definitions(test PRIVATE GAME_INTERNAL=0 GAME_SLOW=1)
# 指定库文件目录
set(LibraryDir ${CMAKE_BINARY_DIR}/game)
link_directories(LibraryDir)
message("库路径${LibraryDir}")
# 链接 User32.lib 库
target_link_libraries(win32_game PRIVATE User32.lib Gdi32.lib Winmm.lib)
# 为 MSVC 编译器添加参数
if(MSVC) # 如果编译器是 MSVC(Microsoft Visual C++)
# 设置 C++ 编译选项
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /WX /W4 /wd4819 /wd4201 /wd4505 /Zi /FC /Fmwin32_game.map")
# /WX : 将所有警告视为错误(会让编译因警告失败)
# /W4 : 设置警告级别为 4,显示大多数警告
# /wd4819 : 屏蔽警告 C4819,避免文件编码问题导致的警告
endif()
if(CMAKE_VERSION VERSION_GREATER 3.12)
set_property(TARGET win32_game PROPERTY CXX_STANDARD 20)
set_property(TARGET game PROPERTY CXX_STANDARD 20)
endif()
# TODO: Add tests and install targets if needed.
现在貌似DLL没有导出使用的是stub 的函数
查看导出的函数
cpp
dumpbin /exports game.dll
C:/Program Files (x86)/Microsoft Visual Studio//VC/Tools/MSVC//bin/Hostx64/x64/
或者
C:/Program Files/Microsoft Visual Studio//VC/Tools/MSVC//bin/Hostx64/x64/
"C:/Program Files/Microsoft Visual Studio/2022/Community/VC/Tools/MSVC/14.40.33807/bin/Hostx64/x64/dumpbin.exe" /exports game.dll
在 CMake
中,你可以使用 CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS
或手动设置 EXPORT
选项来指定要导出的符号(函数、变量等)。下面是两种常见的做法:
1. 自动导出所有符号
你可以使用 CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS
来自动导出所有函数符号。这通常用于创建动态链接库(DLL)。
在 CMakeLists.txt
中添加以下行:
cmake
set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS ON)
这将指示 CMake 在构建 DLL 时自动导出所有公共符号。使用这种方法,你不需要显式地指定每个要导出的函数。
2. 手动导出符号
如果你希望更加控制导出的符号,可以使用 __declspec(dllexport)
或 __declspec(dllimport)
来标记需要导出的函数或类。然后,使用 CMake
来创建导出文件。
步骤:
-
在源代码中使用
__declspec(dllexport)
你需要在源代码中显式标记导出符号。例如:
cpp// MyLibrary.h #ifdef BUILDING_MYLIBRARY #define MYLIBRARY_API __declspec(dllexport) #else #define MYLIBRARY_API __declspec(dllimport) #endif class MYLIBRARY_API MyClass { public: void MyFunction(); };
在构建 DLL 时,需要定义
BUILDING_MYLIBRARY
,以便将MYLIBRARY_API
设置为__declspec(dllexport)
。 -
在
CMakeLists.txt
中指定EXPORT
如果你想使用
EXPORT
指令来显式导出符号,可以创建一个导出文件来指定要导出的符号:cmake# CMakeLists.txt # 设置动态库目标 add_library(MyLibrary SHARED MyLibrary.cpp MyLibrary.h ) # 定义导出符号 set_target_properties(MyLibrary PROPERTIES WINDOWS_EXPORT_ALL_SYMBOLS YES ) # 导出符号 target_compile_definitions(MyLibrary PRIVATE BUILDING_MYLIBRARY)
在这个例子中,
WINDOWS_EXPORT_ALL_SYMBOLS YES
会启用自动导出符号。 -
使用
EXPORT
文件(对于复杂的符号管理)你也可以使用
CMake
的export()
和install()
来导出 DLL 符号:cmake# CMakeLists.txt add_library(MyLibrary SHARED MyLibrary.cpp MyLibrary.h ) # 创建导出文件 install(TARGETS MyLibrary EXPORT MyLibraryExport RUNTIME DESTINATION bin LIBRARY DESTINATION lib ) # 导出库的符号 export(EXPORT MyLibraryExport FILE MyLibraryExport.cmake)
通过这种方法,CMake 会创建一个导出文件,包含所有需要导出的符号。
3. 手动设置导出符号(/EXPORT
)
如果你想手动在链接器命令中添加 /EXPORT
标志,可以通过 target_link_libraries()
指定 LINK_FLAGS
:
cmake
add_library(MyLibrary SHARED
MyLibrary.cpp
MyLibrary.h
)
# 手动添加 EXPORT 标志
set_target_properties(MyLibrary PROPERTIES
LINK_FLAGS "/EXPORT:MyFunction"
)
这将告诉链接器在生成 DLL 时将 MyFunction
导出。
总结
- 如果你想自动导出所有符号,可以使用
CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS ON
。 - 如果你想手动控制符号导出,可以通过
__declspec(dllexport)
和__declspec(dllimport)
来标记。 - 你也可以使用
target_compile_definitions()
和set_target_properties()
来指定编译和链接的导出设置。
这些方法可以帮助你在 CMake 构建系统中处理 DLL 的导出符号。
再试一下 好像可以
在 C++ 中,extern "C"
用来指示编译器按照 C 语言的方式来处理函数的名称(即,关闭 C++ 的名称修饰机制)。这通常在以下几种情况下需要使用:
1. 导出 C 函数
如果你希望从 C++ 库中导出函数,并且这些函数要以 C 风格的名称调用(没有 C++ 的名称修饰),你需要使用 extern "C"
。这常见于与其他语言(例如 C)或工具(如动态链接库)进行互操作时。
例如:
cpp
#ifdef __cplusplus
extern "C" {
#endif
__declspec(dllexport) void MyExportedFunction();
#ifdef __cplusplus
}
#endif
2. 避免 C++ 名称修饰(Name Mangling)
C++ 编译器会对函数名进行"名称修饰"(name mangling),即给函数名添加额外的信息,以支持重载等特性。这意味着如果你编写一个 C++ 函数并导出它,函数名会被修改成一个包含类型信息的复杂字符串,其他语言或系统就无法正确调用该函数。
例如,void foo(int)
在 C++ 中可能会被编译成 foo__i
这样的名字,这就不能直接被 C 或其他语言调用了。
使用 extern "C"
可以关闭名称修饰,使得函数的名称像 C 函数一样简单:
cpp
extern "C" {
__declspec(dllexport) void MyFunction();
}
这将确保 MyFunction
的符号在 DLL 中没有名称修饰,可以被 C 或其他语言正确调用。
3. C 与 C++ 代码互操作
如果你有一个 C++ 库并想从 C 代码中调用它,或者反之,如果你希望 C++ 代码能够调用 C 库,使用 extern "C"
可以确保链接器能够正确匹配符号。例如,C 库通常没有 C++ 的名称修饰,所以你需要将 C++ 中的函数标记为 extern "C"
,以确保它们可以在 C 环境中正常链接。
示例:
从 C++ 导出 C 函数:
cpp
// MyLibrary.h
#ifdef __cplusplus
extern "C" {
#endif
__declspec(dllexport) void MyExportedFunction(int x);
#ifdef __cplusplus
}
#endif
从 C++ 代码调用 C 函数:
如果你要从 C++ 调用一个 C 函数,通常会这样做:
cpp
// C function declaration
extern "C" void MyCFunction(int x);
// C++ function calling C function
void CallMyCFunction() {
MyCFunction(42);
}
4. 当你使用 extern "C"
时的注意事项
-
extern "C"
仅影响链接和名称修饰,它不改变函数的调用约定。在 C++ 中使用extern "C"
时,链接器仍然会按照 C 风格来处理函数,但你需要确保使用正确的调用约定(例如__stdcall
、__cdecl
等)。 -
C++ 类成员函数 :成员函数不应该使用
extern "C"
,因为它们在内部需要this
指针。在 C++ 中,extern "C"
仅适用于普通函数,而不适用于类成员函数。
总结
- 使用
extern "C"
时,通常是为了避免 C++ 名称修饰,确保符号可以被 C 语言或其他不支持 C++ 名称修饰的语言正确访问。 - C++ 函数导出时 ,如果你需要从其他语言(如 C)或者其他平台(如 Python、Java)调用这些函数时,必须使用
extern "C"
来确保符号在链接时没有 C++ 的名称修饰。
调试遇到一个问题
动态加载和卸载游戏DLL
在上面的对话中,描述了一个关于加载和卸载游戏代码的过程,以下是详细复述:
-
动态加载和卸载游戏代码
讨论了如何在程序运行时按需加载和卸载游戏代码。通过调用特定的功能,可以在某些条件下加载游戏代码,在游戏循环完成后再卸载它。这里提到,游戏代码可以在每次循环中加载和卸载,虽然这种方式会影响性能,但理论上是可行的。
-
加载与卸载的实现
在循环的顶部,首先加载游戏代码,然后在完成游戏循环后卸载它。这个操作会在运行过程中不断重复,游戏代码在每次循环结束后都被加载和卸载。即使这可能会影响帧速率,但可以观察到加载和卸载操作确实在执行。
-
游戏代码的有效性检查与释放
提到在加载游戏代码后,程序会检查它是否有效。如果游戏代码不再有效,则会释放资源并重置状态。这样一来,程序就不会继续保持对游戏代码的引用,从而达到卸载的效果。
-
实验性使用
尽管加载和卸载游戏代码的做法可能影响性能,但可以通过这种方式实现按需使用的效果。这种方法的关键在于游戏代码的动态管理,可以在不需要时卸载,在需要时再重新加载,从而节省资源。
-
性能影响的警告
由于每次加载和卸载都会消耗一定的资源,因此可能会导致帧速率下降。尽管如此,这个操作是可以实现的,只是在实际应用中可能需要优化以减少性能损失。
-
操作的具体步骤
- 加载游戏代码:当程序需要时,通过某种方式加载游戏代码。
- 卸载游戏代码:当游戏循环完成后,立即卸载游戏代码。
- 循环操作:每次在游戏循环结束后进行这些操作,保持代码的动态加载和卸载。
通过这种方法,游戏代码的加载和卸载可以变得更加灵活,能够根据程序的需要动态进行管理,尽管这种方式可能会对性能产生影响。
将局部持久变量从 DLL 移动到游戏状态
上面内容描述了一个关于游戏代码管理、加载和卸载的过程,其中包含了对静态变量的管理、游戏状态的调整,以及加载卸载过程的优化。下面是对这个过程的详细复述:
-
静态变量的问题 :在代码中,仍然存在一个静态变量(如
tSine
符号),它本应该已经被移到游戏状态(game state)中,但实际上并没有。这导致了一个问题,因为这些静态变量作为局部持久性变量存在,每次游戏代码被加载和卸载时,它们都会被重置,这不符合预期。为了避免这种情况,需要移除所有的局部持久性变量,以确保它们不会在每次加载和卸载时重新初始化。 -
解决方案的思路 :为了修复这个问题,代码需要调整
tSine
符号的初始化,确保它正确地在游戏状态中进行初始化,而不是作为静态变量。具体而言,当设置音调(tone)时,tSine
符号应该被设定为零,并且在游戏状态下使用。 -
游戏状态的变化 :调整后,
tSine
符号将不再是一个静态局部变量,而是一个在游戏状态中管理的变量,这样每次调用时都能使用正确的状态,而不受加载和卸载的影响。这个变动确保了游戏代码每次加载时,所有的状态都会根据预期进行更新,避免了不必要的重置。 -
性能考虑:通过优化加载和卸载过程,可以使得加载和卸载的速度非常快,这意味着理论上每一帧都能进行加载和卸载操作。尽管如此,加载和卸载的速度非常快,以至于编译器可能无法及时写入文件,导致某些操作可能无法成功执行。
-
进一步的步骤:接下来需要确保能够正确地写入文件。由于加载和卸载非常快速,编译器可能无法在适当的时机打开文件进行写入。因此,为了实现更高效的操作,还需要进行进一步的调整。在进行这些调整之前,还需要完成一些基础操作。
-
实现的目标:最终的目标是能够在每一帧中执行加载和卸载操作,并通过适当的调整来确保一切按预期工作。虽然这种快速的加载和卸载操作理论上是可行的,但实际实现时需要确保游戏的其他部分不受影响。
-
未来的扩展:未来可能会进行一些更具挑战性的操作,如在运行过程中进行动态修改,例如对代码进行调整(如"将某部分代码上移16个位置")。然而,问题在于,编译器可能无法在代码运行的同时进行写入,因此还需要进一步的调试和优化,以确保这些操作能够顺利执行。
总结来说,问题的核心在于不恰当的静态变量使用和加载卸载过程中的状态重置,解决方案是将关键的状态移到游戏状态中,并优化加载卸载的速度,确保游戏代码在每次加载时都能按预期工作。同时,未来可能涉及更复杂的动态代码修改,但需要保证编译器能够正确地处理这些操作。
避免锁定 DLL,以允许重写
一种动态加载和卸载代码的过程,主要目标是提高代码开发效率,尤其是在调试和更新代码时,避免了每次更改都需要停止程序并重新启动的繁琐操作。这个过程的核心思想是通过在运行时动态地加载和卸载游戏代码,从而使得修改后的代码能即时生效,达到类似脚本语言的灵活性。
关键步骤包括:
-
动态加载和卸载:通过一个负载计数器控制加载频率,在每帧或每一定时间间隔内加载和卸载代码。此方法使得在运行过程中能持续更新,而无需停止整个程序。
-
避免锁定编译器输出文件:使用"复制文件"技术,将原文件复制到新文件中,避免编译器输出文件在加载时被锁定,从而能继续运行代码并进行实时更新。
-
调整文件路径:通过设置当前目录为数据目录或构建目录,解决了路径问题,确保动态链接的代码可以正常读取和写入。
-
解决PDB文件锁定问题:通过调整操作,解决了调试器锁定PDB文件的问题,使得代码能够无障碍地加载和更新。
-
实时反馈和代码编辑:使用这种方法,开发者可以在程序运行时实时编辑代码并看到反馈,模拟了一种类似脚本语言的开发体验。
-
潜在的错误和优化:虽然方法有效,但仍存在一些问题,如程序崩溃后的处理和某些功能的实现可能需要进一步调整和优化。
整体来说,这种方法提供了一个强大的工具,可以大大提高开发效率,尤其是在需要频繁修改和测试代码的情况下。
CopyFileA
是 Windows API 函数,用于将现有文件复制到新的位置。A
表示该函数使用 ANSI 字符集(即传统的单字节字符集),而 Unicode 版本的该函数为 CopyFileW
,后者使用宽字符(Unicode)字符串。
函数原型:
c
BOOL WINAPI CopyFileA(
_In_ LPCSTR lpExistingFileName,
_In_ LPCSTR lpNewFileName,
_In_ BOOL bFailIfExists
);
参数说明:
-
lpExistingFileName (In ):
一个指向以空字符结尾的字符串,指定要复制的源文件路径。
-
lpNewFileName (In ):
一个指向以空字符结尾的字符串,指定目标文件的路径,也就是复制后的文件路径。
-
bFailIfExists (In ):
一个布尔值,指定如果目标文件已存在时应该如何处理:
- TRUE:如果目标文件已存在,则复制操作失败。
- FALSE:如果目标文件已存在,将覆盖目标文件。
返回值:
- 非零值(TRUE):表示文件复制成功。
- 0(FALSE) :表示文件复制失败。可以调用
GetLastError()
来获取详细的错误信息。
使用说明:
lpExistingFileName
和lpNewFileName
都可以是相对路径或绝对路径,确保源文件存在且可访问。- 如果目标文件已存在且
bFailIfExists
为TRUE
,则函数会失败。 - 如果函数返回
FALSE
,可以调用GetLastError()
获取详细的错误信息,例如文件无法访问、磁盘已满等。
问答环节
-LD 用于构建 DLL 并移除增量构建
在 Microsoft Visual C++ (MSVC) 编译器中,/INCREMENTAL
是一个与链接器相关的选项,表示 增量链接。
功能解释
增量链接的目标是加速链接过程。通常,当你编译一个程序后,链接器会将所有目标文件和库重新组合成一个完整的可执行文件或动态库。如果每次改动都需要完全重新链接整个项目,耗时较长。
/INCREMENTAL
选项允许链接器只重新链接那些自上次链接以来发生变化的部分,而不是重新处理所有内容。
主要特点
-
加速调试和开发过程:
- 增量链接通过减少链接所需的时间,加快了编译-调试-迭代的周期,尤其是在处理大型项目时更为显著。
-
适用于开发阶段:
- 该选项通常用于开发和调试阶段,因为它可以节省时间。
- 在最终生成发布版本时,建议关闭增量链接(通过
/INCREMENTAL:NO
),以确保生成的二进制文件更紧凑和高效。
-
输出文件稍大:
- 使用增量链接时,生成的可执行文件或库可能会包含一些额外信息(如重定位信息),从而稍微增加文件大小。
命令行语法
bash
link /INCREMENTAL
默认行为
- 如果不指定,MSVC 通常会根据项目环境自动决定是否启用增量链接:
- 调试模式(Debug) :默认启用
/INCREMENTAL
。 - 发布模式(Release) :默认禁用
/INCREMENTAL
。
- 调试模式(Debug) :默认启用
使用场景
- 启用增量链接 (
/INCREMENTAL
):- 当频繁修改代码并需要快速构建。
- 禁用增量链接 (
/INCREMENTAL:NO
):- 在最终发布时生成更高效、更小的二进制文件。
总结
/INCREMENTAL
是 MSVC 链接器的一个选项,用于加速链接过程,特别适合开发阶段的快速迭代,但建议在发布版本中禁用以优化性能和文件大小。
问题:我们现在的代码很小。动态链接和编译到一个 EXE 在游戏体积巨大(多个 GB)时如何工作?
动态链接在编译过程中,尤其是对于庞大的游戏项目(通常达到数十GB规模)的处理,实际上并不会直接导致巨大的可执行文件。大型游戏的规模之所以显得庞大,通常是因为项目中存在许多需要优化或调整的因素,而不是因为代码本身是错误的。以下是关键点的详细说明:
-
可执行文件的规模限制
无论游戏的复杂程度如何,可执行文件的大小通常不会达到几十GB的规模。这种现象在正常的架构中是完全不会发生的,因为编译和链接的过程会确保生成的可执行文件包含必要的代码和资源引用,而不是直接将所有内容都打包进去。
-
设计和决策的影响
游戏开发中出现大规模内容的原因,更多与编程决策有关。有时,这些决策并不一定是错误的,而可能是项目规模庞大、人员众多且时间紧迫等综合因素下的权衡结果。例如,当项目有数百甚至上千名开发人员参与时,确保每个人都遵循最佳实践是非常困难的。在这种情况下,某些设计上的妥协可能会导致资源膨胀,但仍然是项目顺利推进的最佳选择。
-
架构的重要性
如果在开发过程中采取了正确的架构设计,游戏的资源管理和代码组织会更加高效。这种情况下,无论游戏多么复杂,其可执行文件的规模都可以保持在合理的范围内。而动态链接则能进一步优化运行时的资源利用,将代码和资源分离,以减少不必要的重复和冗余。
-
可扩展性和灵活性
动态链接的核心优势之一是将独立模块与主程序分离。这种方法允许开发者对资源和功能进行模块化设计,从而避免将所有内容直接嵌入可执行文件。这不仅提升了灵活性,还使得开发团队能够更轻松地管理和更新不同的组件。
总结来说,通过合理的架构设计和编程决策,动态链接和编译完全可以有效处理复杂的游戏项目,而不会导致可执行文件规模不合理地膨胀。这种方法对于资源密集型的游戏尤其重要,确保开发过程中资源利用高效且易于维护。
关于循环的简短回顾
循环是编程中一种常见的结构,其本质是一种控制代码执行流程的方法,用于重复执行某段代码,直到某个条件不再满足。以下是对循环及其工作原理的详细说明:
循环的基本概念
-
循环的核心思想
循环的本质是一个流程控制工具,允许程序在某个条件成立时重复执行一段代码。当循环达到某个终点时,它会返回到起点,继续执行代码,直到条件不再成立。
-
循环的本质等价性
从根本上来说,循环可以看作是一种语法糖。它是一种方便的表示方法,其底层实现实际上可以简化为一系列的条件判断(
if
语句)和跳转指令(go to
语句)。高层次语言(如C、Python等)引入循环语法,是为了让代码更加易读和易于维护,但它的底层机制只是跳转和条件检查的结合。
循环的实现方式
以 while
循环为例,它的结构可以概括如下:
- 条件判断:在循环开始时检查某个条件是否为真。如果为真,进入循环体;如果为假,跳出循环。
- 执行代码:在循环体中完成指定任务。
- 返回顶部:执行完任务后,再次检查条件是否为真。
具体流程如下:
- 检查条件是否为真。
- 如果为真,执行代码。
- 执行完代码后,返回顶部,重新检查条件。
- 如果条件为假,直接跳出循环。
这样的过程实际上可以通过如下代码表示:
c
while (something is true) {
// 如果条件成立,执行某些操作
do some stuff;
}
top:
// 如果条件成立,执行某些操作
if (something is true) {
do some stuff;
// 返回顶部,重复检查条件
goto top;
} else {
// 退出循环
}
高级语言中的循环
在高级编程语言中,循环被设计为一种更易于编写和理解的形式,比如:
while
循环:在每次迭代前检查条件。for
循环:将条件检查、初始化和更新逻辑整合为一体。do-while
循环:先执行代码,再检查条件。
它们虽然看起来功能不同,但本质上都可以转化为底层的跳转和条件语句。
循环的哲学
对于理解循环的本质,不需要将它们看作某种复杂或神秘的概念。它们只是编程中的一种工具,本质上是条件判断和跳转的高层封装。如果你对循环的机制感到困惑,可以尝试将它们拆解成一系列的 if
判断和 goto
语句,从底层逻辑的角度理解它们。
总之,循环只是让代码执行重复任务的工具,其底层没有任何特殊之处。它们只是语法上的便利,让开发者能够更轻松地实现逻辑复杂的任务。
现在能否创建一个伪造的 handmade.dll,并用它拦截 GameUpdateAndRender 函数以获取指向游戏内存的指针并修改内容?安全性问题你会在以后的直播中讨论吗?
这段内容讨论了动态加载代码的安全性、威胁模型的理解,以及在游戏开发中安全问题的相关性。以下是更详细的复述和扩展。
动态加载代码与安全性
-
动态加载的实现
动态加载代码的方式通常涉及创建一个"模拟的工具"(dummy hand)工具,这可以用来拦截游戏的更新和渲染功能。例如,通过获取指针访问内存并链接某些数据,可以灵活实现对游戏逻辑的动态修改。这种技术在开发和调试过程中非常有用。
-
安全性的误解
很多人对安全性的理解存在偏差,常常基于一些表面特征来判断系统的安全性,而这些特征可能与实际的安全性无关。
威胁模型的分析
-
威胁模型的定义
在提到威胁模型时,内容假设了一种场景:
- 游戏及其附属工具(如"手工制作工具")是由同一个用户安装的。
- 这些工具和游戏代码位于同一目录下。
在这种情况下,加载多个文件中的代码(例如
.dll
或.exe
文件)不会对安全性造成额外风险。因为如果攻击者能够修改其中一个文件,他们同样有权限修改另一个文件。 -
多文件加载与安全性
- 将代码分布在多个文件中加载(例如从两个文件中动态加载代码),并不会降低整体的安全性。
- 修改权限集中于文件的写权限。如果攻击者能够覆盖一个文件,理论上他们也可以覆盖其他文件,因此加载多个文件并不会增加新的威胁。
游戏开发中的安全性问题
-
沙盒运行的理想
游戏并不是需要广泛互联的应用程序。理论上,它们应该在隔离的沙盒环境中运行,这样即使存在安全漏洞也不会影响系统的整体安全性。
- 游戏设计时不应承担全面的安全责任。
- 理想情况下,操作系统(如 Windows)应该提供足够的沙盒支持,确保游戏即使存在安全漏洞也不会影响系统。
-
现实问题
- 游戏开发需要考虑安全性,这主要是因为现有操作系统的设计缺陷。
- 游戏中需要防范的威胁模型可能涉及病毒检测、动态代码加载以及防止恶意修改。
-
对安全的反思
- 理想的安全机制应该由操作系统负责,而不是由游戏开发者承担。
- 现代游戏开发中,安全问题往往是由于操作系统未能有效隔离游戏进程而产生的。
总结
动态加载代码本身并不会显著增加安全风险,特别是在权限相同的前提下。如果系统设计合理(例如支持沙盒运行),游戏开发者可以专注于核心功能而非安全问题。然而,由于当前环境的限制,游戏开发中仍需谨慎处理动态加载和代码安全的问题。
我们现在有一个小应用,如果编译需要几分钟,如何才能做我们刚才做的事?
这段内容讨论了代码编译时间和编程实践的相关问题,特别强调了编译速度的重要性和良好的编码方式。以下是对这些内容的详细复述:
-
关于编译时间的问题 :
文章开头提到,由于编译时间过长,有人担心自己的问题被忽视。作者反复强调,如果你的代码需要几分钟才能编译,那说明代码写得不对。正常情况下,编译时间应该很短,理想状态下不超过10秒钟。如果编译时间超过10秒钟,就说明代码存在问题,需要检查为什么会这样编写代码。
-
代码编写的建议 :
代码编译应当快速进行,如果需要更多的时间,那就要停下来反思并修改自己的代码方式。作者表示,从未遇到过不能在10秒内编译的复杂问题。编译器和计算机的性能都非常强大,因此,编译时间过长很可能是因为使用了不合适的编程方法,如过度使用模板元编程等会减慢编译速度的技巧。选择这种方式来编写代码是错误的,因为它不仅降低编译速度,还增加了后续的调试和优化难度。
-
模板元编程的危害 :
使用复杂的模板元编程会导致编译器的速度大幅度下降,影响整个开发效率。作者警告说,如果继续使用这些方法,你会丧失其他可能的优化和性能优势。这样做是一个糟糕的编程决定,应该避免。
-
后果和建议 :
如果坚持使用这种不合理的方式,你可能会陷入一个不断恶化的局面,需要采取额外的手段来弥补性能上的问题。比如,可能需要实现一整套脚本语言来优化代码,甚至需要依赖一些不常见的编程技巧。这样不仅增加了开发的复杂度,还可能无法获得原本可以获得的性能优势。为了避免这种情况,作者建议,如果你的代码编译时间超过10秒,就应该立刻停止,重新审视并重构代码。
-
如何优化代码 :
文章最后提到了一种可能的优化方法,分解代码并通过重加载已经改变的部分来缩短编译时间。比如,假设你有大量的代码(例如20GB的代码),你可以将其分解成多个部分,只重新构建和加载那些发生了变化的部分,这样可以大大减少编译时间。然而,这种方法虽然可行,但也伴随了一些复杂的实现细节,可能带来新的问题,因此需要谨慎使用。
-
总结 :
总体来说,作者认为,编译时间过长反映了代码设计和编写上的问题。无论代码规模多大,都应该力求编译速度短而高效,避免因不良设计导致的性能瓶颈。
你能使用 ReadFile() 加载 DLL 并实现 getProcAddress() 吗,假设有一种简单的方法让内存变得可执行,比如使用 mmap()?
这段内容讨论了内存可执行性、调试和代码加载的问题,特别是当涉及到动态链接库(DLL)时,调试器如何处理加载的代码及其调试信息。以下是详细的复述:
-
内存可执行的实现 :
通过
mmap
(内存映射)等方法,确实可以将内存标记为可执行,从而允许程序在运行时加载并执行动态代码。这是一个可以实现的技术,但作者选择不这么做,主要是因为希望调试器能够理解代码的执行过程。 -
调试器与可执行文件 :
调试器通过查看可执行文件及其调试信息来运行。当加载一个动态链接库时,它引入了一段新的代码,这段代码的调试信息并不包含在主可执行文件的调试信息中。因此,调试器必须去寻找并加载这些新代码的调试信息。
-
加载机制 :
在 Windows 等操作系统中,动态库的加载是通过标准机制进行的。这些标准机制能够让调试器察觉到加载的库并识别它们。当调试器看到动态链接库的加载事件时,它会自动查找相应的调试信息,从而支持对加载代码的调试。
-
绕过标准加载机制的风险 :
如果自己实现代码加载过程(而不是通过操作系统的标准机制),调试器将无法察觉到新加载的代码。这样,调试器就无法为这段代码提供调试信息,也无法逐步调试这段新加载的代码。这会导致调试过程变得复杂,因为调试器无法理解加载代码的存在。
-
解决方案 :
为了弥补这种缺失,可以选择编写一个简单的服务器来连接调试器。这种服务器可以"假装"或伪造加载的代码符号,使调试器认为这些符号就像是通过标准方式加载的代码一样。这种做法虽然可行,但它需要额外的开发工作和对调试器的深度定制。
-
为什么选择标准加载机制 :
使用标准的加载方式(如 Windows 的标准机制)来加载代码,是因为这能够让调试器正确理解并处理加载的代码。当通过标准信道加载代码时,调试器能够识别并加载相应的调试信息,从而有效支持调试过程。如果绕过这种标准机制,调试过程将变得更加复杂。
-
跨平台的实现 :
虽然这里的例子以 Windows 为基础,但相同的原理也可以应用到其他操作系统上,比如 Linux。在 Linux 上,也会采用类似的加载和调试机制,尽管它的实现方式可能有所不同。
总之,主要的观点是,使用标准的加载机制来加载代码可以保证调试器能够有效地提供调试支持。如果决定绕过这些标准机制,可能会面临更多的调试挑战,需要额外的工作来确保调试器理解并支持加载的代码。
如果不使用 DLL 和 EXE 之间的大内存块传递,这种技术还可行吗?
这段讨论讲述了在不使用传统的"大内存块"管理的情况下,如何确保内存管理仍然能够正常工作,特别是在加载和卸载动态链接库(DLL)时。
以下是详细的复述:
-
内存分配和管理的基本问题 :
该技术的核心是确保内存的管理是有效的,并且能够避免使用每一个大内存块。在这种情况下,关键是确保内存的分配始终来自主可执行文件,这样就不会产生问题。即使涉及到加载和卸载的动态链接库,只要内存来自可执行文件的分配,系统就可以正常工作。
-
在Windows上的默认行为 :
在Windows系统中,通常,当动态链接库加载时,它会从主可执行文件获取内存分配,并且当库被卸载时,它会释放这部分内存。这是一个默认行为,这意味着只要代码遵循这种模式,内存管理就不会有问题。
-
静态链接的C运行时库 :
如果使用的是静态链接的C运行时库(CRT),可能会遇到问题,特别是在内存分配和释放过程中。若从动态链接库直接调用
new
或malloc
,当动态库被卸载时,内存可能会变得不可用或"炸掉"(即内存被错误释放或无效)。这表明静态链接的C运行时库在动态库加载和卸载时可能会造成内存管理的问题。 -
使用动态链接C运行时库 :
为了避免上述问题,可以选择使用动态链接的C运行时库(CRT)。这种方式下,主可执行文件和加载的动态库共享同一个C运行时库,因此内存管理变得更加一致和安全。这样,两者都通过同一个运行时库进行内存管理,可以避免内存泄漏或非法访问的问题。
-
构建时的调整 :
为了实现动态链接的C运行时库,可能需要在构建过程中进行特定的配置调整。例如,需要将编译选项从
-mt
(静态链接)修改为-md
(动态链接)。这要求开发人员有一定的构建配置经验。 -
总结 :
总的来说,这种技术确实是可行的,不需要"大内存块"。然而,尽管它是可行的,但并不是没有代价的。开发者需要谨慎地处理内存的来源,并确保它来自正确的位置------即来自主可执行文件的分配。否则,当动态链接库被卸载时,内存可能会被错误清理或无法访问,这可能导致程序崩溃或错误行为。
因此,尽管能够实现这种技术,但需要仔细规划和设计内存管理的细节,确保内存的分配和释放按预期执行,以避免潜在的问题。
DLL 会阻止编译器内联吗?
这段讨论主要围绕防止编译器内联的技术,特别是在涉及跨越函数边界时的影响。
详细复述:
-
防止编译器内联 :
目的是防止编译器对某些函数进行内联操作(即将函数调用直接替换为函数体的代码),尤其是在跨越函数边界时。内联通常能提高性能,但有时会导致问题,特别是当跨越较大边界(如不同模块或函数的边界)时。
-
函数边界的影响 :
在某些情况下,内联可能会使得编译器跨越不同的函数边界进行优化,这是不希望出现的情况。特别是当一个函数涉及到复杂的逻辑,跨越模块的边界进行优化可能会破坏预期的行为或导致不必要的性能开销。
-
游戏的结构 :
在讨论中提到,整个游戏的逻辑都运行在一个特定的函数中。在这个函数中,游戏的更新、渲染等所有操作都被调用并处理。因此,所有的游戏操作都发生在一个集中点,避免了编译器进行不必要的内联跨越函数边界。
-
不需要担心内联 :
因为游戏逻辑已经在一个函数内集中处理,所以不需要担心编译器内联可能会跨越这些边界。所有的游戏处理都集中在这个函数中,避免了性能或行为上的问题。
-
在交易中使用多个小函数的潜在问题 :
如果出于某种原因,开发者希望在一个动态链接库(dll)中使用很多小函数,并且希望这些函数能从平台层快速回调,编译器的内联优化可能会带来问题。因为内联操作会使得这些小函数的调用被直接展开,这可能导致无法保持预期的效率或代码行为。
-
解决方案 :
为了避免出现上述问题,建议避免将大量的小函数放入动态链接库中,并期望编译器的内联优化能提高性能。如果这样做,可能会导致无法达到预期效果,且会使得程序变得更复杂,不容易调试或维护。
总结:
该段文字的核心思想是,避免在不适当的情况下依赖编译器内联,特别是在跨越函数或模块边界时。如果整个程序逻辑都能集中在一个函数中,就不必担心内联的负面影响。避免在动态库中设计过多的小函数,并依赖内联来提高性能,避免这种做法会更可靠。
我们如何在 Linux/Mac 上实现 DLL?
这段对话涉及到如何在 Linux 和 Mac 上实现"交易"(可能是指动态链接库或模块的加载),并讨论了类似的实现方式。
详细复述:
-
Linux 和 Mac 上的交易(交易模块) :
讨论中提到,Linux 和 Mac 系统也有类似于 Windows 的共享库机制。尽管命名和实现方式可能不同,但它们的核心原理是相似的。即操作系统允许通过动态链接的方式加载共享库,在运行时将外部代码引入主程序中,允许代码在运行时动态地调用这些库。
-
共享库 :
在 Linux 和 Mac 上,共享库通常是通过
.so
(共享对象)文件在 Linux 中实现的,而在 Mac 上则使用.dylib
文件。这些文件类似于 Windows 上的 DLL(动态链接库)。它们包含可以被多个程序共享和调用的代码。这样做的目的是为了提高效率和代码复用性。 -
不同的调用方式 :
尽管这些系统都支持共享库机制,它们的具体实现和调用方式可能不同。例如,Linux 和 Mac 系统中的动态加载方式可能采用不同的系统调用或链接机制,但整体的功能和目标是相似的,即实现动态代码加载。
总结:
讨论的主要内容是 Linux 和 Mac 上的共享库机制,它们通过动态链接库的方式实现与 Windows 类似的功能,允许代码在运行时被动态加载。尽管不同操作系统使用不同的文件类型和调用机制,但基本原理是相同的,即共享库允许程序通过链接和加载外部代码来扩展功能。而讨论中提到的手工制作和剧集指南可能是指另外一个独立的非技术话题。
在 Linux 上共享库的过程大致相同吗?
这段对话讨论了将共享库的技术从 Windows 移植到 Linux 的过程,主要集中在如何处理函数导出和库加载,涉及到 GCC 的使用以及一些资源的推荐。
详细复述:
-
Linux 和 Windows 共享库的相似性 :
讨论中提到,Windows 和 Linux 在处理共享库时的基本流程是类似的,尽管操作系统的具体实现和工具链不同。Linux 中使用的共享库是通过
gcc
编译器创建的,而 Windows 使用的是类似的 DLL 文件。虽然这两个系统的具体细节有所不同,但整体思路和原理是相通的,因此可以通过一定的时间学习和查阅文档来解决问题。 -
GCC 文档的作用 :
提到使用 GCC 的文档来实现这个过程,GCC 是 Linux 上的标准编译器,能够帮助用户编译出动态链接库(共享库)。文档中会详细介绍如何导出函数、创建动态库等内容,这是实现 Linux 版本的关键步骤。通过这些文档,用户可以找到如何将代码编译成共享库并导出所需的功能。
-
寻找具体的步骤 :
讨论中提到的具体步骤包括:
- 导出函数:首先需要知道如何从代码中导出函数。
- 使用编译器:通过编译器生成动态库,并确保所需的函数被正确导出。
- 获取过程地址 :然后需要使用相应的方法获取库中的函数地址,类似于 Windows 中通过
GetProcAddress
获取 DLL 中函数地址的方式。 - 加载和释放库:通过合适的系统调用,动态加载和卸载这些共享库。
-
资源推荐 :
提到一个名为"手工企鹅"的资源,这是一个与编码相关的项目或学习平台,可能提供了类似的技术实现和详细指导,特别是针对 Linux 的部分。虽然对 Linux 的了解不如对其他平台的了解深入,但通过这些资源,可以帮助开发者快速上手,并且可能已经有一些相关的实现示例。
-
不需要太长时间 :
讨论中表示,虽然有些不确定性,但实现这个过程并不需要太长时间。只需要明确三个关键步骤:如何导出函数、如何创建动态库、以及如何加载并调用这些库。
-
未来计划 :
最后提到,明天会进行一些更复杂的操作,可能需要更多的工具或知识,但如果只是想要实现基本功能,以上步骤已经足够。
总结:
本段讨论了将 Windows 上的共享库加载技术迁移到 Linux 的方法。通过使用 GCC 编译器和相关文档,开发者可以创建动态链接库并导出函数,然后通过系统调用加载这些库。实现这个过程所需的关键步骤包括导出函数、创建动态库、获取过程地址等。这一过程不复杂,可以在较短时间内完成,并且推荐了一些学习资源,帮助进一步深入理解和实现这些功能。