Arrow C 数据接口#

原理#

Apache Arrow 旨在成为一种通用的内存格式,用于表示表格(“列式”)数据。然而,一些项目可能面临一个艰难的选择:要么依赖于像 Arrow C++ 库这样快速演进的项目,要么不得不重新实现数据交换的适配器,这可能需要大量、冗余的开发工作。

Arrow C 数据接口定义了一套非常小且稳定的 C 定义,可以轻松地复制到任何项目的源代码中,并用于以 Arrow 格式进行列式数据交换。对于非 C/C++ 的语言和运行时,将这些 C 定义转换为相应的 C FFI 声明也应该同样简单。

因此,应用程序和库可以在不一定使用 Arrow 库或重新造轮子的情况下处理 Arrow 内存。开发者可以在与 Arrow 软件项目的紧密集成(受益于例如 Apache Arrow 的 C++ 或 Java 实现所提供的日益增多的功能,但需要付出依赖的代价)和仅与 Arrow 格式的最小集成之间做出选择。

目标#

  • 公开一个 ABI 稳定的接口。

  • 使第三方项目能够轻松实现支持(包括在足够情况下的部分支持),只需很少的初始投入。

  • 允许在同一进程中运行的独立运行时和组件之间零拷贝共享 Arrow 数据。

  • 紧密匹配 Arrow 数组概念,以避免开发另一个编组层。

  • 避免需要一对一的适配层,例如 Java 和 Python 之间基于 JPype 的有限桥接。

  • 在没有对 Arrow 软件项目显式依赖(编译时或运行时)的情况下实现集成。

理想情况下,Arrow C 数据接口可以成为在运行时共享列式数据的底层通用语言,并将 Arrow 确立为列式处理生态系统中的通用构建块。

非目标#

  • 公开一个模仿更高级别运行时(如 C++、Java…)中可用操作的 C API。

  • 在不同进程或存储持久化之间共享数据。

与 Arrow IPC 格式的比较#

C 数据接口相对于 IPC 格式的优点

  • 不依赖 Flatbuffers。

  • 无需缓冲区重组(数据已以逻辑 Arrow 格式公开)。

  • 设计上即为零拷贝。

  • 易于从头重新实现。

  • 可以轻松复制到其他代码库的最小化 C 定义。

  • 通过自定义释放回调进行资源生命周期管理。

IPC 格式相对于数据接口的优点

  • 可跨进程和机器工作。

  • 允许数据存储和持久化。

  • 作为一种可流式处理的格式,IPC 格式有空间组合更多功能(如完整性检查、压缩……)。

  • 不需要显式的 C 数据访问。

数据类型描述 – 格式字符串#

数据类型使用格式字符串来描述。格式字符串只编码顶层类型的信息;对于嵌套类型,子类型是分开描述的。此外,元数据也编码在一个单独的字符串中。

格式字符串的设计旨在易于解析,即使是从像 C 这样的语言中。最常见的原生格式具有单字符格式字符串

格式字符串

Arrow 数据类型

备注

n

空 (null)

b

布尔型 (boolean)

c

int8

C

uint8

s

int16

S

uint16

i

int32

I

uint32

l

int64

L

uint64

e

float16

f

float32

g

float64

格式字符串

Arrow 数据类型

备注

z

二进制型 (binary)

Z

大二进制

vz

二进制视图

u

utf-8 字符串

U

大 utf-8 字符串

vu

utf-8 视图

d:19,10

decimal128 [精度 19, 小数位数 10]

d:19,10,NNN

decimal 位宽 = NNN [精度 19, 小数位数 10]

w:42

固定宽度二进制 [42 字节]

时间类型具有以 t 开头的多字符格式字符串

格式字符串

Arrow 数据类型

备注

tdD

date32 [天]

tdm

date64 [毫秒]

tts

time32 [秒]

ttm

time32 [毫秒]

ttu

time64 [微秒]

ttn

time64 [纳秒]

tss:...

timestamp [秒] 带时区 “…”

(1)

tsm:...

