从零开始C语言调用GLM-OCR本地库的完整示例如果你是一名C语言开发者或者你的项目运行在嵌入式、物联网这类资源受限的环境里那么你很可能遇到过这样的困扰看到一个用Python写得很酷的AI功能比如文字识别OCR但怎么把它集成到你的C项目里呢直接跑Python解释器太重用网络API又有延迟和隐私的顾虑。今天我们就来解决这个问题。我将手把手带你把一个像GLM-OCR这样的模型“打包”成一个C语言可以直接调用的动态链接库在Linux上是.so文件Windows上是.dll文件。之后你就可以像调用printf、malloc一样在你的C程序里轻松使用OCR功能了。整个过程听起来有点复杂但别担心我会把它拆解得清清楚楚。我们会从最基础的接口设计讲起一步步走到内存管理和数据转换这些容易踩坑的地方。跟着做下来你不仅能得到一个可运行的例子更能掌握把AI模型集成到原生C环境的核心思路。1. 我们的目标与准备工作在开始写代码之前我们先明确一下要做什么以及需要准备些什么。最终目标是创建一个C语言动态库它对外提供简单的函数比如ocr_init(),ocr_detect(),ocr_release()。然后我们再写一个C程序调用这个库完成对一张图片的文字识别。为了实现这个目标我们需要做一些准备工作1. 环境与工具C编译器比如gcc或clang。这是必须的。构建工具推荐使用CMake它能帮我们轻松管理跨平台的编译过程。当然如果你习惯写Makefile也行。模型与推理框架这是核心。我们需要一个OCR模型例如GLM-OCR的权重文件和一个支持C或C接口的推理框架。常见的选择有ONNX Runtime支持C API跨平台性好模型通常需要先转换为ONNX格式。TensorFlow C API或LibTorch (PyTorch C)如果你熟悉对应的生态。NCNN、TNN等这些是针对移动端/嵌入式优化的前向推理框架通常有更简洁的C接口易于封装。图像处理库我们需要读取图片、转换颜色空间、调整尺寸。OpenCV的C接口是绝佳选择它功能强大我们可以在封装层C使用它然后通过我们的C接口暴露处理后的数据。2. 项目结构规划在动手前规划好目录结构会让后续工作清晰很多。我建议这样组织glm-ocr-c-demo/ ├── CMakeLists.txt # 顶层的CMake配置文件 ├── src/ │ ├── ocr_lib/ # 动态库的源代码 │ │ ├── CMakeLists.txt │ │ ├── ocr_engine.cpp # 核心引擎用C实现调用模型 │ │ └── ocr_engine.h │ └── demo/ # 测试用的C程序 │ ├── CMakeLists.txt │ └── main.c # 我们的C语言调用示例 ├── lib/ # 放置编译好的动态库文件.so/.dll ├── include/ # 放置对外公开的C头文件.h ├── models/ # 放置OCR模型文件.onnx, .param, .bin等 └── images/ # 放置测试图片思路是这样的我们在ocr_engine.cpp里用 C 和 OpenCV 实现所有复杂逻辑然后设计一套纯C的函数接口通过extern C暴露出去。最后main.c只需要包含那个C头文件就能调用这些函数了。2. 设计C语言接口这是最关键的一步接口设计得好调用起来就简单又安全。我们要遵循C语言的习惯简单、明确、由调用者负责内存。我们来设计三个核心函数// 文件include/ocr_c_api.h #ifndef OCR_C_API_H #define OCR_C_API_H #ifdef __cplusplus extern C { #endif // 句柄类型用来代表一个OCR引擎实例对C语言隐藏具体的C类 typedef void* OcrHandle; /** * 初始化OCR引擎 * param model_path 模型文件路径字符串 * param config_path 配置文件路径字符串可为NULL * return 成功返回引擎句柄失败返回NULL */ OcrHandle ocr_init(const char* model_path, const char* config_path); /** * 对图像进行文字识别 * param handle 由ocr_init返回的句柄 * param image_data 图像数据的字节数组例如RGB格式 * param width 图像宽度 * param height 图像高度 * param channels 图像通道数例如3代表RGB * param results 输出参数用于接收识别结果字符串。调用者负责分配和释放该指针指向的内存。 * return 成功返回0失败返回非0错误码 */ int ocr_detect(OcrHandle handle, const unsigned char* image_data, int width, int height, int channels, char** results); /** * 释放OCR引擎及相关资源 * param handle 由ocr_init返回的句柄指针的地址。 * 函数内部会释放资源并将*handle置为NULL避免野指针。 */ void ocr_release(OcrHandle* handle); #ifdef __cplusplus } #endif #endif // OCR_C_API_H设计要点解析OcrHandle句柄这是C语言封装C对象的经典模式。C语言没有“类”的概念我们用void*来指向内部创建的C对象。调用者不需要知道里面是什么只需要在函数间传递这个“令牌”即可。extern C这个修饰符告诉C编译器按C语言的规则来编译这些函数名不进行名字修饰这样C代码才能正确链接到它们。ocr_detect的参数图像数据通过指针const unsigned char* image_data和宽、高、通道数来描述这是一种非常通用和底层的方式可以兼容各种图像来源。char** results是一个输出参数。这里采用了“调用者分配”的一种变体由库函数内部分配内存填充结果然后将指针赋值给*results。调用者在使用完毕后必须调用free(*results)来释放内存。这是一种常见的约定需要在文档中明确说明。ocr_release的参数这里传入句柄指针的地址OcrHandle* handle。这样做的好处是函数内部在释放完资源后可以将*handle设为NULL防止调用者后续误用已释放的句柄野指针。3. 实现C核心引擎与封装层现在我们来实现在C世界里干脏活累活的部分。这里假设我们选用ONNX Runtime作为推理后端并使用OpenCV处理图像。// 文件src/ocr_lib/ocr_engine.cpp #include ocr_engine.h #include opencv2/opencv.hpp #include onnxruntime_cxx_api.h #include vector #include string #include cstring // for strdup // 1. 内部OCR引擎类对C代码不可见 class OcrEngineImpl { public: OcrEngineImpl(const std::string model_path) { // 初始化ONNX Runtime环境 env_ Ort::Env(ORT_LOGGING_LEVEL_WARNING, GLM-OCR); session_options_.SetIntraOpNumThreads(1); // 根据实际情况设置线程数 // 加载模型 session_ Ort::Session(env_, model_path.c_str(), session_options_); // 假设我们已知模型的输入输出名称实际应从模型读取 input_name_ input; output_name_ output; // ... 其他初始化如加载字符字典等 } std::string detect(const cv::Mat image) { // 1. 图像预处理 (使用OpenCV) cv::Mat processed; // 例如调整大小、归一化、BGR转RGB、CHW转换等 cv::resize(image, processed, cv::Size(224, 224)); // 假设输入尺寸是224x224 processed.convertTo(processed, CV_32F, 1.0 / 255.0); // 归一化到[0,1] // 将HWC [224,224,3] 转换为 CHW [3,224,224] 并转为vector std::vectorcv::Mat channels(3); cv::split(processed, channels); std::vectorfloat input_tensor_values; for (const auto channel : channels) { input_tensor_values.insert(input_tensor_values.end(), (float*)channel.data, (float*)channel.data channel.total()); } // 2. 准备ONNX Runtime的输入Tensor std::vectorint64_t input_shape {1, 3, 224, 224}; size_t input_tensor_size 1 * 3 * 224 * 224; auto memory_info Ort::MemoryInfo::CreateCpu(OrtArenaAllocator, OrtMemTypeDefault); Ort::Value input_tensor Ort::Value::CreateTensorfloat(memory_info, input_tensor_values.data(), input_tensor_size, input_shape.data(), input_shape.size()); // 3. 运行推理 auto output_tensors session_.Run(Ort::RunOptions{nullptr}, input_name_, input_tensor, 1, output_name_, 1); // 4. 后处理解析输出Tensor得到识别文字 // 这里简化处理实际需要根据模型输出结构解析 float* floatarr output_tensors[0].GetTensorMutableDatafloat(); // ... 解码逻辑最终得到识别文本 std::string result_text std::string result_text 识别结果: 你好世界; // 示例文本 return result_text; } ~OcrEngineImpl() { // 清理资源ORT环境和会话有RAII会自动释放 } private: Ort::Env env_; Ort::SessionOptions session_options_; Ort::Session session_; const char* input_name_; const char* output_name_; // ... 其他成员如字符字典 }; // 2. C接口的具体实现 extern C { OcrHandle ocr_init(const char* model_path, const char* config_path) { try { // 使用new在堆上创建C对象返回其地址作为句柄 OcrEngineImpl* engine new OcrEngineImpl(std::string(model_path)); return static_castOcrHandle(engine); } catch (const std::exception e) { // 可以在这里记录日志 fprintf(stderr, OCR初始化失败: %s\n, e.what()); return nullptr; } } int ocr_detect(OcrHandle handle, const unsigned char* image_data, int width, int height, int channels, char** results) { if (!handle || !image_data || !results) { return -1; // 错误码无效参数 } OcrEngineImpl* engine static_castOcrEngineImpl*(handle); try { // 将C数组转换为OpenCV Mat // 注意这里假设image_data是连续的RGB数据 cv::Mat image(height, width, CV_8UC(channels), (void*)image_data); // 如果数据是BGR可能需要转换颜色空间这里假设输入就是RGB // cv::cvtColor(image, image, cv::COLOR_BGR2RGB); // 调用C引擎进行识别 std::string text_result engine-detect(image); // 关键步骤为结果分配内存并复制字符串。 // 使用strdup可以方便地复制字符串调用者用free释放。 *results strdup(text_result.c_str()); if (!*results) { return -2; // 错误码内存分配失败 } return 0; // 成功 } catch (const std::exception e) { fprintf(stderr, OCR检测失败: %s\n, e.what()); return -3; // 错误码推理过程异常 } } void ocr_release(OcrHandle* handle) { if (handle *handle) { OcrEngineImpl* engine static_castcrEngineImpl*(*handle); delete engine; // 释放C对象 *handle nullptr; // 将外部指针置空防止野指针 } } } // extern C关键点说明错误处理使用try-catch捕获C异常并转换为C接口的错误码返回。这是保证库稳定性的重要一环。内存转换ocr_detect函数中我们将unsigned char*和尺寸信息成功转换成了OpenCV的cv::Mat对象这是连接C数据与C库的桥梁。结果返回使用strdup复制结果字符串。这是一个标准C库函数它在堆上分配内存并复制字符串返回的指针需要用free()释放。这明确地将内存释放的责任交给了调用者。4. 编写C语言调用示例库封装好了现在我们来写一个简单的C程序测试它。// 文件src/demo/main.c #include stdio.h #include stdlib.h #include string.h // 包含我们设计的C接口头文件 #include ocr_c_api.h // 一个辅助函数读取图片文件到内存简单示例假设是RGB Raw数据 // 实际项目中你可能需要用libpng, libjpeg或OpenCV的C版本读图 unsigned char* load_image(const char* filename, int* width, int* height, int* channels) { // 这里为了演示我们模拟一张 640x480 的RGB图片 *width 640; *height 480; *channels 3; size_t size (*width) * (*height) * (*channels); unsigned char* data (unsigned char*)malloc(size); if (data) { // 填充一些模拟数据比如全黑图片 memset(data, 0, size); // 或者在中间画个白色方块 for (int y 200; y 280; y) { for (int x 240; x 400; x) { int idx (y * (*width) x) * (*channels); data[idx] 255; // R data[idx 1] 255; // G data[idx 2] 255; // B } } } return data; } int main() { printf( C语言调用GLM-OCR本地库示例 \n); const char* model_path ../models/glm-ocr.onnx; const char* config_path NULL; // 本例不需要配置文件 // 1. 初始化引擎 OcrHandle ocr_handle ocr_init(model_path, config_path); if (!ocr_handle) { fprintf(stderr, 错误初始化OCR引擎失败。\n); return 1; } printf(OCR引擎初始化成功。\n); // 2. 加载测试图像 int img_width, img_height, img_channels; unsigned char* image_data load_image(../images/test.png, img_width, img_height, img_channels); if (!image_data) { fprintf(stderr, 错误加载图像失败。\n); ocr_release(ocr_handle); return 1; } printf(加载图像: %dx%d, %d通道\n, img_width, img_height, img_channels); // 3. 执行文字识别 char* result_text NULL; int ret ocr_detect(ocr_handle, image_data, img_width, img_height, img_channels, result_text); if (ret 0 result_text) { printf(识别成功结果\n%s\n, result_text); } else { fprintf(stderr, 识别失败错误码%d\n, ret); } // 4. 清理资源 (非常重要) // 释放识别结果字符串的内存 if (result_text) { free(result_text); result_text NULL; } // 释放图像数据 free(image_data); image_data NULL; // 释放OCR引擎 ocr_release(ocr_handle); // 检查句柄是否被正确置空 if (ocr_handle NULL) { printf(资源已全部释放。程序结束。\n); } return 0; }这个示例清晰地展示了C语言调用的完整生命周期初始化 - 准备数据 - 调用 - 释放资源。特别注意对于ocr_detect返回的result_text以及我们自己分配的image_data都进行了手动释放。5. 编译与链接让一切跑起来最后一步我们需要一个CMakeLists.txt来指挥编译器工作。# 文件顶层的 CMakeLists.txt cmake_minimum_required(VERSION 3.10) project(glm_ocr_c_demo) set(CMAKE_CXX_STANDARD 11) set(CMAKE_C_STANDARD 99) # 查找依赖库 find_package(OpenCV REQUIRED) # 假设ONNX Runtime通过解压包引入我们手动指定路径 set(ONNXRUNTIME_ROOT_DIR /path/to/your/onnxruntime-linux-x64) # 修改为你的路径 set(ONNXRUNTIME_INCLUDE_DIR ${ONNXRUNTIME_ROOT_DIR}/include) set(ONNXRUNTIME_LIB_DIR ${ONNXRUNTIME_ROOT_DIR}/lib) include_directories(${ONNXRUNTIME_INCLUDE_DIR} ${OpenCV_INCLUDE_DIRS}) link_directories(${ONNXRUNTIME_LIB_DIR}) # 添加子目录库和演示程序 add_subdirectory(src/ocr_lib) add_subdirectory(src/demo)# 文件src/ocr_lib/CMakeLists.txt # 构建动态库 add_library(glm_ocr_c SHARED ocr_engine.cpp) target_include_directories(glm_ocr_c PRIVATE ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_SOURCE_DIR}/include # 包含我们的C API头文件 ) target_link_libraries(glm_ocr_c ${OpenCV_LIBS} onnxruntime # 链接ONNX Runtime库 ) # 将头文件复制到公共include目录方便demo程序使用 configure_file(${CMAKE_SOURCE_DIR}/include/ocr_c_api.h ${CMAKE_BINARY_DIR}/include/ocr_c_api.h COPYONLY)# 文件src/demo/CMakeLists.txt # 构建可执行演示程序 add_executable(ocr_demo main.c) target_include_directories(ocr_demo PRIVATE ${CMAKE_BINARY_DIR}/include # 包含生成的公共头文件 ${CMAKE_SOURCE_DIR}/include ) target_link_libraries(ocr_demo glm_ocr_c # 链接我们刚刚创建的动态库 )编译和运行在项目根目录创建build文件夹mkdir build cd build运行CMake生成构建文件cmake ..编译make运行演示程序./src/demo/ocr_demo如果一切顺利你应该能看到程序输出初始化成功、加载图像并打印出模拟的识别结果。6. 总结与关键要点回顾走完这一趟我们完成了一个从AI模型到C语言可调用动态库的完整封装流程。整个过程的核心其实是在C的便利性与C的简洁性之间架起一座桥。回头看看有几个地方特别容易出问题需要你在实际项目中多加留意内存管理是重中之重C语言没有RAII每一块malloc或库函数返回的内存都必须有对应的free。我们设计的接口明确区分了“谁分配谁释放”一定要遵守这个约定。数据格式要匹配图像数据从C数组到cv::Mat的转换维度、通道顺序RGB/BGR、数据类型uint8/float必须完全匹配模型的要求错一点都会导致识别失败或乱码。错误处理要健全C接口的每个函数都应该有明确的返回值来指示成功或失败并且能处理异常。在C层用try-catch兜底防止崩溃传导到调用方。资源释放要彻底ocr_release函数接受指针的地址并在内部将句柄置NULL这是一个好习惯能有效避免“释放后使用”的错误。对于嵌入式或高性能场景你还可以进一步优化比如预分配内存池、使用静态链接减少依赖、针对特定硬件如ARM NEON进行加速等。把复杂的AI功能封装成简洁的C接口就像为你的C项目打开了一扇新的大门。希望这个详细的示例能给你提供一个坚实的起点。在实际集成时你可能需要根据选择的推理框架和模型格式调整部分代码但整体的架构和思路是相通的。动手试试看把你需要的AI能力也“打包”进下一个C项目里吧。获取更多AI镜像想探索更多AI镜像和应用场景访问 CSDN星图镜像广场提供丰富的预置镜像覆盖大模型推理、图像生成、视频生成、模型微调等多个领域支持一键部署。