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(即时)引擎的限制。

External C functions and IR functions integrating with Gandiva

外部函数注册#

要使函数可用于 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:此参数指示结果是否可以为空,具体取决于输入参数的可空性。 它可以采用以下值之一
    • 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::kNeedsContextNativeFunction::kNeedsFunctionHolderNativeFunction::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::StringTypearrow::BinaryType 都是可变长度类型。 它们在外部函数中的处理方式相似。 由于 arrow::StringType(utf8 类型)更常用,因此我们将在下面使用它作为示例来解释如何在外部函数中处理可变长度类型。

在外部函数中,将 arrow::StringType(也称为 utf8 类型)用作函数参数或返回值需要特殊处理。 本节提供有关如何处理 arrow::StringType 的详细信息。

作为参数

arrow::StringType 用作函数签名中的参数类型时,应将相应的 C 函数定义为接受两个参数

  • const char*:此参数用作指向字符串数据的指针。

  • uint32_t:此参数表示字符串数据的长度。

作为返回类型

arrow::StringTypeutf8 类型)用作函数签名中的返回类型时,需要考虑几个特定事项

  1. NativeFunction 元数据标志: * 此函数的 NativeFunction 元数据必须包含 NativeFunction::kNeedsContext 标志。 此标志对于确保函数中正确的上下文管理至关重要。

  2. 函数参数
    • 上下文参数: C 函数应该以一个额外的参数开始,int64_t context。 此参数对于函数中的上下文管理至关重要。

    • 字符串长度输出参数: 函数还应该在末尾包含一个 uint32_t* 参数。 这个输出参数将存储返回的字符串数据的长度。

  3. 返回值: 函数应该返回一个 const char* 指针,指向字符串数据。

  4. 函数实现: * 内存分配和错误消息: 在函数的实现中,分别使用 gdv_fn_context_arena_mallocgdv_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(中间表示)函数的支持提供了使用各种编程语言实现这些函数的灵活性,具体取决于您的特定需求。

编译的示例和工具#
  1. 使用 C++ 或 C

    • 如果您的 IR 函数是用 C++ 或 C 实现的,它们可以被编译成 LLVM bitcode,这是 Gandiva 理解的中间表示。

    • 使用 Clang 进行编译:对于 C++ 实现,您可以使用带有 -emit-llvm 选项的 clang。 这种方法将您的 IR 函数直接编译成 LLVM bitcode,使其可以与 Gandiva 集成。

  2. 与 CMake 集成

    • 在使用 C++ 和 CMake 的项目中,考虑利用 Arrow 存储库中的 GandivaAddBitcode.cmake 模块。 该模块可以简化将您的自定义 bitcode 添加到 Gandiva 的过程。

参数和返回类型的一致性#

重要的是要保持与 C 函数中建立的参数和返回类型的一致性。 遵守前一节中讨论的规则可确保与 Gandiva 的类型系统的兼容性。

在 Gandiva 中注册外部 IR 函数#

  1. 实现和编译后

    成功地将您的 IR 函数实现和编译成 LLVM bitcode 后,下一个关键步骤是在 Gandiva 中注册它们。

  2. 利用 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 的文档和源代码中的示例实现。