timestamp [毫秒] 带时区 “…”

(1)

tsu:...

timestamp [微秒] 带时区 “…”

(1)

tsn:...

timestamp [纳秒] 带时区 “…”

(1)

tDs

duration [秒]

tDm

duration [毫秒]

tDu

duration [微秒]

tDn

duration [纳秒]

tiM

interval [月]

tiD

interval [天, 时间]

tin

interval [月, 天, 纳秒]

字典编码类型没有特定的格式字符串。相反,基础数组的格式字符串代表字典索引类型,而值类型可以从依赖的字典数组中读取(见下文“字典编码数组”)。

嵌套类型具有以 + 开头的多字符格式字符串。子字段的名称和类型从子数组中读取。

格式字符串

Arrow 数据类型

备注

+l

列表型 (list)

+L

大列表

+vl

列表视图

+vL

大列表视图

+w:123

固定大小列表 [123 项]

+s

结构体

+m

映射型 (map)

(2)

+ud:I,J,...

稠密联合,类型 ID 为 I,J…

+us:I,J,...

稀疏联合,类型 ID 为 I,J…

+r

行程长度编码

(3)

备注

  1. 时区字符串原样附加在冒号字符 : 之后,不带任何引号。如果时区为空,仍然必须包含冒号 :

  2. 如 Arrow 列式格式中所规定,映射类型有一个名为 entries 的子类型,它本身是一个包含两个子字段 (key, value) 的结构体类型。

  3. 如 Arrow 列式格式中所规定,行程长度编码类型有两个子元素,第一个是(整型的)run_ends,第二个是 values

示例#

  • 一个字典编码的 decimal128(precision = 12, scale = 5) 数组,其索引为 int16,其格式字符串为 s,其依赖的字典数组的格式字符串为 d:12,5

  • 一个 list<uint64> 数组的格式字符串为 +l,其唯一的子数组的格式字符串为 L

  • 一个 large_list_view<uint64> 数组的格式字符串是 +vL,它的单个子数组的格式字符串是 L

  • 一个 struct<ints: int32, floats: float32> 数组的格式字符串为 +s;它的两个子数组的名称分别为 intsfloats,格式字符串分别为 if

  • 一个 map<string, float64> 数组的格式字符串为 +m;其唯一的子数组名称为 entries,格式字符串为 +s;其两个孙子数组名称为 keyvalue,格式字符串分别为 ug

  • 一个 sparse_union<ints: int32, floats: float32> 数组,其类型 ID 为 4, 5,其格式字符串为 +us:4,5;它的两个子数组名称为 intsfloats,格式字符串分别为 if

  • 一个 run_end_encoded<int32, float32> 的格式字符串为 +r;它的两个子元素名为 run_endsvalues,格式字符串分别为 if

结构体定义#

以下独立定义足以在您的项目中支持 Arrow C 数据接口。与 Arrow 项目的其他部分一样,它们在 Apache License 2.0 下可用。

#ifndef ARROW_C_DATA_INTERFACE
#define ARROW_C_DATA_INTERFACE

#define ARROW_FLAG_DICTIONARY_ORDERED 1
#define ARROW_FLAG_NULLABLE 2
#define ARROW_FLAG_MAP_KEYS_SORTED 4

struct ArrowSchema {
  // Array type description
  const char* format;
  const char* name;
  const char* metadata;
  int64_t flags;
  int64_t n_children;
  struct ArrowSchema** children;
  struct ArrowSchema* dictionary;

  // Release callback
  void (*release)(struct ArrowSchema*);
  // Opaque producer-specific data
  void* private_data;
};

struct ArrowArray {
  // Array data description
  int64_t length;
  int64_t null_count;
  int64_t offset;
  int64_t n_buffers;
  int64_t n_children;
  const void** buffers;
  struct ArrowArray** children;
  struct ArrowArray* dictionary;

  // Release callback
  void (*release)(struct ArrowArray*);
  // Opaque producer-specific data
  void* private_data;
};

#endif  // ARROW_C_DATA_INTERFACE

