Gandiva 外部函数开发指南#
介绍#
Gandiva 作为一个分析表达式编译器框架,通过外部函数扩展其功能。本指南旨在帮助开发者理解、创建和集成外部函数到 Gandiva 中。外部函数是用户定义的第三方函数,可用于 Gandiva 表达式。
Gandiva 外部函数类型概述#
Gandiva 支持两种主要的外部函数类型:
C 函数:符合 C 调用约定的函数。开发者可以使用各种语言(如 C++、Rust、C 或 Zig)实现函数,并将其作为 C 函数暴露给 Gandiva。
IR 函数:用 LLVM 中间表示 (LLVM IR) 实现的函数。这些函数可以用多种语言编写,然后编译成 LLVM IR 以在 Gandiva 中注册。
根据您的需求选择正确的外部函数类型#
将外部函数集成到 Gandiva 中时,选择最符合您特定需求的类型至关重要。以下是 C 函数和 IR 函数之间的主要区别,以指导您的决策:
- C 函数
语言灵活性: C 函数提供了灵活性,允许您使用首选编程语言实现逻辑,并随后将其作为 C 函数暴露。
广泛适用性: 由于其兼容性和易于集成,它们通常是各种用例的首选。
- IR 函数
推荐用例: IR 函数擅长处理不需要复杂逻辑或依赖复杂第三方库的直接任务。与 C 函数不同,IR 函数具有可内联的优点,这对于调用开销构成显著成本的简单操作特别有利。此外,它们是已与 LLVM 工具链集成的项目的理想选择。
IR 编译要求: 对于 IR 函数,整个实现(包括所使用的任何第三方库)必须编译成 LLVM IR。这可能会影响性能,特别是如果依赖库很复杂。
能力限制: 某些高级功能,例如使用线程局部变量,在 IR 函数中不受支持。这是由于 Gandiva 内部使用的当前 JIT(即时)引擎的限制。
外部函数注册#
要使函数可供 Gandiva 使用,您需要将其注册为外部函数,并向 Gandiva 提供函数的元数据和实现。
使用 NativeFunction 类进行元数据注册#
要在 Gandiva 中注册函数,请使用 gandiva::NativeFunction 类。此类捕获外部函数的签名和元数据。
gandiva::NativeFunction 的构造函数详细信息
NativeFunction(const std::string& base_name, const std::vector<std::string>& aliases,
const DataTypeVector& param_types, const DataTypePtr& ret_type,
the ResultNullableType& result_nullable_type, std::string pc_name,
int32_t flags = 0);
NativeFunction 类用于定义外部函数的元数据。以下是其构造函数参数的细分:
base_name:函数在表达式中使用的名称。aliases:函数的备用名称列表。param_types:一个arrow::DataType对象向量,表示函数接受的参数类型。ret_type:一个std::shared_ptr<arrow::DataType>,表示函数的返回类型。result_nullable_type:此参数指示结果是否可以为 null,基于输入参数的 nullability。它可以取以下值之一:ResultNullableType::kResultNullIfNull:结果有效性是子元素有效性的交集。ResultNullableType::kResultNullNever:结果始终有效。ResultNullableType::kResultNullInternal:结果有效性取决于一些内部逻辑。
pc_name:相应预编译函数的名称。* 通常,此名称遵循{base_name}+_{param1_type}+{param2_type}+ … +{paramN_type}的约定。例如,如果基本名称是add,并且函数接受两个int32参数并返回一个int32,则预编译函数名称将是add_int32_int32,但只要能保证其唯一性,此约定不是强制性的。flags:用于附加函数属性的可选标志(默认为 0)。请查看NativeFunction::kNeedsContext、NativeFunction::kNeedsFunctionHolder和NativeFunction::kCanReturnErrors以获取更多详细信息。
函数注册后,其实现需要通过 C 函数指针或 LLVM IR 函数提供。
外部 C 函数#
外部 C 函数可以用不同的语言编写并作为 C 函数暴露。与 Gandiva 的类型系统兼容至关重要。
C 函数签名#
签名映射#
并非所有 Arrow 数据类型都在 Gandiva 中受支持。下表列出了 Gandiva 外部函数签名类型与 C 函数签名类型之间的映射:
Gandiva 类型 (Arrow 数据类型) |
C 函数类型 |
|---|---|
int8 |
int8_t |
int16 |
int16_t |
int32 |
int32_t |
int64 |
int64_t |
uint8 |
uint8_t |
uint16 |
uint16_t |
uint32 |
uint32_t |
uint64 |
uint64_t |
float32 |
float |
float64 |
double |
布尔型 (boolean) |
bool |
date32 |
int32_t |
date64 |
int64_t |
时间戳型 (timestamp) |
int64_t |
time32 |
int32_t |
time64 |
int64_t |
interval_month |
int32_t |
interval_day_time |
int64_t |
utf8 (作为参数类型) |
const char*, uint32_t [参见下一节] |
utf8 (作为返回类型) |
int64_t context, const char*, uint32_t* [参见下一节] |
binary (作为参数类型) |
const char*, uint32_t [参见下一节] |
utf8 (作为返回类型) |
int64_t context, const char*, uint32_t* [参见下一节] |
处理 arrow::StringType (utf8 类型) 和 arrow::BinaryType#
arrow::StringType 和 arrow::BinaryType 都是变长类型。它们在外部函数中以类似方式处理。由于 arrow::StringType (utf8 类型) 更常用,我们将在下面用它作为示例来解释如何在外部函数中处理变长类型。
将 arrow::StringType(也称为 utf8 类型)用作函数参数或返回值需要在外部函数中进行特殊处理。本节详细介绍了如何处理 arrow::StringType。
作为参数
当 arrow::StringType 在函数签名中用作参数类型时,相应的 C 函数应定义为接受两个参数:
const char*:此参数用作字符串数据的指针。uint32_t:此参数表示字符串数据的长度。
作为返回类型
当 arrow::StringType(utf8 类型)在函数签名中用作返回类型时,需要考虑几个具体事项:
NativeFunction 元数据标志: * 此函数的
NativeFunction元数据必须包含NativeFunction::kNeedsContext标志。此标志对于确保函数中正确的上下文管理至关重要。- 函数参数
上下文参数:C 函数应以一个附加参数
int64_t context开始。此参数对于函数内的上下文管理至关重要。字符串长度输出参数:函数还应在末尾包含一个
uint32_t*参数。此输出参数将存储返回字符串数据的长度。
返回值:函数应返回一个
const char*指针,指向字符串数据。函数实现: * 内存分配和错误消息: 在函数的实现中,分别使用
gdv_fn_context_arena_malloc和gdv_fn_context_set_error_msg进行内存分配和错误消息处理。这两个函数都将int64_t context作为它们的第一个参数,从而促进高效的上下文利用。
外部 C 函数注册 API#
您可以使用 gandiva::FunctionRegistry 的 API 来注册外部 C 函数:
/// \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);
上述 API 允许您注册外部 C 函数。
NativeFunction对象描述了外部 C 函数的元数据。c_function_ptr是指向外部 C 函数实现的函数指针。可选的
function_holder_maker用于为外部 C 函数创建一个函数持有者(如果外部 C 函数需要函数持有者)。请查看gandiva::FunctionHolder类及其几个子类以获取更多详细信息。
外部 IR 函数#
IR 函数实现#
Gandiva 对 IR(中间表示)函数的支持提供了在各种编程语言中实现这些函数的灵活性,具体取决于您的特定需求。
编译示例和工具#
使用 C++ 或 C
如果您的 IR 函数是用 C++ 或 C 实现的,它们可以编译成 LLVM bitcode,这是 Gandiva 理解的中间表示。
使用 Clang 编译:对于 C++ 实现,您可以使用 clang 和
-emit-llvm选项。此方法将您的 IR 函数直接编译成 LLVM bitcode,使其可以与 Gandiva 集成。
与 CMake 集成
在 C++ 与 CMake 一起使用的项目中,考虑利用 Arrow 存储库中的
GandivaAddBitcode.cmake模块。此模块可以简化将自定义 bitcode 添加到 Gandiva 的过程。
参数和返回类型的一致性#
保持与 C 函数中建立的参数和返回类型的一致性很重要。遵循上一节中讨论的规则可确保与 Gandiva 类型系统兼容。
在 Gandiva 中注册外部 IR 函数#
实现和编译后
成功实现 IR 函数并将其编译成 LLVM bitcode 后,下一个关键步骤是在 Gandiva 中注册它们。
利用 Gandiva 的 FunctionRegistry API
Gandiva 在
gandiva::FunctionRegistry类中提供了特定的 API 来促进此注册过程。注册 API
从 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 旨在从指定的 bitcode 文件或预加载的 bitcode 缓冲区注册一组外部 IR 函数。
确保 bitcode 文件或缓冲区包含正确编译的 IR 函数至关重要。
NativeFunction实例在此过程中发挥着关键作用,用于定义正在注册的每个外部 IR 函数的元数据。
结论#
本指南提供了将外部函数集成到 Gandiva 中的概述和详细步骤。它涵盖了 C 和 IR 函数,以及它们在 Gandiva 中的注册。对于更复杂的场景,请参考 Gandiva 的文档和源代码中的示例实现。