news 2026/9/14 16:05:30

Apache Arrow Gandiva 外部函数开发指南:从 C 函数到 LLVM IR 函数的注册与集成

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Apache Arrow Gandiva 外部函数开发指南:从 C 函数到 LLVM IR 函数的注册与集成

Apache Arrow Gandiva 外部函数开发指南:从 C 函数到 LLVM IR 函数的注册与集成

【免费下载链接】arrowApache Arrow is the universal columnar format and multi-language toolbox for fast data interchange and in-memory analytics项目地址: https://gitcode.com/GitHub_Trending/arrow3/arrow

本指南基于 Apache Arrow 仓库中的 Gandiva 模块官方文档(docs/source/cpp/gandiva/external_func.rst)整理而成。Gandiva 是一个基于 LLVM 的表达式编译框架,通过"外部函数"机制,开发者可以用 C、C++、Rust 甚至 Zig 等语言编写自定义函数并注册进 Gandiva 表达式系统。读完本文,你将掌握两类外部函数(C 函数与 IR 函数)的选型原则、NativeFunction元数据注册方法、C 函数签名映射规则(含变长字符串类型的特殊处理),以及通过FunctionRegistry完成注册的完整 API 用法。

Gandiva 外部 C 函数与 IR 函数集成架构图

如上图所示,外部函数从"多语言编写 → 编译为 LLVM IR / 直接暴露为 C 函数 → 注册进 FunctionRegistry → 由 LLVMGenerator 生成 IR → LLVM JIT 引擎编译为机器码"形成了一条完整链路,本文即围绕这条链路的每一步展开。

外部函数的两大类型:C Functions 与 IR Functions

Gandiva 支持两种主要的外部函数类型:

  • C 函数(C Functions):符合 C 调用约定的函数。开发者可以用多种语言(如 C++、Rust、C、Zig)实现逻辑,再以 C 函数的形式暴露给 Gandiva。其核心优势是语言灵活、适用范围广,几乎适用于所有使用场景,集成也相对容易。
  • IR 函数(IR Functions):以 LLVM IR(LLVM 中间表示)形式实现的函数。同样可以用多种语言编写,然后编译成 LLVM IR 后注册进 Gandiva。

从源码结构看,Gandiva 对这两类函数的注册、查询和调用路径是分离但统一的:gandiva::FunctionRegistry(见 function_registry.h)内部同时维护了pc_registry_(预编译函数签名表)、c_functions_(C 函数指针列表)与bitcode_memory_buffers_(bitcode 缓冲区列表)三类数据,说明两类函数最终都汇聚到同一个注册表体系中,只是实现来源不同。

如何选择:C 函数还是 IR 函数

考量维度C 函数IR 函数
语言灵活性高,任意语言实现后暴露 C ABI需编译为 LLVM IR,依赖 LLVM 工具链
适用场景绝大多数通用场景逻辑简单的操作,如单步算术/比较
内联能力不可内联,存在调用开销可内联,对调用开销敏感的热点操作更优
第三方库依赖直接链接即可依赖库也必须一并编译进 LLVM IR,复杂库会拖累性能
高级特性可使用线程局部变量等不支持线程局部变量(受 Gandiva 内部 JIT 引擎限制)

文档中特别指出:IR 函数的可内联优势在"简单操作且调用开销占比显著"的场景下收益最大;而像使用 thread-local 变量这类高级特性,由于当前 Gandiva 内部 JIT 引擎的限制,IR 函数无法支持,此时应选用 C 函数。

外部函数注册总览:元数据 + 实现

要让一个函数对 Gandiva 可用,必须完成两件事:注册函数元数据(签名与属性)提供函数实现(C 函数指针或 LLVM IR 函数)

元数据注册使用gandiva::NativeFunction类,实现则通过gandiva::FunctionRegistry的三种Register重载接入。下面分别展开。

用 NativeFunction 类注册函数元数据

gandiva::NativeFunction类负责捕获外部函数的签名与元数据,其定义位于 native_function.h(构造函数见第 56-68 行):

NativeFunction(const std::string& base_name, const std::vector<std::string>& aliases, const DataTypeVector& param_types, const DataTypePtr& ret_type, const ResultNullableType& result_nullable_type, std::string pc_name, int32_t flags = 0);

