Skip to content

Commit 2fb5e49

Browse files
authored
docs: add C integration guide (#581)
1 parent fbde45c commit 2fb5e49

3 files changed

Lines changed: 322 additions & 0 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/mkdocs.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ nav:
5050
- Getting Started: getting-started.md
5151
- SQL Integration: sql.md
5252
- Performance: benchmark.md
53+
- C Integration: c-binding.md
5354
- Go Integration: go-binding.md
5455
- Python Integration: python-binding.md
5556
- Architecture: architecture.md

docs/src/c-binding.md

Lines changed: 320 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,320 @@
1+
<!--
2+
Licensed to the Apache Software Foundation (ASF) under one
3+
or more contributor license agreements. See the NOTICE file
4+
distributed with this work for additional information
5+
regarding copyright ownership. The ASF licenses this file
6+
to you under the Apache License, Version 2.0 (the
7+
"License"); you may not use this file except in compliance
8+
with the License. You may obtain a copy of the License at
9+
10+
http://www.apache.org/licenses/LICENSE-2.0
11+
12+
Unless required by applicable law or agreed to in writing,
13+
software distributed under the License is distributed on an
14+
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
KIND, either express or implied. See the License for the
16+
specific language governing permissions and limitations
17+
under the License.
18+
-->
19+
20+
# C Integration
21+
22+
The C integration exposes Apache Paimon Rust through a C ABI. It provides
23+
catalog and table access, scan planning, predicate push-down, streaming reads,
24+
writes and commits, and vector search. Record batches cross the ABI through the
25+
[Arrow C Data Interface](https://arrow.apache.org/docs/format/CDataInterface.html).
26+
27+
The C binding is currently built from source. The repository does not check in
28+
a generated header or publish pre-built C packages.
29+
30+
## Prerequisites
31+
32+
- A Rust toolchain supported by this repository
33+
- A C11-compatible compiler
34+
- [`cbindgen`](https://github.com/mozilla/cbindgen) for generating the C header
35+
- An Arrow implementation if the application reads or writes record batches
36+
37+
Install `cbindgen` when it is not already available:
38+
39+
```bash
40+
cargo install cbindgen --locked
41+
```
42+
43+
## Building the Library and Header
44+
45+
Run the following commands from the repository root:
46+
47+
```bash
48+
cargo build --release -p paimon-c
49+
cbindgen bindings/c --lang c --output target/release/paimon.h
50+
```
51+
52+
The build produces a dynamic library and a static library under
53+
`target/release/`. Dynamic library names are platform-specific:
54+
55+
| Platform | Dynamic library |
56+
|----------|-----------------|
57+
| Linux | `libpaimon_c.so` |
58+
| macOS | `libpaimon_c.dylib` |
59+
| Windows | `paimon_c.dll` |
60+
61+
Link the generated header and library into an application:
62+
63+
```bash
64+
cc -std=c11 example.c \
65+
-Itarget/release \
66+
-Ltarget/release \
67+
-lpaimon_c \
68+
-o example
69+
```
70+
71+
Make the dynamic library visible when running the executable. For example:
72+
73+
```bash
74+
# Linux
75+
LD_LIBRARY_PATH=target/release ./example /path/to/warehouse
76+
77+
# macOS
78+
DYLD_LIBRARY_PATH=target/release ./example /path/to/warehouse
79+
```
80+
81+
## Opening and Scanning a Table
82+
83+
The following program opens `default.my_table` from a filesystem catalog and
84+
plans its data splits. Result structs contain either the requested handle or a
85+
non-null error.
86+
87+
```c
88+
#include <stdint.h>
89+
#include <stdio.h>
90+
#include <stdlib.h>
91+
92+
#include "paimon.h"
93+
94+
#define CHECK_RESULT(result) \
95+
do { \
96+
if ((result).error != NULL) { \
97+
fprintf(stderr, "Paimon error %d: %.*s\n", \
98+
(result).error->code, \
99+
(int)(result).error->message.len, \
100+
(const char *)(result).error->message.data); \
101+
paimon_error_free((result).error); \
102+
goto cleanup; \
103+
} \
104+
} while (0)
105+
106+
int main(int argc, char **argv) {
107+
int status = EXIT_FAILURE;
108+
paimon_catalog *catalog = NULL;
109+
paimon_identifier *identifier = NULL;
110+
paimon_table *table = NULL;
111+
paimon_read_builder *read_builder = NULL;
112+
paimon_table_scan *scan = NULL;
113+
paimon_plan *plan = NULL;
114+
115+
if (argc != 2) {
116+
fprintf(stderr, "usage: %s WAREHOUSE\n", argv[0]);
117+
return EXIT_FAILURE;
118+
}
119+
120+
paimon_option options[] = {
121+
{.key = "warehouse", .value = argv[1]},
122+
};
123+
paimon_result_catalog_new catalog_result =
124+
paimon_catalog_create(options, 1);
125+
CHECK_RESULT(catalog_result);
126+
catalog = catalog_result.catalog;
127+
128+
paimon_result_identifier_new identifier_result =
129+
paimon_identifier_new("default", "my_table");
130+
CHECK_RESULT(identifier_result);
131+
identifier = identifier_result.identifier;
132+
133+
paimon_result_get_table table_result =
134+
paimon_catalog_get_table(catalog, identifier);
135+
CHECK_RESULT(table_result);
136+
table = table_result.table;
137+
138+
paimon_result_read_builder builder_result =
139+
paimon_table_new_read_builder(table);
140+
CHECK_RESULT(builder_result);
141+
read_builder = builder_result.read_builder;
142+
143+
paimon_result_table_scan scan_result =
144+
paimon_read_builder_new_scan(read_builder);
145+
CHECK_RESULT(scan_result);
146+
scan = scan_result.scan;
147+
148+
paimon_result_plan plan_result = paimon_table_scan_plan(scan);
149+
CHECK_RESULT(plan_result);
150+
plan = plan_result.plan;
151+
152+
printf("planned splits: %zu\n", paimon_plan_num_splits(plan));
153+
status = EXIT_SUCCESS;
154+
155+
cleanup:
156+
paimon_plan_free(plan);
157+
paimon_table_scan_free(scan);
158+
paimon_read_builder_free(read_builder);
159+
paimon_table_free(table);
160+
paimon_identifier_free(identifier);
161+
paimon_catalog_free(catalog);
162+
return status;
163+
}
164+
```
165+
166+
Catalog options are the same options accepted by the Rust catalog factory. For
167+
example, a REST catalog can be created with:
168+
169+
```c
170+
paimon_option options[] = {
171+
{.key = "metastore", .value = "rest"},
172+
{.key = "uri", .value = "http://localhost:8080"},
173+
{.key = "warehouse", .value = "my_warehouse"},
174+
};
175+
176+
paimon_result_catalog_new result = paimon_catalog_create(options, 3);
177+
```
178+
179+
## Reading Arrow Record Batches
180+
181+
Paimon uses a **scan-then-read** flow. A scan creates a plan, and a table read
182+
consumes a range of that plan's splits through a streaming Arrow reader:
183+
184+
```c
185+
paimon_result_new_read read_result =
186+
paimon_read_builder_new_read(read_builder);
187+
CHECK_RESULT(read_result);
188+
paimon_table_read *read = read_result.read;
189+
190+
size_t split_count = paimon_plan_num_splits(plan);
191+
paimon_result_record_batch_reader reader_result =
192+
paimon_table_read_to_arrow(read, plan, 0, split_count);
193+
CHECK_RESULT(reader_result);
194+
paimon_record_batch_reader *reader = reader_result.reader;
195+
196+
for (;;) {
197+
paimon_result_next_batch next = paimon_record_batch_reader_next(reader);
198+
CHECK_RESULT(next);
199+
200+
if (next.batch.array == NULL && next.batch.schema == NULL) {
201+
break; /* End of stream. */
202+
}
203+
204+
/* Import next.batch.array and next.batch.schema with an Arrow C Data
205+
Interface consumer before freeing their container structs. */
206+
paimon_arrow_batch_free(next.batch);
207+
}
208+
209+
paimon_record_batch_reader_free(reader);
210+
paimon_table_read_free(read);
211+
```
212+
213+
`paimon_table_read_to_arrow` accepts an `offset` and `length`, so separate
214+
workers can process disjoint contiguous ranges of the same plan. The requested
215+
range is clamped to the number of available splits.
216+
217+
!!! note "Arrow ownership"
218+
After importing a returned batch with the Arrow C Data Interface, call
219+
`paimon_arrow_batch_free` to release the heap-allocated `ArrowArray` and
220+
`ArrowSchema` container structs. When writing, the ownership direction is
221+
reversed: `paimon_table_write_write_arrow_batch` consumes the exported
222+
Arrow structures, so the caller must not release them again.
223+
224+
## Projection and Predicates
225+
226+
Projection uses a null-terminated array of column names:
227+
228+
```c
229+
const char *columns[] = {"id", "name", NULL};
230+
paimon_error *error =
231+
paimon_read_builder_with_projection(read_builder, columns);
232+
if (error != NULL) {
233+
/* Inspect error->code and error->message, then free the error. */
234+
paimon_error_free(error);
235+
}
236+
```
237+
238+
Predicate literals are passed as a tagged `paimon_datum`. This example builds
239+
`id = 42` for an `INT` column and transfers the predicate to the read builder:
240+
241+
```c
242+
paimon_datum value = {0};
243+
value.tag = 3; /* INT */
244+
value.int_val = 42;
245+
246+
paimon_result_predicate predicate_result =
247+
paimon_predicate_equal(table, "id", value);
248+
CHECK_RESULT(predicate_result);
249+
250+
paimon_error *error = paimon_read_builder_with_filter(
251+
read_builder, predicate_result.predicate);
252+
if (error != NULL) {
253+
paimon_error_free(error);
254+
}
255+
```
256+
257+
The supported datum tags are:
258+
259+
| Tag | Paimon type | Value fields |
260+
|-----|-------------|--------------|
261+
| 0 | `BOOL` | `int_val` (`0` or non-zero) |
262+
| 1–4 | `TINYINT`, `SMALLINT`, `INT`, `BIGINT` | `int_val` |
263+
| 5–6 | `FLOAT`, `DOUBLE` | `double_val` |
264+
| 7 | `STRING` | `str_data`, `str_len` |
265+
| 8–9 | `DATE`, `TIME` | `int_val` |
266+
| 10–11 | `TIMESTAMP`, `TIMESTAMP WITH LOCAL TIME ZONE` | `int_val`, `int_val2` |
267+
| 12 | `DECIMAL` | `int_val`, `int_val2`, `uint_val`, `uint_val2` |
268+
| 13 | `BYTES` | `str_data`, `str_len` |
269+
270+
Leaf constructors support comparisons, null checks, `IN`, string operations,
271+
and ranges. Combine predicates with `paimon_predicate_and`,
272+
`paimon_predicate_or`, and `paimon_predicate_not`.
273+
274+
!!! warning "Predicate ownership"
275+
`paimon_read_builder_with_filter`, the compound predicate functions, and
276+
`paimon_vector_search_builder_with_filter` consume their predicate inputs.
277+
Do not reuse or free a predicate after passing it to one of these functions.
278+
A predicate that has not been consumed must be released with
279+
`paimon_predicate_free`.
280+
281+
## Writing and Committing
282+
283+
Writing uses a **write-then-commit** flow:
284+
285+
1. Create one `paimon_write_builder` from the table.
286+
2. Create a `paimon_table_write` and `paimon_table_commit` from that same
287+
builder.
288+
3. Export each record batch through the Arrow C Data Interface and pass it to
289+
`paimon_table_write_write_arrow_batch`.
290+
4. Call `paimon_table_write_prepare_commit` to obtain commit messages.
291+
5. Pass the messages to `paimon_table_commit_commit`.
292+
6. Free the messages, writer, committer, and builder.
293+
294+
The input Arrow schema must match the table schema exactly, including field
295+
count, order, names, and types. A non-nullable table field must not contain null
296+
values.
297+
298+
!!! warning "Write builder consistency"
299+
The writer and committer must be created from the same write builder because
300+
they share a commit identity. Commit messages must be freed with
301+
`paimon_commit_messages_free` even after a successful commit. The caller
302+
retains message ownership and may retry a failed commit.
303+
304+
## Error Handling and Resource Ownership
305+
306+
Functions that can fail use one of two conventions:
307+
308+
- Constructors and terminal operations return a result struct with an `error`
309+
field. On success, `error` is null.
310+
- Mutating builder functions return `paimon_error *` directly. A null pointer
311+
means success.
312+
313+
Error messages are byte buffers and are not null-terminated. Read exactly
314+
`error->message.len` bytes, and release the entire error with
315+
`paimon_error_free`. Do not separately call `paimon_bytes_free` on an error
316+
message.
317+
318+
Every opaque handle returned by the C API has a matching free function. Free
319+
resources in reverse construction order. Pointer-based free functions accept
320+
null, which makes cleanup paths straightforward.

0 commit comments

Comments
 (0)