注意

规范的保护宏 ARROW_C_DATA_INTERFACE 旨在避免当两个项目将 C 数据接口定义复制到它们自己的头文件中,而第三方项目又同时包含了这两个项目的头文件时,出现重复定义的问题。因此,在复制这些定义时,保持这个保护宏原样不变非常重要。

ArrowSchema 结构体#

ArrowSchema 结构体描述了导出的数组或记录批次的类型和元数据。它有以下字段

const char *ArrowSchema.format#

必需。一个以 null 结尾的 UTF8 编码字符串,用于描述数据类型。如果数据类型是嵌套的,子类型不在此处编码,而是在 ArrowSchema.children 结构中编码。

消费者可以决定不支持所有数据类型,但应记录此限制。

const char *ArrowSchema.name#

可选。字段或数组名称的空终止 UTF8 编码字符串。这主要用于重构嵌套类型的子字段。

生产者可以决定不提供此信息,消费者可以决定忽略它。如果省略,可以是 NULL 或空字符串。

const char *ArrowSchema.metadata#

可选。一个描述类型元数据的二进制字符串。如果数据类型是嵌套的,子类型不在这里编码,而是在 ArrowSchema.children 结构中编码。

此字符串不是以 null 结尾的,而是遵循特定格式

int32: number of key/value pairs (noted N below)
int32: byte length of key 0
key 0 (not null-terminated)
int32: byte length of value 0
value 0 (not null-terminated)
...
int32: byte length of key N - 1
key N - 1 (not null-terminated)
int32: byte length of value N - 1
value N - 1 (not null-terminated)

整数以本机字节序存储。例如,元数据 [('key1', 'value1')] 在小端机器上编码为

\x01\x00\x00\x00\x04\x00\x00\x00key1\x06\x00\x00\x00value1

在大端机器上,同样的示例将被编码为

\x00\x00\x00\x01\x00\x00\x00\x04key1\x00\x00\x00\x06value1

如果省略,此字段必须为 NULL(而不是空字符串)。

消费者可以选择忽略此信息。

int64_t ArrowSchema.flags#

可选。一个丰富类型描述的标志位域。其值是通过将标志值进行或运算计算得出的。可用的标志如下

  • ARROW_FLAG_NULLABLE:此字段在语义上是否可为空(无论其是否实际包含空值)。

  • ARROW_FLAG_DICTIONARY_ORDERED:对于字典编码类型,字典索引的顺序在语义上是否有意义。

  • ARROW_FLAG_MAP_KEYS_SORTED:对于映射类型,每个映射值中的键是否已排序。

如果省略,必须为 0。

消费者可以选择忽略部分或全部标志。即使如此,他们也应该保留这个值,以便将其信息传播给他们自己的消费者。

int64_t ArrowSchema.n_children#

必需。此类型拥有的子项数量。

ArrowSchema **ArrowSchema.children#

可选。一个指向该类型的每个子类型的 C 数组指针。必须有 ArrowSchema.n_children 个指针。

仅当 ArrowSchema.n_children 为 0 时,可以为 NULL。

ArrowSchema *ArrowSchema.dictionary#

可选。指向字典值类型的指针。

如果 ArrowSchema 代表字典编码类型,则必须存在。否则必须为 NULL。

void (*ArrowSchema.release)(struct ArrowSchema*)#

必需。一个指向由生产者提供的释放回调的指针。

有关内存管理和释放回调的语义,请参见下文。

void *ArrowSchema.private_data#

可选。一个指向生产者提供的私有数据的不透明指针。

消费者不得处理此成员。此成员的生命周期由生产者处理,尤其是通过释放回调处理。

ArrowArray 结构体#

ArrowArray 描述了导出的数组或记录批次的数据。为了正确解释 ArrowArray 结构,必须已经知道数组类型或记录批次模式。这可以通过约定实现——例如,一个总是产生相同数据类型的生产者 API——或者通过同时传递一个 ArrowSchema 来实现。

它有以下字段

