前言在 C 项目里调用 TensorFlow 做推理是很多工程团队绕不开的需求训练脚本用 Python 写线上服务却要求低延迟、易部署、无 Python 运行时依赖。真正动手时会发现第一个障碍不是代码而是到底该用哪套接口。TensorFlow 历史上存在两套 C 相关接口一套是tensorflow::Session所在的 C API另一套是稳定的 C API。TensorFlow 2.x 之后官方不再保证 C API 的 ABI 稳定性头文件和库版本必须严格匹配而 C API 的 ABI 是稳定的预编译库也更容易获得。因此本文的主线是用 Python 训练并导出 SavedModel用 TensorFlow 的 C API 在 C 里加载模型、构造输入张量、执行推理、读取输出。这条路径在工程上最稳代价是需要手写一些 C 风格的资源管理代码。文中还会说明 CMake 的链接方式以及为什么直接 include C 头文件然后链接 libtensorflow经常失败。需要提前说明TensorFlow 各版本的头文件位置和可用函数有差异本文的函数名按官方 C API 文档给出具体以你所用版本的c_api.h与官方文档为准。示例只做推理不涉及训练。一、三条可选路径路径接口稳定性适用场景C APItensorflow/c/c_api.hABI 稳定官方承诺生产推理服务跨语言绑定C APItensorflow::Session等2.x 起不保证 ABI 稳定与 TF 源码一同构建的内部项目其他运行时ONNX Runtime 等各自独立模型可转换、追求部署简单C API 是 TensorFlow 官方为其他语言绑定提供的底层接口Python、Java、Go 的绑定本质上都建立在它之上。它以TF_前缀的函数和若干不透明指针TF_Graph、TF_Session、TF_Tensor、TF_Status工作没有异常所有错误通过TF_Status返回。这种风格在 C 里写起来啰嗦但边界清晰。C API 在 TensorFlow 1.x 时代是tensorflow::Session配合tensorflow::GraphDef需要自己protobuf序列化图。TensorFlow 2.x 把重心放在 Python 与 C API 上C API 的构建依赖 Bazel 和完整源码树二进制接口随版本变化是常态。如果只是做推理没有必要承担这份复杂度。无论选哪条路模型本身都由 Python 侧产出。C 不负责训练只负责按图执行。二、模型从哪来SavedModel 的导出与检查推荐在 Python 侧用 Keras 或tf.Module训练然后导出为 SavedModel 目录。SavedModel 包含图定义、变量权重和签名signatureC API 通过TF_LoadSessionFromSavedModel一次性加载。导出后要做的第一件事是确认输入输出节点的名字。SavedModel 里真正的计算节点通常被包在StatefulPartitionedCall或PartitionedCall之下签名里的inputs和outputs是逻辑名不一定等于图里的操作名。可以用官方提供的saved_model_cli查看# 仅用于查看模型签名与节点名需在 Python 环境中执行saved_model_cli show --dir ./saved_model --all输出里会列出签名键常见为serving_default、输入张量的名称、形状和数据类型以及输出张量的名称。记下这些名字后面调用TF_GraphOperationByName时要原样使用。名字写错时函数返回空指针而不是抛出异常。如果模型来自 TensorFlow 1.x 的冻结图frozen GraphDef流程会略有不同用TF_GraphImportGraphDef导入序列化后的图再按节点名取操作。本文以 SavedModel 为主。三、用 C API 完成一次推理下面是一段完整的推理代码。它加载 SavedModel、构造一个形状为 1×3 的float输入、执行一次TF_SessionRun、读取输出并释放全部资源。代码中的输入输出操作名是占位示例必须换成上一步查到的真实名字。// 适用于 C17 TensorFlow C API#include tensorflow/c/c_api.h#include cstdint#include cstring#include cstdio#include vectorint main() {TF_Status* status TF_NewStatus();TF_Graph* graph TF_NewGraph();TF_SessionOptions* options TF_NewSessionOptions();TF_Buffer* meta_graph TF_NewBuffer();const char* tags[] {serve};TF_Session* session TF_LoadSessionFromSavedModel(options, nullptr, saved_model, tags, 1,graph, meta_graph, status);if (TF_GetCode(status) ! TF_OK) {std::fprintf(stderr, load failed: %s\n, TF_Message(status));TF_DeleteBuffer(meta_graph);TF_DeleteSessionOptions(options);TF_DeleteGraph(graph);TF_DeleteStatus(status);return 1;}// 输入形状 1x3 的 floatconst int64_t dims[2] {1, 3};const float values[3] {1.0f, 2.0f, 3.0f};TF_Tensor* input TF_AllocateTensor(TF_FLOAT, dims, 2, sizeof(values));std::memcpy(TF_TensorData(input), values, sizeof(values));// 节点名来自 saved_model_cli 的输出必须按实际模型替换TF_Operation* in_op TF_GraphOperationByName(graph, serving_default_input);TF_Operation* out_op TF_GraphOperationByName(graph, StatefulPartitionedCall);if (in_op nullptr || out_op nullptr) {std::fprintf(stderr, operation not found\n);TF_DeleteTensor(input);TF_DeleteSession(session, status);TF_DeleteBuffer(meta_graph);TF_DeleteSessionOptions(options);TF_DeleteGraph(graph);TF_DeleteStatus(status);return 1;}TF_Output input_port{in_op, 0};TF_Output output_port{out_op, 0};TF_Tensor* output nullptr;TF_SessionRun(session, nullptr,input_port, input, 1,output_port, output, 1,nullptr, 0, nullptr, status);if (TF_GetCode(status) ! TF_OK) {std::fprintf(stderr, run failed: %s\n, TF_Message(status));} else {const int ndim TF_NumDims(output);int64_t total 1;for (int i 0; i ndim; i) {total * TF_Dim(output, i);}const float* data static_castconst float*(TF_TensorData(output));for (int64_t i 0; i total; i) {std::printf(%f\n, static_castdouble(data[i]));}}TF_DeleteTensor(output);TF_DeleteTensor(input);TF_DeleteSession(session, status);TF_DeleteBuffer(meta_graph);TF_DeleteSessionOptions(options);TF_DeleteGraph(graph);TF_DeleteStatus(status);return 0;}逐段说明几个关键点。TF_LoadSessionFromSavedModel会向graph中导入模型图加载出的session绑定这个图meta_graph用来接收序列化的元图可以传空缓冲区但不要传空指针。TF_NewSessionOptions返回的选项只在加载时使用加载完成后即可释放。TF_Output是一个二元组操作指针加输出端口索引。一个操作可以有多个输出例如分裂操作索引从 0 开始。TF_SessionRun的输入输出都以数组形式传入ninputs和noutputs是元素个数。TF_TensorData返回的是原始缓冲区类型必须与模型声明的TF_DataType一致。上例按TF_FLOAT读取若模型输入是float64或int32必须相应修改否则读出的字节会被错误解释属于逻辑错误而不是库会替你检查的问题。四、构建与链接C API 的头文件在include/tensorflow/c/下库是单个动态库。官方为 Linux 和 macOS 提供预编译的 C 库压缩包Windows 上通常需要自行构建或借助 WSL具体以官方安装文档为准。# CMakeLists.txtcmake_minimum_required(VERSION 3.16)project(tf_c_demo LANGUAGES CXX)set(CMAKE_CXX_STANDARD 17)set(CMAKE_CXX_STANDARD_REQUIRED ON)add_executable(tf_c_demo main.cpp)# TF_ROOT 指向解压后的 libtensorflow 目录target_include_directories(tf_c_demo PRIVATE ${TF_ROOT}/include)target_link_libraries(tf_c_demo PRIVATE ${TF_ROOT}/lib/libtensorflow.so)运行时动态库要能被找到Linux 下把库目录加入LD_LIBRARY_PATH或写入rpathWindows 下把 DLL 放到可执行文件同目录或加入PATH。这一步没做对表现是程序启动即报找不到动态库而不是编译错误。如果确实要尝试 C API必须在 CMake 中引入 TensorFlow 源码的构建产物版本、编译器、C 标准都要与构建 TensorFlow 时一致否则会在链接期出现大量符号未定义。这不是配置技巧能绕过的属于 ABI 不匹配。常见坑点❌ 用 TensorFlow 2.x 的预编译库链接tensorflow::Session相关头文件。✅ 预编译的libtensorflow只导出 C APIC 符号需要源码构建推理场景直接用 C API。❌ 直接把签名里的逻辑名当作TF_GraphOperationByName的参数。✅ 逻辑名与图内操作名可能不同先用saved_model_cli查出真实名称。❌ 忘记检查TF_GetCode(status)后续用空指针继续操作。✅ 每个可能失败的 C API 调用后立即检查状态失败时打印TF_Message并清理资源。❌ 输入张量的TF_DataType与模型签名不一致读出的数值完全错乱。✅ 按签名声明构造张量形状也要匹配-1的动态维度要填入实际批次大小。❌ 把栈上数组直接交给TF_NewTensor张量生命周期超过数组。✅ 用TF_AllocateTensor分配并由张量自己持有或保证张量先于数据销毁。❌ 只删除 session 不删除 graph、status、options长期运行内存持续增长。✅ C API 没有 RAII逐个配对释放C 里可以封装成自定义 deleter 的智能指针。❌ 在多个线程里共享同一个TF_Session却不做同步。✅ 官方文档说明TF_SessionRun可并发调用但对图结构的修改必须外部同步拿不准时每线程一个 session 或用互斥量保护。❌ 在 Windows 上直接套用 Linux 的库路径和.so文件名。✅ 库名、后缀和运行时搜索路径都不同按平台文档配置必要时用 CMake 的if(WIN32)分支。总结环节做法模型产出Python 训练导出 SavedModel接口选择生产推理优先 C APIABI 稳定加载TF_LoadSessionFromSavedModel填充 graph 与 session取名saved_model_cli查真实操作名执行构造TF_Output与TF_Tensor调用TF_SessionRun资源所有TF_*对象手动配对释放构建链接libtensorflow配置运行时库搜索路径C 调用 TensorFlow 的难点不在算法而在接口边界模型名、数据类型、生命周期和 ABI。把这几处固定下来推理代码本身只有几十行。若团队不需要 TensorFlow 特有的算子把模型转成 ONNX 再用 ONNX Runtime 往往能省掉不少构建上的麻烦。