各构造参数含义如下:

  • base_name:函数在表达式中使用的名字,例如"add""upper"

  • aliases:函数的别名列表。例如仓库中upper函数注册时就带有别名"ucase"(见 function_registry_string.cc 第 90 行)。从构造函数实现看(native_function.h),base_name与每个别名都会各生成一个FunctionSignature存入signatures_,即别名与主名在签名查找层面完全等价。

  • param_typesstd::vector<std::shared_ptr<arrow::DataType>>,函数接受的参数类型列表。

  • ret_typestd::shared_ptr<arrow::DataType>,函数返回类型。

  • result_nullable_type:结果可空性规则,由输入的 nullability 推导,取值为:

    取值含义
    ResultNullableType::kResultNullIfNull结果有效性是各子节点有效性的交集(任一输入为 null 则结果为 null)
    ResultNullableType::kResultNullNever结果永远有效(不产生 null)
    ResultNullableType::kResultNullInternal结果有效性取决于函数内部逻辑

    该枚举定义在 native_function.h,与文档完全一致。

  • pc_name:对应预编译函数(precompiled function)的名字。通常遵循{base_name}_{param1_type}_{param2_type}...{paramN_type}命名约定,例如base_nameadd、两个int32参数并返回int32的函数,其预编译函数名为add_int32_int32。该约定并非强制,只要保证唯一即可,因为注册后引擎会以pc_name作为符号进行全局映射查找(见下文AddGlobalMappingForFunc)。

  • flags:可选函数属性标志,默认 0。可组合使用以下位标志(定义见 native_function.h):

    • NativeFunction::kNeedsContext:函数需要一个int64_t context参数(返回字符串类型时必需,见下文);
    • NativeFunction::kNeedsFunctionHolder:函数需要一个函数持有者(function holder)参数;
    • NativeFunction::kCanReturnErrors:函数可以通过 context 返回错误信息。

源码中的真实注册示例

仓库内置函数的注册代码就是最好的范本。例如 function_registry_string.cc 第 77 行注册base64函数:

NativeFunction("base64", {}, DataTypeVector{binary()}, utf8(), kResultNullIfNull, "base64_binary", NativeFunction::kNeedsContext)

可以看到:base64接受binary参数、返回utf8,可空性为kResultNullIfNull,预编译函数名为base64_binary,并且由于返回类型是字符串,必须带上kNeedsContext标志——这正是文档中"返回 utf8 必须设置 kNeedsContext"规则的直接体现。类似地,upper(别名ucase,function_registry_string.cc)也是典型的字符串返回函数。

外部 C 函数开发

C 函数签名映射表

并非所有 Arrow 数据类型都被 Gandiva 外部函数支持,且 Gandiva 类型与 C 函数签名类型存在固定映射。文档给出的完整映射表如下:

Gandiva 类型(Arrow 数据类型)C 函数类型
int8int8_t
int16int16_t
int32int32_t
int64int64_t
uint8uint8_t
uint16uint16_t
uint32uint32_t
uint64uint64_t
float32float
float64double
booleanbool
date32int32_t
date64int64_t
timestampint64_t
time32int32_t
time64int64_t
interval_monthint32_t
interval_day_timeint64_t
utf8(作为参数)const char*,uint32_t(见变长类型小节)
utf8(作为返回)int64_t context,const char*,uint32_t*(见变长类型小节)
binary(作为参数)const char*,uint32_t(见变长类型小节)
binary(作为返回)int64_t context,const char*,uint32_t*(见变长类型小节)

这一映射在源码的 LLVM 签名生成逻辑中得到了印证:external_c_functions.cc 的GetNumArgs会为kNeedsContext增加 1 个参数、为字符串类型参数额外增加 1 个长度参数、为字符串返回类型额外增加 1 个输出长度指针参数;MapToLLVMSignature 则据此把字符串参数映射为i64(context)、i8*/指针加i32(长度)、i32*(输出长度指针),与上表逐项对应。

变长类型(utf8 / binary)的特殊处理

arrow::StringType(即utf8类型)与arrow::BinaryType都是变长类型,在外部函数中的处理方式相同。由于utf8更常用,文档以它为例说明变长类型的处理规则:

作为参数时,对应的 C 函数需要接受两个参数:

  • const char*:指向字符串数据的指针;
  • uint32_t:字符串数据的长度。