int64_t ArrowArray.length#

必需。数组的逻辑长度(即其项目数)。

int64_t ArrowArray.null_count#

必需。数组中空项的数量。如果尚未计算,可以为 -1。

int64_t ArrowArray.offset#

必需。数组内部的逻辑偏移量(即从缓冲区的物理起始位置算起的项目数)。必须为 0 或正数。

生产者可以指定它们只生产偏移量为 0 的数组,以简化消费者代码的实现。消费者可以决定不支持非 0 偏移量的数组,但应记录此限制。

int64_t ArrowArray.n_buffers#

必需。支持此数组的物理缓冲区数量。缓冲区的数量是数据类型的函数,如 列式格式规范 中所述,但二进制或 utf-8 视图类型除外,它比列式格式规范多一个缓冲区(参见 二进制视图数组)。

不包括子数组的缓冲区。

const void **ArrowArray.buffers#

必需。一个 C 数组,其元素为指向支持该数组的每个物理缓冲区起始位置的指针。每个 `void*` 指针是连续缓冲区的物理起始地址。必须有 ArrowArray.n_buffers 个指针。

生产者必须确保每个连续的缓冲区都足够大,以表示根据 列式格式规范 编码的 `length + offset` 个值。

建议但不强制要求,缓冲区的内存地址至少根据其包含的原生数据类型进行对齐。消费者可以决定不支持未对齐的内存。

缓冲区指针仅在两种情况下可以为 null

  1. 对于空值位图缓冲区,如果 ArrowArray.null_count 是 0;

  2. 对于任何缓冲区,如果相应缓冲区的字节大小为 0。

不包括子数组的缓冲区。

int64_t ArrowArray.n_children#

必需。该数组拥有的子数组数量。子数组的数量是数据类型的函数,如 列式格式规范 所述。

ArrowArray **ArrowArray.children#

可选。一个指向该数组每个子数组的 C 数组指针。必须有 ArrowArray.n_children 个指针。

仅当 ArrowArray.n_children 为 0 时,可以为 NULL。

ArrowArray *ArrowArray.dictionary#

可选。指向底层字典值数组的指针。

如果 ArrowArray 代表一个字典编码的数组,则必须存在。否则必须为 NULL。

void (*ArrowArray.release)(struct ArrowArray*)#

必需。一个指向由生产者提供的释放回调的指针。

有关内存管理和释放回调的语义,请参见下文。

void *ArrowArray.private_data#

可选。一个指向生产者提供的私有数据的不透明指针。

消费者不得处理此成员。此成员的生命周期由生产者处理,尤其是通过释放回调处理。

字典编码数组#

对于字典编码的数组,ArrowSchema.format 字符串编码的是*索引*类型。字典的*值*类型可以从 ArrowSchema.dictionary 结构中读取。

这同样适用于 ArrowArray 结构:父结构指向索引数据,而 ArrowArray.dictionary 指向字典值数组。

扩展数组#

对于扩展数组,ArrowSchema.format 字符串编码的是*存储*类型。关于扩展类型的信息编码在 ArrowSchema.metadata 字符串中,类似于 IPC 格式。具体来说,元数据键 ARROW:extension:name 编码扩展类型名称,而元数据键 ARROW:extension:metadata 编码扩展类型的实现特定序列化(用于参数化扩展类型)。

从扩展数组导出的 ArrowArray 结构体只是指向扩展数组的存储数据。

二进制视图数组#

对于二进制或 utf-8 视图数组,会附加一个额外的缓冲区,该缓冲区以 int64_t 的形式存储每个可变数据缓冲区的长度。这个缓冲区是必需的,因为这些缓冲区的长度无法从二进制或 utf-8 视图类型数组的其他数据中轻易提取。

语义#

内存管理#

ArrowSchemaArrowArray 结构在内存管理方面遵循相同的约定。下文中的术语*“基础结构”*指的是在生产者和消费者之间传递的 ArrowSchemaArrowArray——而不是其任何子结构。

成员分配#

