C语言跨平台开发Linux Windows macOS 嵌入式多平台编译配置实战避坑经验详解
做C语言跨平台开发的人,多少都经历过那种”在我电脑上明明能跑啊”的崩溃时刻。我带过不少团队,也自己踩过无数坑,今天就把这些血泪经验掰开揉碎讲给你听。
先说说为什么要跨平台
想象一下,你花半年写了一个好东西,结果只能跑在Linux上,Windows用户想夸你都找不到入口。跨平台开发就是让一份代码,尽量多地跑在不同系统上,少写一遍重复代码。
但这事没那么简单。不同平台的编译器、头文件、函数名、路径分隔符、线程模型、文件系统都有差异,稍不注意就踩雷。
编译器的选择——你的第一道坎
不同平台的主流编译器完全不一样:
Linux 上基本是 GCC 或者 Clang,这两个其实长得差不多,因为 Clang 一开始就是冲着兼容 GCC 的选项来的。
macOS 上 Xcode 自带的 clang 是主力,但也可以用 Homebrew 装 GCC。
Windows 上情况最复杂:
- MSVC(Visual C++)是微软自家编译器,随 Visual Studio 安装
- MinGW 是 Windows 上的 GCC 移植版
- MinGW-w64 是 MinGW 的改进版,支持 64 位
- Clang for Windows 也有人在用
嵌入式 平台更夸张,ARM 用 ARM Compiler 或 GCC ARM Embedded,RISC-V 用 riscv-gnu-toolchain,不同芯片厂商还可能有自己的编译器。
我见过最坑的场景:代码在 GCC 下编译通过,换到 MSVC 下一堆 error,因为 MSVC 对 C99 的支持一直很保守,连 for (int i = 0; ...) 这种写法在旧版 VS 里都要报错。
实战:用 CMake 管理多平台编译
CMake 是跨平台编译的事实标准,它能生成不同平台的构建文件。先建一个最基础的项目结构:
myproject/
├── CMakeLists.txt
├── src/
│ ├── main.c
│ └── platform.c
├── include/
│ ├── common.h
│ └── platform.h
└── tests/
└── test_main.c
CMakeLists.txt 是整个项目的核心配置文件:
cmake_minimum_required(VERSION 3.16)
project(MyProject VERSION 1.0.0 LANGUAGES C)
# 设置C标准,这里选C11比较通用
set(CMAKE_C_STANDARD 11)
set(CMAKE_C_STANDARD_REQUIRED ON)
# 输出目录设置,避免污染源码目录
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin)
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib)
set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib)
# 添加可执行文件
add_executable(myapp
src/main.c
src/platform.c
)
# 包含头文件目录
target_include_directories(myapp PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/include
)
# 链接pthread(跨平台处理见下文)
if(UNIX)
find_package(Threads REQUIRED)
target_link_libraries(myapp PRIVATE Threads::Threads)
endif()
# 添加测试
enable_testing()
add_executable(test_app tests/test_main.c)
target_link_libraries(test_app PRIVATE Threads::Threads)
add_test(NAME test_app COMMAND test_app)
注意几个细节:
cmake_minimum_required不要设太低,3.16 是个比较合理的下限,太旧的系统会遇到兼容问题CMAKE_RUNTIME_OUTPUT_DIRECTORY这种设置能保持构建目录干净整洁,不然几十个子项目的可执行文件全堆在根目录会让你怀疑人生
头文件差异——平台条件编译的正确姿势
这是跨平台开发的核心技术。你不可能指望所有平台的头文件都一样,所以要用条件编译来隔离差异。
先建立一个 platform.h:
#ifndef PLATFORM_H
#define PLATFORM_H
#include <stdint.h>
#include <stdbool.h>
/* 平台检测宏 */
#if defined(_WIN32) || defined(_WIN64)
#define PLATFORM_WINDOWS
#define PLATFORM_NAME "Windows"
#include <windows.h>
typedef LONG64 platform_long_t;
#elif defined(__linux__)
#define PLATFORM_LINUX
#define PLATFORM_NAME "Linux"
#include <unistd.h>
#include <pthread.h>
typedef long platform_long_t;
#elif defined(__APPLE__)
#define PLATFORM_MACOS
#define PLATFORM_NAME "macOS"
#include <pthread.h>
#include <mach/mach_time.h>
typedef long platform_long_t;
#elif defined(__arm__) || defined(_M_ARM)
#define PLATFORM_EMBEDDED_ARM
#define PLATFORM_NAME "Embedded ARM"
typedef long platform_long_t;
#else
#error "Unsupported platform"
#endif
/* 路径分隔符 */
#ifdef PLATFORM_WINDOWS
#define PATH_SEPARATOR '\\'
#define PATH_SEPARATOR_STR "\\"
#else
#define PATH_SEPARATOR '/'
#define PATH_SEPARATOR_STR "/"
#endif
/* 文件模式 */
#ifdef PLATFORM_WINDOWS
#define FOPEN_READ "rb"
#define FOPEN_WRITE "wb"
#else
#define FOPEN_READ "r"
#define FOPEN_WRITE "w"
#endif
/* 线程相关 */
#ifdef PLATFORM_WINDOWS
#define PLATFORM_THREAD_HANDLE HANDLE
#define PLATFORM_THREAD_FUNC DWORD WINAPI
#define PLATFORM_THREAD_PARAM const void*
#define PLATFORM_CREATE_THREAD(handle, func, param) \
((handle) = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)(func), (void*)(param), 0, NULL)) ? (DWORD)0 : (DWORD)-1
#define PLATFORM_JOIN_THREAD(handle) WaitForSingleObject((handle), INFINITE)
#define PLATFORM_CLOSE_HANDLE(handle) CloseHandle((handle))
#else
#define PLATFORM_THREAD_HANDLE pthread_t
#define PLATFORM_THREAD_FUNC void*
#define PLATFORM_THREAD_PARAM void*
#define PLATFORM_CREATE_THREAD(handle, func, param) pthread_create(&(handle), NULL, (func), (param))
#define PLATFORM_JOIN_THREAD(handle) pthread_join((handle), NULL)
#define PLATFORM_CLOSE_HANDLE(handle)
#endif
/* 跨平台打印格式 */
#ifdef PLATFORM_WINDOWS
#define PRIdPTR "I64d"
#define PRIuPTR "I64u"
#else
#define PRIdPTR "ld"
#define PRIuPTR "lu"
#endif
#endif /* PLATFORM_H */
这段代码里藏了几个经典坑:
坑一:_WIN32 和 _WIN64 的关系
Windows 64 位编译时,_WIN32 和 _WIN64 都会被定义。所以判断 Windows 平台用 #ifdef _WIN32 就足够了,不用额外判断 _WIN64。这是一个很多人搞混的点。
坑二:long 类型在不同平台的位数
Linux 64位系统上 long 是 64 位,但 Windows 上 long 永远是 32 位。这就是为什么代码里我定义了一个 platform_long_t 的别名,实际开发中要特别注意这种隐式类型差异,用 int64_t 和 uint64_t 才是王道。
坑三:文件打开模式
Windows 的文本模式和二进制模式有本质区别——Windows 读文本文件时会自动把 \n 转成 \r\n,写的时候反过来。如果不加 b,处理二进制数据时会出现乱码甚至数据损坏。而 Unix 系系统不区分这两种模式。
字符串和路径处理——跨平台重灾区
这是实际开发中最容易踩坑的地方,没有之一。
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include "platform.h"
/* 跨平台的文件路径拼接 */
char* join_path(const char* dir, const char* filename) {
size_t dir_len = strlen(dir);
size_t file_len = strlen(filename);
/* 分配空间:dir + separator + filename + null */
char* result = (char*)malloc(dir_len + 1 + file_len + 1);
if (!result) return NULL;
memcpy(result, dir, dir_len);
result[dir_len] = PATH_SEPARATOR;
memcpy(result + dir_len + 1, filename, file_len + 1);
return result;
}
/* 跨平台的绝对路径获取(简化版) */
char* get_current_dir(void) {
char* buffer = NULL;
#ifdef PLATFORM_WINDOWS
buffer = (char*)malloc(MAX_PATH);
if (buffer && GetFullPathName(".", MAX_PATH, buffer, NULL)) {
/* Windows返回的是\,统一转成/更方便处理 */
for (char* p = buffer; *p; p++) {
if (*p == '\\') *p = '/';
}
return buffer;
}
#else
buffer = getcwd(NULL, 0); /* POSIX扩展,自动分配内存 */
if (buffer) return buffer;
#endif
free(buffer);
return NULL;
}
/* 跨平台的毫秒级时间戳 */
unsigned long long get_timestamp_ms(void) {
#ifdef PLATFORM_WINDOWS
FILETIME ft;
GetSystemTimeAsFileTime(&ft);
unsigned long long ticks = ((unsigned long long)ft.dwHighDateTime << 32) | ft.dwLowDateTime;
/* FILETIME是1601年1月1日到现在的100纳秒数 */
return (ticks / 10000) - 11644473600000ULL;
#else
struct timespec ts;
clock_gettime(CLOCK_REALTIME, &ts);
return (unsigned long long)(ts.tv_sec) * 1000ULL + (unsigned long long)(ts.tv_nsec) / 1000000ULL;
#endif
}
几个值得注意的点:
路径分隔符问题:Windows 用反斜杠 \,其他平台用正斜杠 /。好消息是,GCC 和 MSVC 实际上都接受正斜杠作为路径分隔符,所以如果你只在内部用,全部用 / 反而更省事。上面的代码里 GetFullPathName 返回的是反斜杠,我做了一次统一转换。
getcwd 的跨平台写法:POSIX 标准允许 getcwd(NULL, 0) 让系统自动分配内存,但 Windows 没有这个写法。这就是为什么要用条件编译隔离平台差异。
时间戳的坑:Windows 的 GetSystemTimeAsFileTime 返回的是”文件时间”,起点是 1601 年 1 月 1 日(UTC),而 Unix 时间戳起点是 1970 年 1 月 1 日。两者差了 11644473600 秒,这个常数是固定的,跨平台传递时间戳时一定要统一转换成同一基准。
线程和并发——每个平台都有自己的脾气
线程是跨平台开发中难度最高的部分之一。POSIX 线程(pthread)和 Windows 线程API完全不同。
#include <stdio.h>
#include <stdlib.h>
#include "platform.h"
typedef struct {
int id;
int data[1000];
} TaskData;
/* 线程函数 */
PLATFORM_THREAD_FUNC worker_thread(PLATFORM_THREAD_PARAM param) {
TaskData* task = (TaskData*)param;
/* 模拟一些计算 */
int sum = 0;
for (int i = 0; i < 1000; i++) {
sum += task->data[i];
}
printf("Thread %d: sum = %d\n", task->id, sum);
return 0;
}
/* 跨平台线程管理 */
typedef struct {
PLATFORM_THREAD_HANDLE handle;
bool running;
} PlatformThread;
bool platform_thread_create(PlatformThread* thread, void* (*start)(void*), void* arg) {
int ret = PLATFORM_CREATE_THREAD(thread->handle, start, arg);
if (ret == 0) {
thread->running = true;
return true;
}
return false;
}
void platform_thread_join(PlatformThread* thread) {
if (thread->running) {
PLATFORM_JOIN_THREAD(thread->handle);
PLATFORM_CLOSE_HANDLE(thread->handle);
thread->running = false;
}
}
/* 使用示例 */
int main(void) {
printf("Running on %s\n", PLATFORM_NAME);
PlatformThread threads[4];
TaskData tasks[4];
for (int i = 0; i < 4; i++) {
tasks[i].id = i;
for (int j = 0; j < 1000; j++) {
tasks[i].data[j] = j;
}
if (!platform_thread_create(&threads[i], worker_thread, &tasks[i])) {
printf("Failed to create thread %d\n", i);
return 1;
}
}
for (int i = 0; i < 4; i++) {
platform_thread_join(&threads[i]);
}
printf("All threads completed.\n");
return 0;
}
这里的关键设计思路是抽象:把所有平台差异藏在 platform.h 和对应的实现里,上层代码完全不用关心底层是 pthread 还是 Windows Thread API。
坑四:线程返回值的处理
Windows 的线程函数返回 DWORD,而 POSIX 线程函数返回 void*。在 worker_thread 里我返回了 0,这在两种平台上都能正常工作(0 可以隐式转换成 DWORD 也可以转换成 (void*)0)。但如果你想在线程间传递数据指针,Windows 和 POSIX 的处理方式又有微妙差异,需要小心。
动态链接库——.so .dll .dylib 的战争
不同平台动态库的扩展名不同,加载方式也不同:
/* library_loader.h - 跨平台动态库加载 */
#ifndef LIBRARY_LOADER_H
#define LIBRARY_LOADER_H
#include "platform.h"
#include <stdio.h>
#ifdef PLATFORM_WINDOWS
#include <windows.h>
typedef HMODULE PlatformLibrary;
typedef FARPROC PlatformSymbol;
#else
#include <dlfcn.h>
typedef void* PlatformLibrary;
typedef void* PlatformSymbol;
#endif
typedef PlatformSymbol (*FunctionPointer)(void);
typedef struct {
PlatformLibrary handle;
char error[256];
bool loaded;
} DynamicLibrary;
static inline bool dl_open(DynamicLibrary* lib, const char* path) {
#ifdef PLATFORM_WINDOWS
lib->handle = LoadLibraryA(path);
if (lib->handle == NULL) {
DWORD err = GetLastError();
sprintf(lib->error, "LoadLibrary failed with error %lu", err);
lib->loaded = false;
return false;
}
#else
/* 关闭之前打开的错误报告 */
dlerror();
lib->handle = dlopen(path, RTLD_NOW);
const char* err = dlerror();
if (err != NULL) {
snprintf(lib->error, sizeof(lib->error), "dlopen failed: %s", err);
lib->loaded = false;
return false;
}
#endif
lib->loaded = true;
return true;
}
static inline PlatformSymbol dl_symbol(DynamicLibrary* lib, const char* name) {
#ifdef PLATFORM_WINDOWS
return GetProcAddress(lib->handle, name);
#else
return dlsym(lib->handle, name);
#endif
}
static inline void dl_close(DynamicLibrary* lib) {
if (lib->loaded && lib->handle) {
#ifdef PLATFORM_WINDOWS
FreeLibrary(lib->handle);
#else
dlclose(lib->handle);
#endif
lib->handle = NULL;
lib->loaded = false;
}
}
#endif /* LIBRARY_LOADER_H */
坑五:Windows DLL 的导出符号
Windows 的动态链接库需要用 __declspec(dllexport) 来导出符号,而在调用方需要用 __declspec(dllimport) 来导入。这个宏管理起来很麻烦,通常的做法是定义一个统一的宏:
#ifdef BUILDING_MYLIB
#define MYLIB_API __declspec(dllexport)
#else
#define MYLIB_API __declspec(dllimport)
#endif
然后在使用时:
MYLIB_API int my_function(int x);
这是一个非常容易忘的坑。如果你在 Linux 上写好代码,直接拿到 Windows 上用 MSVC 编译,会发现所有导出的函数都找不到。
坑六:符号名称修饰(Name Mangling)
Windows 和 Linux 对函数名的处理完全不同。Linux 的符号名通常就是函数名,而 Windows 的导出符号会被编译器加上前缀或后缀(比如 _my_function@8 这种)。如果你用 dlsym 或者 GetProcAddress 来查找符号,一定要用正确的名称。最简单的办法是在 C++ 代码里用 extern "C" 来禁止名称修饰,但在纯 C 项目里这个问题不存在。
端序(Endianness)——容易被忽视的隐形杀手
大端序和小端序是跨平台开发中最隐蔽的问题之一。x86 和 ARM 通常是小端序,而网络传输用的是大端序(网络字节序)。如果你的数据要在不同架构之间传输或持久化存储,这个问题会悄无声息地毁掉你的数据。
#include <stdint.h>
#include <string.h>
/* 检测系统端序 */
static inline bool is_little_endian(void) {
uint16_t test = 0x0001;
return (*(uint8_t*)&test) == 0x01;
}
/* 跨平台字节序转换 */
static inline uint16_t swap_endian_u16(uint16_t x) {
return (uint16_t)(((x & 0x00FF) << 8) | ((x & 0xFF00) >> 8));
}
static inline uint32_t swap_endian_u32(uint32_t x) {
return (uint32_t)(((x & 0x000000FFu) << 24) |
((x & 0x0000FF00u) << 8) |
((x & 0x00FF0000u) >> 8) |
((x & 0xFF000000u) >> 24));
}
static inline uint64_t swap_endian_u64(uint64_t x) {
return (uint64_t) (((x & 0x00000000000000FFULL) << 56) |
((x & 0x000000000000FF00ULL) << 40) |
((x & 0x0000000000FF0000ULL) << 24) |
((x & 0x00000000FF000000ULL) << 8) |
((x & 0x000000FF00000000ULL) >> 8) |
((x & 0x0000FF0000000000ULL) >> 24) |
((x & 0x00FF000000000000ULL) >> 40) |
((x & 0xFF00000000000000ULL) >> 56));
}
/* 网络字节序 <-> 主机字节序 转换 */
static inline void host_to_network_u16(uint8_t* buf, uint16_t val) {
if (is_little_endian()) {
buf[0] = (val >> 8) & 0xFF;
buf[1] = val & 0xFF;
} else {
buf[0] = val & 0xFF;
buf[1] = (val >> 8) & 0xFF;
}
}
static inline void network_to_host_u16(uint16_t* val, const uint8_t* buf) {
*val = ((uint16_t)buf[0] << 8) | (uint16_t)buf[1];
}
坑七:直接内存拷贝的陷阱
如果你有一个结构体,里面含有整数字段,然后直接把这个结构体 memcpy 到网络缓冲区——在相同端序的机器之间没问题,但只要端序不同,数据就全乱了。正确的做法是逐个字段转换,或者使用固定的网络字节序(大端)来序列化数据。
嵌入式平台的特殊挑战
嵌入式开发和其他平台最大的区别在于:资源极其有限,而且硬件差异巨大。
/* embedded_platform.h - 嵌入式平台适配层 */
#ifndef EMBEDDED_PLATFORM_H
#define EMBEDDED_PLATFORM_H
/* 嵌入式环境通常没有标准库,需要自己实现一些基础功能 */
#ifdef EMBEDDED
#include <stddef.h>
/* 内存操作 */
#define malloc em_malloc
#define free em_free
#define memcpy em_memcpy
#define memset em_memset
#define strlen em_strlen
/* 没有标准IO,用自定义实现 */
#define printf em_printf
#define fprintf em_fprintf
#define fopen em_fopen
#define fclose em_fclose
#define fread em_fread
#define fwrite em_fwrite
/* 时间相关 */
#define clock em_clock
#define time em_time
/* 重定向这些宏到嵌入式实现 */
extern void* em_malloc(size_t size);
extern void em_free(void* ptr);
extern void* em_memcpy(void* dst, const void* src, size_t n);
extern void* em_memset(void* ptr, int value, size_t num);
extern size_t em_strlen(const char* s);
extern int em_printf(const char* fmt, ...);
extern int em_fprintf(void* stream, const char* fmt, ...);
extern size_t em_fread(void* ptr, size_t size, size_t nmemb, void* stream);
extern size_t em_fwrite(const void* ptr, size_t size, size_t nmemb, void* stream);
#endif /* EMBEDDED */
#endif /* EMBEDDED_PLATFORM_H */
嵌入式开发的几个核心坑:
坑八:堆内存的大小限制
嵌入式系统往往只有几十KB到几MB的RAM。如果你的跨平台代码在桌面端用了很多动态内存分配,在嵌入式平台上可能会直接 OOM。解决方案是:
- 在嵌入式构建中用静态内存池替代
malloc/free - 代码中避免不必要的动态分配
- 添加内存分配失败的保护逻辑
坑九:浮点运算的性能差异
很多嵌入式 MCU(比如 Cortex-M0/M3)没有硬件浮点单元,浮点运算要通过软件模拟,速度可能比整数运算慢几十倍甚至上百倍。如果你的跨平台算法大量使用浮点,在嵌入式平台上可能要换成定点数运算。
坑十:位域和结构体对齐
不同编译器对结构体的对齐方式不同。ARM GCC 默认按 4 字节对齐,而某些嵌入式编译器可能按 1 字节对齐。这会导致同一个结构体在不同平台上占用不同的内存大小。解决方法是使用 #pragma pack(push, 1) 强制对齐,或者用 __attribute__((packed))(GCC/Clang)。
CMake 的进阶用法——一套配置适配所有平台
光有代码层面的适配还不够,构建系统也要跨平台。CMake 的 toolchain file 是实现这个目标的关键:
# toolchains/arm-gcc.cmake - ARM嵌入式交叉编译工具链
set(CMAKE_SYSTEM_NAME Generic)
set(CMAKE_SYSTEM_PROCESSOR ARM)
# 交叉编译器路径
set(CROSS_COMPILE /opt/gcc-arm-none-eabi-10-2020-q4-major/bin/arm-none-eabi-)
set(CMAKE_C_COMPILER ${CROSS_COMPILE}gcc)
set(CMAKE_CXX_COMPILER ${CROSS_COMPILE}g++)
set(CMAKE_ASM_COMPILER ${CROSS_COMPILE}gcc)
set(CMAKE_LINKER ${CROSS_COMPILE}ld)
set(CMAKE_OBJCOPY ${CROSS_COMPILE}objcopy)
set(CMAKE_SIZE ${CROSS_COMPILE}size)
# 目标平台的特定标志
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -mcpu=cortex-m4 -mthumb -mfpu=fpv4-sp-d16 -mfloat-abi=hard" CACHE STRING "" FORCE)
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Os -ffunction-sections -fdata-sections" CACHE STRING "" FORCE)
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Tlinker.ld -nostartfiles -Wl,--gc-sections" CACHE STRING "" FORCE)
# 不需要在目标机上运行的探测步骤
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)
使用这个工具链的方式:
# 交叉编译到 ARM
cmake -B build-arm -DCMAKE_TOOLCHAIN_FILE=toolchains/arm-gcc.cmake
cmake --build build-arm
# 编译到 Linux x86_64
cmake -B build-linux
cmake --build build-linux
# 编译到 Windows (需要安装 MinGW 或使用 Visual Studio)
cmake -B build-windows -G "MinGW Makefiles"
cmake --build build-windows
坑十一:路径分隔符在 CMake 里的坑
CMake 本身很智能,会自动处理路径分隔符。但如果你在 CMakeLists.txt 里硬编码了路径(比如 C:/project/src/main.c),在 Linux 上就会出问题。永远使用 CMake 提供的变量和命令来处理路径,比如 file(TO_CMAKE_PATH ...) 和 ${CMAKE_CURRENT_SOURCE_DIR}。
坑十二:编译器标志的差异
GCC 和 Clang 的编译器标志大部分通用,但 MSVC 完全不同。比如:
- GCC/Clang:
-Wall -Wextra -O2 - MSVC:
/W3 /O2
在 CMake 里统一管理这些标志:
# 统一的编译选项,自动适配编译器
if(CMAKE_C_COMPILER_ID MATCHES "GNU|Clang|AppleClang")
set(COMPILER_FLAGS -Wall -Wextra -Wpedantic -O2)
set(COMPILER_WARNINGS -Werror)
elseif(CMAKE_C_COMPILER_ID STREQUAL "MSVC")
set(COMPILER_FLAGS /W3 /O2)
set(COMPILER_WARNINGS /WX)
endif()
target_compile_options(myapp PRIVATE ${COMPILER_FLAGS} ${COMPILER_WARNINGS})
测试策略——如何确保跨平台代码真的能用
写了跨平台代码,不代表它真的能跑在所有平台上。一个健壮的测试策略至关重要:
/* test_platform.c - 平台兼容性自检 */
#include <stdio.h>
#include <assert.h>
#include <stdint.h>
#include <limits.h>
#include "platform.h"
static void test_integer_sizes(void) {
printf("Testing integer sizes...\n");
assert(sizeof(int8_t) == 1);
assert(sizeof(int16_t) == 2);
assert(sizeof(int32_t) == 4);
assert(sizeof(int64_t) == 8);
assert(sizeof(uint8_t) == 1);
assert(sizeof(uint16_t) == 2);
assert(sizeof(uint32_t) == 4);
assert(sizeof(uint64_t) == 8);
printf(" Integer sizes OK\n");
}
static void test_path_separator(void) {
printf("Testing path separator...\n");
#ifdef PLATFORM_WINDOWS
assert(PATH_SEPARATOR == '\\');
#else
assert(PATH_SEPARATOR == '/');
#endif
printf(" Path separator OK\n");
}
static void test_endian(void) {
printf("Testing endianness...\n");
uint16_t val = 0x1234;
uint8_t bytes[2];
memcpy(bytes, &val, 2);
#ifdef PLATFORM_WINDOWS
/* Windows x86/x64 都是小端序 */
assert(bytes[0] == 0x34);
assert(bytes[1] == 0x12);
printf(" Endianness: Little Endian (confirmed)\n");
#else
printf(" Endianness: %s\n", is_little_endian() ? "Little Endian" : "Big Endian");
#endif
printf(" Endian test OK\n");
}
static void test_thread_creation(void) {
printf("Testing thread creation...\n");
PlatformThread thread;
int* result = (int*)malloc(sizeof(int));
*result = 42;
if (platform_thread_create(&thread,
PLATFORM_THREAD_FUNC start_func PLATFORM_THREAD_PARAM param,
result)) {
platform_thread_join(&thread);
printf(" Thread creation OK\n");
} else {
printf(" Thread creation FAILED\n");
}
free(result);
}
int main(void) {
printf("=== Platform Compatibility Test Suite ===\n");
printf("Platform: %s\n\n", PLATFORM_NAME);
test_integer_sizes();
test_path_separator();
test_endian();
test_thread_creation();
printf("\n=== All tests passed! ===\n");
return 0;
}
/* 辅助线程函数 */
PLATFORM_THREAD_FUNC start_func(PLATFORM_THREAD_PARAM param) {
int* val = (int*)param;
*val = *val * 2; /* 简单测试 */
return 0;
}
这套测试在每次构建时自动运行,能尽早发现跨平台兼容性问题。
坑十三:测试环境的覆盖不全
很多开发者只在 Linux 上开发和测试,然后指望代码能在 Windows 上也能跑。这是最大的误区。至少要在 CI/CD 管道里覆盖所有目标平台:GitHub Actions 可以免费在 Linux、Windows 和 macOS 上跑构建和测试。
几个通用的黄金法则
基于以上所有经验,总结几条最核心的原则:
第一条:永远用固定宽度的类型
不要相信 int 在某个平台上是 32 位。用 int32_t、uint16_t 这些 <stdint.h> 里定义的类型。这是跨平台代码的基本功。
第二条:区分编译时平台和运行时平台
#ifdef _WIN32 判断的是编译时的目标平台,但有时候你写的库可能在运行时检测到实际平台。这两种情况要区分清楚,处理方式不同。
第三条:使用成熟的跨平台库
如果不需要手写每一行适配代码,优先考虑成熟方案:
- Cross platform I/O:使用 CMake + 标准 POSIX API,避免直接调用平台 API
- 第三方库:如
crossplatform、picojson等轻量级跨平台库 - 游戏引擎级方案:如 SDL2、raylib 等,它们已经处理了绝大部分跨平台细节
第四条:保持代码分层清晰
把跨平台适配层和核心业务逻辑彻底分离。核心逻辑应该完全不知道运行在什么平台上,所有平台差异都封装在适配层里。这样的代码才真正具备跨平台能力。
第五条:CI/CD 是你的朋友
没有自动化测试的跨平台代码就是赌博。在 GitHub Actions、GitLab CI 或 Azure Pipelines 上配置多平台构建,每次提交都自动在 Linux、Windows、macOS 上编译测试。这一步的投资回报远超你的想象。
最后的忠告
跨平台 C 开发是一场马拉松,不是短跑。最难的从来不是某一行代码怎么写,而是建立一套可靠的流程——从开发环境到构建系统到测试策略,每一个环节都要考虑所有目标平台。
代码写得好只是开始,能让代码在所有平台上可靠地跑起来,才是真正的能力。
