Skip to content

Commit 597df2c

Browse files
committed
Testing FFI from C side
Signed-off-by: Mikhail Kot <mikhail@spiraldb.com>
1 parent 1c8667c commit 597df2c

6 files changed

Lines changed: 214 additions & 8 deletions

File tree

.github/workflows/ci.yml

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -821,6 +821,31 @@ jobs:
821821
run: |
822822
find flatbuffers/ -type f -name "*.fbs" | sed 's/^flatbuffers\///' | xargs -I{} -n1 flatc -I flatbuffers.HEAD --conform-includes flatbuffers --conform flatbuffers/{} flatbuffers.HEAD/{}
823823
824+
ffi-c-test:
825+
name: "C API test build"
826+
timeout-minutes: 10
827+
runs-on: >-
828+
${{ github.repository == 'vortex-data/vortex'
829+
&& format('runs-on={0}/runner=amd64-medium/image=ubuntu24-full-x64-pre-v2/tag=cxx-build', github.run_id)
830+
|| 'ubuntu-latest' }}
831+
steps:
832+
- uses: runs-on/action@v2
833+
if: github.repository == 'vortex-data/vortex'
834+
with:
835+
sccache: s3
836+
- uses: actions/checkout@v6
837+
- uses: ./.github/actions/setup-prebuild
838+
- name: "regenerate FFI header file"
839+
run: |
840+
cargo +$NIGHTLY_TOOLCHAIN build -p vortex-ffi
841+
- name: Build and run C++ unit tests
842+
run: |
843+
cd vortex-ffi
844+
mkdir build
845+
cmake -Bbuild
846+
cmake --build build --parallel $(nproc)
847+
ctest --test-dir build -V
848+
824849
check-java-publish-build:
825850
runs-on: ${{ matrix.target.runs-on }}
826851
container:

vortex-ffi/CMakeLists.txt

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
# SPDX-License-Identifier: Apache-2.0
2+
# SPDX-FileCopyrightText: Copyright the Vortex contributors
3+
cmake_minimum_required(VERSION 3.10)
4+
5+
project(VortexFFI
6+
VERSION 0.0.1
7+
LANGUAGES C CXX)
8+
set(CMAKE_C_STANDARD 17)
9+
set(CMAKE_CXX_STANDARD 20)
10+
set(CMAKE_CXX_STANDARD_REQUIRED ON)
11+
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
12+
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wall -Werror -Wextra -Wpedantic")
13+
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Werror -Wextra -Wpedantic")
14+
15+
option(BUILD_TESTING "Build tests" ON)
16+
17+
if (NOT CMAKE_BUILD_TYPE)
18+
set(CMAKE_BUILD_TYPE Debug)
19+
endif()
20+
21+
string(TOLOWER ${CMAKE_BUILD_TYPE} CMAKE_BUILD_TYPE_LOWER)
22+
set(LIBRARY_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../target/${CMAKE_BUILD_TYPE_LOWER}")
23+
24+
if(WIN32)
25+
set(LIBRARY_PATH "${LIBRARY_DIR}/libvortex_ffi.dll")
26+
elseif(APPLE)
27+
set(LIBRARY_PATH "${LIBRARY_DIR}/libvortex_ffi.dylib")
28+
else()
29+
set(LIBRARY_PATH "${LIBRARY_DIR}/libvortex_ffi.so")
30+
endif()
31+
32+
set(LIBRARY_HEADERS "${CMAKE_CURRENT_SOURCE_DIR}/cinclude")
33+
34+
message("Library dir ${LIBRARY_DIR}
35+
Library path ${LIBRARY_PATH}
36+
Headers path ${LIBRARY_HEADERS}")
37+
38+
if (NOT EXISTS "${LIBRARY_PATH}")
39+
message(FATAL_ERROR "FFI library not found")
40+
endif()
41+
42+
add_library(vortex_ffi SHARED IMPORTED)
43+
set_target_properties(vortex_ffi PROPERTIES
44+
IMPORTED_LOCATION "${LIBRARY_PATH}"
45+
INTERFACE_INCLUDE_DIRECTORIES "${LIBRARY_HEADERS}"
46+
INTERFACE_LINK_OPTIONS "LINKER:-rpath,${LIBRARY_DIR}"
47+
)
48+
49+
if (BUILD_TESTING)
50+
enable_testing()
51+
add_subdirectory(test)
52+
endif()

vortex-ffi/README.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,3 +40,18 @@ Stable builds use the checked-in header file at `cinclude/vortex.h`.
4040
- **For header changes**: Use nightly toolchain to regenerate headers after modifying FFI code
4141
- **For regular development**: Stable toolchain builds work with existing checked-in headers
4242
- **CI validation**: Automated checks verify header freshness using nightly toolchain
43+
44+
### Testing
45+
46+
Build the test library
47+
48+
```
49+
cmake -Bbuild
50+
cmake --build build
51+
```
52+
53+
Run the tests
54+
55+
```
56+
ctest --test-dir build
57+
```

vortex-ffi/src/dtype.rs

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -206,10 +206,9 @@ pub unsafe extern "C-unwind" fn vx_dtype_decimal_scale(dtype: *const vx_dtype) -
206206
pub unsafe extern "C-unwind" fn vx_dtype_struct_dtype(
207207
dtype: *const vx_dtype,
208208
) -> *const vx_struct_fields {
209-
// TODO(joe): propagate this error up instead of expecting
210-
let struct_dtype = vx_dtype::as_ref(dtype)
211-
.as_struct_fields_opt()
212-
.vortex_expect("not a struct dtype");
209+
let Some(struct_dtype) = vx_dtype::as_ref(dtype).as_struct_fields_opt() else {
210+
return ptr::null();
211+
};
213212
vx_struct_fields::new_ref(struct_dtype)
214213
}
215214

@@ -219,10 +218,9 @@ pub unsafe extern "C-unwind" fn vx_dtype_struct_dtype(
219218
/// Do NOT free the returned dtype pointer - it shares the lifetime of the list dtype.
220219
#[unsafe(no_mangle)]
221220
pub unsafe extern "C-unwind" fn vx_dtype_list_element(dtype: *const vx_dtype) -> *const vx_dtype {
222-
// TODO(joe): propagate this error up instead of expecting
223-
let element_dtype = vx_dtype::as_ref(dtype)
224-
.as_list_element_opt()
225-
.vortex_expect("not a list dtype");
221+
let Some(element_dtype) = vx_dtype::as_ref(dtype).as_list_element_opt() else {
222+
return ptr::null();
223+
};
226224
vx_dtype::new_ref(element_dtype)
227225
}
228226

vortex-ffi/test/CMakeLists.txt

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# SPDX-License-Identifier: Apache-2.0
2+
# SPDX-FileCopyrightText: Copyright the Vortex contributors
3+
include(CTest)
4+
include(FetchContent)
5+
6+
# TODO submodule may be cleaner since we want same testing for vortex-cxx
7+
FetchContent_Declare(
8+
Catch
9+
GIT_REPOSITORY https://github.com/catchorg/Catch2.git
10+
GIT_TAG v3.8.1
11+
)
12+
FetchContent_MakeAvailable(Catch)
13+
include(Catch)
14+
15+
file(GLOB TEST_FILES "${CMAKE_CURRENT_SOURCE_DIR}/*.cpp")
16+
add_executable(vortex_ffi_test ${TEST_FILES})
17+
target_link_libraries(vortex_ffi_test PRIVATE vortex_ffi Catch2::Catch2WithMain)
18+
catch_discover_tests(vortex_ffi_test)

vortex-ffi/test/main.cpp

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
#include <catch2/catch_test_macros.hpp>
2+
3+
// TODO remove
4+
typedef void FFI_ArrowSchema;
5+
typedef void FFI_ArrowArrayStream;
6+
7+
#include "vortex.h"
8+
9+
using namespace std::string_literals;
10+
using namespace std::string_view_literals;
11+
12+
TEST_CASE("Session creation", "[session]") {
13+
vx_session *session = vx_session_new();
14+
REQUIRE(session != nullptr);
15+
vx_session *session2 = vx_session_clone(session);
16+
REQUIRE(session2 != nullptr);
17+
REQUIRE(session != session2);
18+
vx_session_free(session);
19+
vx_session_free(session2);
20+
}
21+
22+
TEST_CASE("Creating and iterating binaries", "[binary]") {
23+
for (std::string_view str : {"ololo"sv, "Широкая строка"sv, "مرحبا بالعالم"sv}) {
24+
const vx_binary *binary = vx_binary_new(str.data(), str.size());
25+
26+
REQUIRE(binary != nullptr);
27+
const size_t len = vx_binary_len(binary);
28+
REQUIRE(len == str.size());
29+
30+
const char *ptr = vx_binary_ptr(binary);
31+
REQUIRE(std::string_view {ptr, len} == str);
32+
33+
const vx_binary *binary2 = vx_binary_clone(binary);
34+
vx_binary_free(binary);
35+
36+
ptr = vx_binary_ptr(binary2);
37+
REQUIRE(std::string_view {ptr, len} == str);
38+
39+
vx_binary_free(binary2);
40+
}
41+
}
42+
43+
TEST_CASE("Creating dtypes", "[dtype]") {
44+
const vx_dtype *dtype = vx_dtype_new_null();
45+
REQUIRE(dtype != nullptr);
46+
CHECK(vx_dtype_get_variant(dtype) == DTYPE_NULL);
47+
CHECK(vx_dtype_is_nullable(dtype));
48+
vx_dtype_free(dtype);
49+
50+
dtype = vx_dtype_new_decimal(5, 2, false);
51+
REQUIRE(dtype != nullptr);
52+
CHECK(vx_dtype_get_variant(dtype) == DTYPE_DECIMAL);
53+
CHECK(vx_dtype_decimal_precision(dtype) == 5);
54+
CHECK(vx_dtype_decimal_scale(dtype) == 2);
55+
CHECK_FALSE(vx_dtype_is_nullable(dtype));
56+
57+
CHECK(vx_dtype_struct_dtype(dtype) == nullptr);
58+
CHECK(vx_dtype_list_element(dtype) == nullptr);
59+
60+
vx_dtype_free(dtype);
61+
}
62+
63+
constexpr size_t STRUCT_LEN = 10;
64+
TEST_CASE("Creating structs", "[struct]") {
65+
vx_struct_fields_builder *builder = vx_struct_fields_builder_new();
66+
REQUIRE(builder != nullptr);
67+
68+
for (size_t i = 0; i < STRUCT_LEN; ++i) {
69+
const std::string target_name = "name"s + std::to_string(i);
70+
const vx_string *name = vx_string_new(target_name.data(), target_name.size());
71+
const vx_dtype *dtype = i % 2 ? vx_dtype_new_binary(false) : vx_dtype_new_primitive(PTYPE_F32, true);
72+
vx_struct_fields_builder_add_field(builder, name, dtype);
73+
}
74+
const vx_struct_fields *fields = vx_struct_fields_builder_finalize(builder);
75+
REQUIRE(fields != nullptr);
76+
77+
const size_t len = vx_struct_fields_nfields(fields);
78+
CHECK(len == STRUCT_LEN);
79+
for (size_t i = 0; i < len; ++i) {
80+
const vx_string *name = vx_struct_fields_field_name(fields, i);
81+
const vx_dtype *dtype = vx_struct_fields_field_dtype(fields, i);
82+
83+
std::string_view name_view {vx_string_ptr(name), vx_string_len(name)};
84+
std::string target_name = "name"s + std::to_string(i);
85+
86+
CHECK(name_view == target_name);
87+
88+
if (i % 2) {
89+
CHECK_FALSE(vx_dtype_is_nullable(dtype));
90+
CHECK(vx_dtype_get_variant(dtype) == DTYPE_BINARY);
91+
} else {
92+
CHECK(vx_dtype_is_nullable(dtype));
93+
CHECK(vx_dtype_get_variant(dtype) == DTYPE_PRIMITIVE);
94+
}
95+
}
96+
97+
vx_struct_fields_free(fields);
98+
}

0 commit comments

Comments
 (0)