基础结构旨在由消费者在栈上或堆上分配。在这种情况下,生产者 API 应接受一个指向消费者分配的结构的指针。

然而,任何由结构体指向的数据都必须由生产者分配和维护。这包括格式和元数据字符串、缓冲区和子指针的数组等。

因此,消费者不得试图干预生产者对这些成员生命周期的处理。消费者影响数据生命周期的唯一方式是调用基础结构的 release 回调。

已释放的结构#

通过将其 release 回调设置为 NULL 来表示一个结构体已被释放。在读取和解释一个结构体的数据之前,消费者应该检查其 release 回调是否为 NULL,并相应地处理它(可能会报错)。

释放回调语义——对消费者而言#

当消费者不再使用一个基础结构时,必须调用该结构的释放回调,但不得调用其任何子结构(包括可选的字典)的释放回调。生产者负责释放子结构。

在任何情况下,消费者在调用基础结构的释放回调后,都不得再尝试访问该结构——包括其子结构等任何相关数据。

释放回调语义——对生产者而言#

如果生产者需要额外的信息来处理生命周期(例如,C++ 生产者可能希望使用 shared_ptr 来管理数组和缓冲区的生命周期),他们必须使用 private_data 成员来定位所需的记账信息。

释放回调不能假设结构体将位于与其最初生成时相同的内存位置。消费者可以自由移动结构体(参见“移动数组”)。

释放回调必须遍历所有子结构(包括可选的字典)并调用它们自己的释放回调。

释放回调必须释放由结构体直接拥有的任何数据区(例如缓冲区和子成员)。

释放回调必须通过将其 release 成员设置为 NULL 来标记该结构已被释放。

以下是实现释放回调的一个良好起点,其中 TODO 部分必须用生产者特定的释放代码填充

static void ReleaseExportedArray(struct ArrowArray* array) {
  // This should not be called on already released array
  assert(array->release != NULL);

  // Release children
  for (int64_t i = 0; i < array->n_children; ++i) {
    struct ArrowArray* child = array->children[i];
    if (child->release != NULL) {
      child->release(child);
      assert(child->release == NULL);
    }
  }

  // Release dictionary
  struct ArrowArray* dict = array->dictionary;
  if (dict != NULL && dict->release != NULL) {
    dict->release(dict);
    assert(dict->release == NULL);
  }

  // TODO here: release and/or deallocate all data directly owned by
  // the ArrowArray struct, such as the private_data.

  // Mark array released
  array->release = NULL;
}

移动数组#

消费者可以通过按位复制或浅层成员复制来*移动* ArrowArray 结构。然后它必须将源结构标记为已释放(参见上面的“已释放结构”了解如何操作),但*不*调用释放回调。这确保在任何给定时间只有一个结构的活动副本,并且生命周期被正确地传达给生产者。

和往常一样,当不再需要目标结构时,将调用其上的释放回调。

移动子数组#

也可以移动一个或多个子数组,但父 ArrowArray 结构必须在此后立即释放,因为它将不再指向有效的子数组。

这样做的主要用例是只保留一部分子数组(例如,如果您只对数据的某些列感兴趣),同时释放其他子数组。

注意

为了使移动能够正常工作,ArrowArray 结构必须是可简单重定位的。因此,ArrowArray 结构内部的指针成员(包括 private_data)不得指向结构本身内部。此外,生产者不得单独存储指向该结构的外部指针。相反,生产者必须使用 private_data 成员来记录任何必要的簿记信息。

记录批次#

一个记录批次可以简单地被视为一个等效的结构体数组。在这种情况下,顶层 ArrowSchema 的元数据可以用于记录批次的模式级元数据。

可变性#

生产者和消费者都应将导出的数据(即通过 ArrowArraybuffers 成员可访问的数据)视为不可变的,否则当一方正在修改数据时,另一方可能会看到不一致的数据。

用例示例#

一个 C++ 数据库引擎希望提供以 Arrow 格式交付结果的选项,但又不想给自己强加对 Arrow 软件库的依赖。借助 Arrow C 数据接口,该引擎可以让调用者传递一个指向 ArrowArray 结构的指针,并用下一块结果填充它。