作为返回类型时,需要满足以下四点:

  1. 元数据标志NativeFunction元数据必须包含NativeFunction::kNeedsContext标志,这是保证函数上下文正确管理的关键(见上文base64注册示例)。
  2. 函数参数:C 函数开头要增加int64_t context参数(负责上下文管理);末尾要增加uint32_t*输出参数,用于写回返回字符串的长度。
  3. 返回值:函数返回const char*指针,指向字符串数据本身。
  4. 实现要点:函数体内应使用gdv_fn_context_arena_malloc进行内存分配、使用gdv_fn_context_set_error_msg设置错误信息。这两个辅助函数都以int64_t context作为第一个参数,声明位于 gdv_function_stubs.h:
void gdv_fn_context_set_error_msg(int64_t context_ptr, const char* err_msg); uint8_t* gdv_fn_context_arena_malloc(int64_t context_ptr, int32_t data_len);

gdv_fn_context_arena_malloc从 context 关联的 arena(内存池,对应SimpleArena机制)中分配临时内存,避免了每次调用都向系统申请堆内存的开销,也保证了 JIT 生成代码中字符串返回值的生命周期安全。仓库内置的字符串/二进制处理函数如gdv_fn_base64_encode_binarygdv_fn_base64_decode_utf8gdv_fn_castVARBINARY_int32_int64(见 gdv_function_stubs.h)都是"int64_t context+const char*输入 +int32_t*输出长度"这一范式的现成范例,可以直接参考其实现来编写自己的变长返回函数。

C 函数注册 API

使用gandiva::FunctionRegistry的注册 API 即可将外部 C 函数接入(声明见 function_registry.h):

/// \brief register a C function into the function registry /// @param func the registered function's metadata /// @param c_function_ptr the function pointer to the /// registered function's implementation /// @param function_holder_maker this will be used as the function holder if the /// function requires a function holder arrow::Status Register( NativeFunction func, void* c_function_ptr, std::optional<FunctionHolderMaker> function_holder_maker = std::nullopt);

参数说明:

  • NativeFunction func:外部 C 函数的元数据;
  • void* c_function_ptr:指向外部 C 函数实现的函数指针;
  • 可选的function_holder_maker:当外部 C 函数需要函数持有者时,用于创建该持有者的工厂函数。FunctionHolderMaker类型定义为std::function<arrow::Result<std::shared_ptr<FunctionHolder>>(const FunctionNode&)>,可参考gandiva::FunctionHolder基类及其若干子类(如InHolderIntervalHolderRandomGeneratorHolderRegexFunctionsHolderToDateHolder等,均位于 cpp/src/gandiva 目录下)。

从 function_registry.cc 的实现可以看到,注册时如果提供了function_holder_maker,它会以第一个签名的base_name为键注册到holder_maker_registry_;随后函数指针被存入c_functions_列表,并调用AddNativeFunction的每个签名加入pc_registry_map_签名查找表。运行时,external_c_functions.cc 的ExternalCFunctions::AddMappings会遍历c_functions_,为每个签名计算 LLVM 签名并调用engine->AddGlobalMappingForFuncpc_name映射到 C 函数指针,从而让 JIT 生成的机器码能够直接调用你的 C 实现。

外部 IR 函数开发

实现与编译

IR 函数允许你用多种语言实现,然后编译为 Gandiva 可识别的 LLVM 中间表示:

  1. 使用 C++ 或 C:实现代码编译成 LLVM bitcode(Gandiva 理解的中间表示)。C++ 实现可用 clang 的-emit-llvm选项直接编译出 LLVM bitcode,例如:
    clang++ -emit-llvm -c my_funcs.cpp -o my_funcs.bc
  2. 集成 CMake:在配合 CMake 的 C++ 项目中,可以使用 Arrow 仓库提供的GandivaAddBitcode.cmake模块,将自定义 bitcode 平滑地接入 Gandiva 构建流程。

类型一致性要求:IR 函数的参数与返回类型必须严格遵循前文 C 函数一节建立的规则,以保证与 Gandiva 类型系统的兼容(包括字符串类型的双参数/输出长度指针约定)。

在 Gandiva 中注册外部 IR 函数

实现并编译出 LLVM bitcode 之后,通过gandiva::FunctionRegistry提供的两个 API 完成注册(声明见 function_registry.h):

从 bitcode 文件注册:

// Registers a set of functions from a specified bitcode file arrow::Status Register(const std::vector<NativeFunction>& funcs, const std::string& bitcode_path);

从 bitcode 缓冲区注册:

// Registers a set of functions from a bitcode buffer arrow::Status Register(const std::vector<NativeFunction>& funcs, std::shared_ptr<arrow::Buffer> bitcode_buffer);

关键点:

  • 这两个 API 用于一次性注册一组外部 IR 函数,来源可以是 bitcode 文件路径,也可以是已加载进内存的 bitcode 缓冲区(arrow::Buffer);
  • 必须确保 bitcode 文件/缓冲区中包含正确编译的 IR 函数实现;
  • 每个NativeFunction实例用于定义被注册 IR 函数的元数据(签名、可空性、pc_name 等)。

从 function_registry.cc 的实现看,文件版本会先把 bitcode 读入内存并包装为LLVMMemoryArrowBuffer,再统一走缓冲区版本:bitcode 缓冲区被存入bitcode_memory_buffers_,随后逐个Add到签名表。JIT 编译时,LLVMGenerator会从GetBitcodeBuffers()取出这些缓冲区,把其中的 IR 函数与签名表中注册的pc_name对应起来。此外,MakeDefaultFunctionRegistry()(function_registry.cc)会把算术、日期时间、哈希、数学运算、字符串、日期时间算术六大内置注册表合并为默认注册表——你的外部函数注册到自定义的FunctionRegistry后,同样可以随该注册表一起参与表达式编译。

总结

在 Gandiva 中扩展自定义函数的核心流程可以归纳为四步:

  1. 选型:根据逻辑复杂度、内联需求、第三方库依赖和 LLVM 工具链集成情况,在 C 函数与 IR 函数之间做出选择;
  2. 实现:按签名映射表编写 C 函数(注意变长字符串类型的const char* + uint32_t参数约定与返回时的kNeedsContext标志、uint32_t*输出长度参数),或将实现编译为 LLVM bitcode;
  3. 注册元数据:用gandiva::NativeFunction描述 base_name、别名、参数/返回类型、结果可空性与 pc_name;
  4. 注册实现:调用gandiva::FunctionRegistry::Register的三种重载之一(C 函数指针、bitcode 文件或 bitcode 缓冲区),把实现与元数据关联起来。

本文涉及的NativeFunctionFunctionRegistryFunctionHolder等类定义与内置注册实例,均可在 cpp/src/gandiva 目录下的头文件与各function_registry_*.cc文件中找到完整实现,需要处理更复杂场景(如函数持有者、错误返回、十进制/哈希等特殊类型)时,直接阅读这些源码与 cpp/src/gandiva/tests 下的测试用例是最快的上手路径。

【免费下载链接】arrowApache Arrow is the universal columnar format and multi-language toolbox for fast data interchange and in-memory analytics项目地址: https://gitcode.com/GitHub_Trending/arrow3/arrow

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/14 16:04:48

全固态激光雷达如何守护铁路安全:异物侵限监测实战解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/14 16:04:42

零代码UI自动化:基于浏览器原生能力的回归测试新范式

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/14 16:03:40

HED边缘检测在Caffe中的部署与推理实战

简介&#xff1a;面向深度学习边缘检测方向研究者与开发者的HED实现示例包&#xff0c;基于Caffe框架构建。HED算法通过多个侧输出层分别预测不同尺度的边缘&#xff0c;再将结果融合&#xff0c;解决了Canny、Sobel等传统算子难以捕捉复杂纹理和多尺度结构的问题。整个资源包内…

作者头像 李华
网站建设 2026/9/14 15:58:46

Flutter鸿蒙角标实现:OpenHarmony应用图标数字精准控制

1. 项目概述&#xff1a;为什么“Flutter 鸿蒙化”不是口号&#xff0c;而是必须落地的工程现实最近三个月&#xff0c;我连续接手了三个客户项目&#xff0c;需求高度一致&#xff1a;用 Flutter 写的跨端 App&#xff0c;要上架到 OpenHarmony 设备——不是模拟器&#xff0c…

作者头像 李华
网站建设 2026/9/14 15:56:43

查重与AIGC检测双红预警?用百考通降重+降AI一次搞定

又到了一年一度被论文支配的季节。查重报告一片标红&#xff0c;AI检测又给你来个“疑似AIGC生成”的高亮警告&#xff0c;说实话这种双重打击放在谁身上都挺崩溃的。我在毕业季帮人改过太多论文&#xff0c;见过太多卡在这两道关卡上的情况——明明是自己一个字一个字写出来的…

作者头像 李华