C++中Raylib结合热重载的链接问题(Windows+Linux平台)
问题:Raylib热重载动态库触发内存访问违规崩溃
场景概述
刚接触C++,基于Raylib开发游戏,尝试将核心逻辑拆分到动态库实现热重载:主程序加载动态库并调用其接口,动态库仅实现绘制矩形和帧率显示功能。项目编译链接正常,但运行时窗口短暂打开后崩溃,崩溃点在调用动态库中的UpdateGame函数时,触发内存访问违规(指向Raylib内部RLGL指针),且此时IsWindowReady()返回false,怀疑Raylib被重复加载。
相关代码
main.cpp
#include "raylib.h" #include "platform.cpp" int screenWidth = 1920; int screenHeight = 1080; int main() { GameAPI gameAPI = {}; GameLib gameLib = LoadGameLibrary(&gameAPI); if (gameLib == nullptr) { TraceLog(LOG_ERROR, "Could not load game API"); return -1; } Game* game = gameAPI.createGame(); if (!game) { TraceLog(LOG_ERROR, "Could not create game!"); FreeGameLibrary(gameLib); return -1; } InitWindow(screenWidth, screenHeight, "Raylib"); // Main game loop while (!WindowShouldClose()) { BeginDrawing(); ClearBackground(WHITE); if (IsWindowReady()) { gameAPI.updateGame(game); } else { TraceLog(LOG_ERROR, "Window not ready!"); } EndDrawing(); } gameAPI.destroyGame(game); FreeGameLibrary(gameLib); }
platform.hpp & platform.cpp
// platform.hpp #ifdef _WIN32 #define NOGDI // All GDI defines and routines #define NOUSER // All USER defines and routines #undef near #undef far #define WIN32_LEAN_AND_MEAN #include "Windows.h" typedef HMODULE GameLib; #else #include <dlfcn.h> typedef void* GameLib; #endif #include "game.hpp" typedef Game* (*CreateGameFunc)(); typedef void (*UpdateGameFunc)(Game*); typedef void (*DestroyGameFunc)(Game*); struct GameAPI { CreateGameFunc createGame; UpdateGameFunc updateGame; DestroyGameFunc destroyGame; }; GameLib LoadGameLibrary(GameAPI* gameAPI); void FreeGameLibrary(void* gameLib); // platform.cpp #include "platform.hpp" #ifdef _WIN32 GameLib LoadGameLibrary(GameAPI* gameAPI) { GameLib gameLib = (GameLib) LoadLibraryA("game.dll"); if (!gameLib) { // Handle error return nullptr; } gameAPI->createGame = (CreateGameFunc)GetProcAddress(gameLib, "CreateGame"); gameAPI->updateGame = (UpdateGameFunc)GetProcAddress(gameLib, "UpdateGame"); gameAPI->destroyGame = (DestroyGameFunc)GetProcAddress(gameLib, "DestroyGame"); if (!gameAPI->createGame || !gameAPI->updateGame || !gameAPI->destroyGame) { return nullptr; } else { return gameLib; } } void FreeGameLibrary(GameLib gameLib) { FreeLibrary(gameLib); } #endif
game.hpp & game.cpp
// game.hpp #ifndef GAME_HPP #define GAME_HPP #define EXTERNAL extern "C" #ifdef _WIN32 #ifdef GAME_EXPORTS #define GAME_API __declspec(dllexport) #else #define GAME_API __declspec(dllimport) #endif #else #define GAME_API #endif EXTERNAL class GAME_API Game { public: void Update(); }; EXTERNAL GAME_API Game* CreateGame(); EXTERNAL GAME_API void DestroyGame(Game* game); EXTERNAL GAME_API void UpdateGame(Game* game); #endif // GAME_HPP // game.cpp void Game::Update() { // Update logic that uses Raylib functions // At this point IsWindowReady() return false for some reason? DrawFPS(10, 10); DrawRectangle(100, 100, 200, 200, GREEN); } // Macro to export using __declspec(dllexport) extern "C" EXTERNAL void UpdateGame(Game *game) { game->Update(); } // 其他CreateGame/DestroyGame实现(无崩溃风险,略)
CMakeLists.txt
cmake_minimum_required(VERSION 3.24) # FetchContent is available in 3.11+ project(Raylib) if (CMAKE_SYSTEM_NAME STREQUAL "Darwin") error("MacOS is not supported") endif() # Generate compile_commands.json set(CMAKE_EXPORT_COMPILE_COMMANDS ON) set(CMAKE_EXPORT_COMPILE_COMMANDS ON CACHE INTERNAL "") # works set(CMAKE_CXX_STANDARD 20) # Dependencies set(RAYLIB_VERSION 5.0) find_package(raylib ${RAYLIB_VERSION} QUIET) # QUIET or REQUIRED if (NOT raylib_FOUND) # If there's none, fetch and build raylib include(FetchContent) FetchContent_Declare( raylib DOWNLOAD_EXTRACT_TIMESTAMP OFF URL https://github.com/raysan5/raylib/archive/refs/tags/${RAYLIB_VERSION}.tar.gz ) FetchContent_GetProperties(raylib) if (NOT raylib_POPULATED) # Have we downloaded raylib yet? set(FETCHCONTENT_QUIET YES) FetchContent_Populate(raylib) set(BUILD_SHARED_LIBS ON CACHE BOOL "" FORCE) add_subdirectory(${raylib_SOURCE_DIR} ${raylib_BINARY_DIR}) endif() endif() if (CMAKE_SYSTEM_NAME STREQUAL "Windows") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -I\"C:/Program Files (x86)/Windows Kits/10/Include/10.0.22621.0/um\"") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -I\"C:/Program Files (x86)/Windows Kits/10/Include/10.0.22621.0/shared\"") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -I\"C:/Program Files (x86)/Windows Kits/10/Include/10.0.22621.0/ucrt\"") if (NOT EXISTS "${CMAKE_BINARY_DIR}/raylib.dll") file(COPY ${raylib_BINARY_DIR}/raylib/raylib.dll DESTINATION ${CMAKE_BINARY_DIR}) endif() else() if (NOT EXISTS "${CMAKE_BINARY_DIR}/raylib.so") file(COPY ${raylib_BINARY_DIR}/raylib/raylib.so DESTINATION ${CMAKE_BINARY_DIR}) endif() endif() # Project add_executable(${PROJECT_NAME} ./src/main.cpp) target_link_libraries(${PROJECT_NAME} PRIVATE raylib) target_include_directories(${PROJECT_NAME} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) # Game dynamic library for hot reloading add_library(game SHARED ./src/game.cpp) target_compile_definitions(game PRIVATE GAME_EXPORTS) target_link_libraries(game PRIVATE raylib) # Custom target to build only the game library add_custom_target(game_only DEPENDS game COMMAND ${CMAKE_COMMAND} --build ${CMAKE_BINARY_DIR} --target game )
解决方案
1. 修正extern "C"的错误使用
extern "C"不能修饰C类,这会导致符号混乱,类的成员函数无法以C链接方式导出。需将C类包装为C风格接口:
// 修改后的game.hpp #ifndef GAME_HPP #define GAME_HPP #ifdef _WIN32 #ifdef GAME_EXPORTS #define GAME_API __declspec(dllexport) #else #define GAME_API __declspec(dllimport) #endif #else #define GAME_API #endif // 仅用extern "C"包裹C风格导出函数 extern "C" { // 前向声明,隐藏类内部细节 typedef struct Game Game; GAME_API Game* CreateGame(); GAME_API void DestroyGame(Game* game); GAME_API void UpdateGame(Game* game); } // 内部C++类,不对外导出 class Game { public: void Update(); }; #endif // GAME_HPP
2. 调整Raylib初始化与动态库加载顺序
当前先加载动态库再初始化Raylib,动态库加载时若触发静态初始化,会在Raylib全局状态就绪前访问其资源,导致状态异常。需先初始化窗口,再加载动态库:
// 修改后的main.cpp核心逻辑 int main() { // 先初始化Raylib窗口 InitWindow(screenWidth, screenHeight, "Raylib"); GameAPI gameAPI = {}; GameLib gameLib = LoadGameLibrary(&gameAPI); if (gameLib == nullptr) { TraceLog(LOG_ERROR, "Could not load game API"); CloseWindow(); // 新增:失败时关闭窗口 return -1; } Game* game = gameAPI.createGame(); if (!game) { TraceLog(LOG_ERROR, "Could not create game!"); FreeGameLibrary(gameLib); CloseWindow(); // 新增:失败时关闭窗口 return -1; } // 主循环逻辑不变... gameAPI.destroyGame(game); FreeGameLibrary(gameLib); CloseWindow(); // 新增:退出前关闭窗口 }
3. 优化CMake配置
- 移除Windows硬编码头文件路径,CMake会自动处理Windows SDK依赖
- 使用
add_custom_command自动同步Raylib共享库,避免手动复制遗漏:
// 替换原Windows/Linux库复制逻辑 if (CMAKE_SYSTEM_NAME STREQUAL "Windows") add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_if_different $<TARGET_FILE:raylib> $<TARGET_FILE_DIR:${PROJECT_NAME}> ) add_custom_command(TARGET game POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_if_different $<TARGET_FILE:raylib> $<TARGET_FILE_DIR:game> ) else() add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_if_different $<TARGET_FILE:raylib> $<TARGET_FILE_DIR:${PROJECT_NAME}> ) add_custom_command(TARGET game POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_if_different $<TARGET_FILE:raylib> $<TARGET_FILE_DIR:game> ) endif()
4. 确保Raylib全局状态共享
动态库和主程序必须使用同一实例的Raylib共享库,CMake中已设置BUILD_SHARED_LIBS ON,需保证编译时动态库与主程序链接的Raylib版本完全一致,避免符号冲突。
核心原因总结
崩溃的本质是:动态库加载时机过早,导致其Raylib函数未关联到主程序初始化的全局状态;同时extern "C"错误修饰C++类,引发符号链接异常,最终导致访问无效的RLGL指针。通过调整加载顺序、修正接口定义、优化CMake配置,即可解决问题。
内容的提问来源于stack exchange,提问作者Archie Murray
相关产品推荐
相关产品推荐