它可以在不包含 Arrow C++ 头文件或链接 Arrow DLL 的情况下做到这一点。此外,数据库引擎的 C API 可以通过例如 C FFI 层,使其他了解 Arrow C 数据接口的运行时和库受益。

C 生产者示例#

导出一个简单的 int32 数组#

导出一个不可为空的 int32 类型,元数据为空。在这种情况下,所有 ArrowSchema 成员都指向静态分配的数据,因此释放回调很简单。

static void release_int32_type(struct ArrowSchema* schema) {
   // Mark released
   schema->release = NULL;
}

void export_int32_type(struct ArrowSchema* schema) {
   *schema = (struct ArrowSchema) {
      // Type description
      .format = "i",
      .name = "",
      .metadata = NULL,
      .flags = 0,
      .n_children = 0,
      .children = NULL,
      .dictionary = NULL,
      // Bookkeeping
      .release = &release_int32_type
   };
}

将一个 C-malloc()ed 的相同类型的数组导出为 Arrow 数组,通过释放回调将所有权转移给消费者

static void release_int32_array(struct ArrowArray* array) {
   assert(array->n_buffers == 2);
   // Free the buffers and the buffers array
   free((void *) array->buffers[1]);
   free(array->buffers);
   // Mark released
   array->release = NULL;
}

void export_int32_array(const int32_t* data, int64_t nitems,
                        struct ArrowArray* array) {
   // Initialize primitive fields
   *array = (struct ArrowArray) {
      // Data description
      .length = nitems,
      .offset = 0,
      .null_count = 0,
      .n_buffers = 2,
      .n_children = 0,
      .children = NULL,
      .dictionary = NULL,
      // Bookkeeping
      .release = &release_int32_array
   };
   // Allocate list of buffers
   array->buffers = (const void**) malloc(sizeof(void*) * array->n_buffers);
   assert(array->buffers != NULL);
   array->buffers[0] = NULL;  // no nulls, null bitmap can be omitted
   array->buffers[1] = data;
}

导出一个 struct<float32, utf8> 数组#

将数组类型导出为带有 C-malloc()ed 子项的 ArrowSchema

static void release_malloced_type(struct ArrowSchema* schema) {
   int i;
   for (i = 0; i < schema->n_children; ++i) {
      struct ArrowSchema* child = schema->children[i];
      if (child->release != NULL) {
         child->release(child);
      }
      free(child);
   }
   free(schema->children);
   // Mark released
   schema->release = NULL;
}

void export_float32_utf8_type(struct ArrowSchema* schema) {
   struct ArrowSchema* child;

   //
   // Initialize parent type
   //
   *schema = (struct ArrowSchema) {
      // Type description
      .format = "+s",
      .name = "",
      .metadata = NULL,
      .flags = 0,
      .n_children = 2,
      .dictionary = NULL,
      // Bookkeeping
      .release = &release_malloced_type
   };
   // Allocate list of children types
   schema->children = malloc(sizeof(struct ArrowSchema*) * schema->n_children);

   //
   // Initialize child type #0
   //
   child = schema->children[0] = malloc(sizeof(struct ArrowSchema));
   *child = (struct ArrowSchema) {
      // Type description
      .format = "f",
      .name = "floats",
      .metadata = NULL,
      .flags = ARROW_FLAG_NULLABLE,
      .n_children = 0,
      .dictionary = NULL,
      .children = NULL,
      // Bookkeeping
      .release = &release_malloced_type
   };

   //
   // Initialize child type #1
   //
   child = schema->children[1] = malloc(sizeof(struct ArrowSchema));
   *child = (struct ArrowSchema) {
      // Type description
      .format = "u",
      .name = "strings",
      .metadata = NULL,
      .flags = ARROW_FLAG_NULLABLE,
      .n_children = 0,
      .dictionary = NULL,
      .children = NULL,
      // Bookkeeping
      .release = &release_malloced_type
   };
}

将 C-malloc() 分配的、符合 Arrow 布局的数组导出为 Arrow 结构体数组,并将所有权转移给消费者

static void release_malloced_array(struct ArrowArray* array) {
   int i;
   // Free children
   for (i = 0; i < array->n_children; ++i) {
      struct ArrowArray* child = array->children[i];
      if (child->release != NULL) {
         child->release(child);
      }
      free(child);
   }
   free(array->children);
   // Free buffers
   for (i = 0; i < array->n_buffers; ++i) {
      free((void *) array->buffers[i]);
   }
   free(array->buffers);
   // Mark released
   array->release = NULL;
}

void export_float32_utf8_array(
      int64_t nitems,
      const uint8_t* float32_nulls, const float* float32_data,
      const uint8_t* utf8_nulls, const int32_t* utf8_offsets, const uint8_t* utf8_data,
      struct ArrowArray* array) {
   struct ArrowArray* child;

   //
   // Initialize parent array
   //
   *array = (struct ArrowArray) {
      // Data description
      .length = nitems,
      .offset = 0,
      .null_count = 0,
      .n_buffers = 1,
      .n_children = 2,
      .dictionary = NULL,
      // Bookkeeping
      .release = &release_malloced_array
   };
   // Allocate list of parent buffers
   array->buffers = malloc(sizeof(void*) * array->n_buffers);
   array->buffers[0] = NULL;  // no nulls, null bitmap can be omitted
   // Allocate list of children arrays
   array->children = malloc(sizeof(struct ArrowArray*) * array->n_children);

   //
   // Initialize child array #0
   //
   child = array->children[0] = malloc(sizeof(struct ArrowArray));
   *child = (struct ArrowArray) {
      // Data description
      .length = nitems,
      .offset = 0,
      .null_count = -1,
      .n_buffers = 2,
      .n_children = 0,
      .dictionary = NULL,
      .children = NULL,
      // Bookkeeping
      .release = &release_malloced_array
   };
   child->buffers = malloc(sizeof(void*) * child->n_buffers);
   child->buffers[0] = float32_nulls;
   child->buffers[1] = float32_data;

   //
   // Initialize child array #1
   //
   child = array->children[1] = malloc(sizeof(struct ArrowArray));
   *child = (struct ArrowArray) {
      // Data description
      .length = nitems,
      .offset = 0,
      .null_count = -1,
      .n_buffers = 3,
      .n_children = 0,
      .dictionary = NULL,
      .children = NULL,
      // Bookkeeping
      .release = &release_malloced_array
   };
   child->buffers = malloc(sizeof(void*) * child->n_buffers);
   child->buffers[0] = utf8_nulls;
   child->buffers[1] = utf8_offsets;
   child->buffers[2] = utf8_data;
}

为何使用两个不同的结构体?#

在许多情况下,相同的类型或模式描述适用于多个可能较短的数据批次。为避免为每个批次导出和导入类型描述的成本,ArrowSchema 可以在生产者和消费者之间对话开始时单独传递一次。

在其他情况下,数据类型由生产者 API 固定,可能根本不需要通信。

然而,如果生产者专注于一次性数据交换,它可以在同一次 API 调用中同时传递 ArrowSchemaArrowArray 结构。

更新此规范#

一旦此规范在 Arrow 的正式版本中得到支持,C ABI 即被冻结。这意味着 ArrowSchemaArrowArray 结构定义不应以任何方式更改——包括添加新成员。

允许向后兼容的更改,例如新的 ArrowSchema.flags 值或扩展 ArrowSchema.format 字符串的可能性。

任何不兼容的更改都应该是新规范的一部分,例如“Arrow C data interface v2”。

灵感#

Arrow C 数据接口的灵感来自于 Python 缓冲区协议,该协议在允许各种 Python 库在互不了解的情况下以接近零的适配成本交换数值数据方面取得了巨大成功。

特定语言的协议#

一些语言可能会在 Arrow C 数据接口之上定义额外的协议。