This is an automated email from the ASF dual-hosted git repository.

paleolimbot pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/sedona-db.git


The following commit(s) were added to refs/heads/main by this push:
     new a780bb88 feat(c/sedona-libgpuspatial): Add Rust Wrapper (#586)
a780bb88 is described below

commit a780bb88e29948c1d6fc163e6cf7b3aaa7bbd025
Author: Liang Geng <[email protected]>
AuthorDate: Wed Feb 18 17:47:22 2026 -0500

    feat(c/sedona-libgpuspatial): Add Rust Wrapper (#586)
    
    Co-authored-by: Dewey Dunnington <[email protected]>
---
 Cargo.lock                                         |  10 +
 c/sedona-libgpuspatial/Cargo.toml                  |  14 +
 .../include/gpuspatial/gpuspatial_c.h              |  46 +-
 .../libgpuspatial/src/gpuspatial_c.cc              |  26 +-
 .../libgpuspatial/test/c_wrapper_test.cc           |  40 +-
 c/sedona-libgpuspatial/src/error.rs                |  65 +++
 c/sedona-libgpuspatial/src/lib.rs                  | 326 +++++++++++-
 c/sedona-libgpuspatial/src/libgpuspatial.rs        | 578 +++++++++++++++++++++
 .../src/libgpuspatial_glue_bindgen.rs              |   8 +-
 c/sedona-libgpuspatial/src/{lib.rs => options.rs}  |  18 +-
 c/sedona-libgpuspatial/src/predicate.rs            |  61 +++
 11 files changed, 1141 insertions(+), 51 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock
index f518de7f..a13a037a 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -5440,9 +5440,19 @@ dependencies = [
 name = "sedona-libgpuspatial"
 version = "0.3.0"
 dependencies = [
+ "arrow-array",
+ "arrow-schema",
  "bindgen",
  "cmake",
+ "geo",
+ "geo-types",
+ "sedona-expr",
+ "sedona-geos",
+ "sedona-schema",
+ "sedona-testing",
+ "thiserror 2.0.17",
  "which",
+ "wkt 0.14.0",
 ]
 
 [[package]]
diff --git a/c/sedona-libgpuspatial/Cargo.toml 
b/c/sedona-libgpuspatial/Cargo.toml
index 01840813..9b872f7b 100644
--- a/c/sedona-libgpuspatial/Cargo.toml
+++ b/c/sedona-libgpuspatial/Cargo.toml
@@ -35,3 +35,17 @@ gpu = []
 bindgen = "0.72.1"
 cmake = "0.1"
 which = "8.0"
+
+[dependencies]
+arrow-array = { workspace = true, features = ["ffi"] }
+arrow-schema = { workspace = true }
+thiserror = { workspace = true }
+geo-types = { workspace = true }
+sedona-schema = { workspace = true }
+
+[dev-dependencies]
+wkt = { workspace = true }
+geo = { workspace = true }
+sedona-expr = { path = "../../rust/sedona-expr" }
+sedona-geos = { path = "../sedona-geos" }
+sedona-testing = { path = "../../rust/sedona-testing" }
diff --git 
a/c/sedona-libgpuspatial/libgpuspatial/include/gpuspatial/gpuspatial_c.h 
b/c/sedona-libgpuspatial/libgpuspatial/include/gpuspatial/gpuspatial_c.h
index 01821ac0..5842bbd5 100644
--- a/c/sedona-libgpuspatial/libgpuspatial/include/gpuspatial/gpuspatial_c.h
+++ b/c/sedona-libgpuspatial/libgpuspatial/include/gpuspatial/gpuspatial_c.h
@@ -54,7 +54,7 @@ void GpuSpatialRuntimeCreate(struct GpuSpatialRuntime* 
runtime);
 
 struct GpuSpatialIndexConfig {
   /** Pointer to an initialized GpuSpatialRuntime struct */
-  struct GpuSpatialRuntime* runtime;
+  const struct GpuSpatialRuntime* runtime;
   /** How many threads will concurrently call Probe method */
   uint32_t concurrency;
 };
@@ -71,10 +71,10 @@ struct SedonaFloatIndex2D {
   void (*create_context)(struct SedonaSpatialIndexContext* context);
   /** Destroy a previously created context */
   void (*destroy_context)(struct SedonaSpatialIndexContext* context);
-  /** Push rectangles for building the spatial index, each rectangle is 
represented by 4
-   * floats: [min_x, min_y, max_x, max_y].
+  /** Push rectangles for building the spatial index.
+   * @param buf each rectangle is represented by 4 floats: [min_x, min_y, 
max_x, max_y].
    * Points can also be indexed by providing degenerated rectangles [x, y, x, 
y].
-   *
+   * @param n_rects The number of rectangles in the buffer
    * @return 0 on success, non-zero on failure
    */
   int (*push_build)(struct SedonaFloatIndex2D* self, const float* buf, 
uint32_t n_rects);
@@ -85,29 +85,27 @@ struct SedonaFloatIndex2D {
    */
   int (*finish_building)(struct SedonaFloatIndex2D* self);
   /**
-   * Probe the spatial index with the given rectangles, each rectangle is 
represented by 4
-   * floats: [min_x, min_y, max_x, max_y] Points can also be probed by 
providing [x, y, x,
-   * y] but points and rectangles cannot be mixed in one Probe call. The 
results of the
-   * probe will be stored in the context.
+   * Probe the spatial index with the given rectangles.
+   * @param buf The buffer of rectangles to probe, stored in the same format 
as the build
+   * rectangles.
+   * @param n_rects The number of rectangles in the probe buffer.
+   * @param callback The callback function to call for each batch of results.
+   * The callback should return 0 to continue receiving results, or non-zero 
to stop the
+   * probe early. The callback will be called with arrays of build and probe 
indices
+   * corresponding to candidate pairs of rectangles that intersect.
+   * The user-provided callback is required to return a value that will be 
further passed
+   * to the probe function to indicate whether there's an error during the 
callback
+   * execution.
+   * @param user_data The user_data pointer will be passed to the callback
    *
    * @return 0 on success, non-zero on failure
    */
   int (*probe)(struct SedonaFloatIndex2D* self, struct 
SedonaSpatialIndexContext* context,
-               const float* buf, uint32_t n_rects);
-  /** Get the build indices buffer from the context
-   *
-   * @return A pointer to the buffer and its length
-   */
-  void (*get_build_indices_buffer)(struct SedonaSpatialIndexContext* context,
-                                   uint32_t** build_indices,
-                                   uint32_t* build_indices_length);
-  /** Get the probe indices buffer from the context
-   *
-   * @return A pointer to the buffer and its length
-   */
-  void (*get_probe_indices_buffer)(struct SedonaSpatialIndexContext* context,
-                                   uint32_t** probe_indices,
-                                   uint32_t* probe_indices_length);
+               const float* buf, uint32_t n_rects,
+               int (*callback)(const uint32_t* build_indices,
+                               const uint32_t* probe_indices, uint32_t length,
+                               void* user_data),
+               void* user_data);
   /** Get the last error message from either the index
    *
    * @return A pointer to the error message string
@@ -131,7 +129,7 @@ int GpuSpatialIndexFloat2DCreate(struct SedonaFloatIndex2D* 
index,
 
 struct GpuSpatialRefinerConfig {
   /** Pointer to an initialized GpuSpatialRuntime struct */
-  struct GpuSpatialRuntime* runtime;
+  const struct GpuSpatialRuntime* runtime;
   /** How many threads will concurrently call Probe method */
   uint32_t concurrency;
   /** Whether to compress the BVH structures to save memory */
diff --git a/c/sedona-libgpuspatial/libgpuspatial/src/gpuspatial_c.cc 
b/c/sedona-libgpuspatial/libgpuspatial/src/gpuspatial_c.cc
index 5c6b530a..0e0c5627 100644
--- a/c/sedona-libgpuspatial/libgpuspatial/src/gpuspatial_c.cc
+++ b/c/sedona-libgpuspatial/libgpuspatial/src/gpuspatial_c.cc
@@ -159,8 +159,6 @@ struct GpuSpatialIndexFloat2DExporter {
     out->push_build = &CPushBuild;
     out->finish_building = &CFinishBuilding;
     out->probe = &CProbe;
-    out->get_build_indices_buffer = &CGetBuildIndicesBuffer;
-    out->get_probe_indices_buffer = &CGetProbeIndicesBuffer;
     out->get_last_error = &CGetLastError;
     out->context_get_last_error = &CContextGetLastError;
     out->release = &CRelease;
@@ -194,12 +192,28 @@ struct GpuSpatialIndexFloat2DExporter {
   }
 
   static int CProbe(self_t* self, SedonaSpatialIndexContext* context, const 
float* buf,
-                    uint32_t n_rects) {
-    return SafeExecute(static_cast<context_t*>(context->private_data), [&] {
+                    uint32_t n_rects,
+                    int (*callback)(const uint32_t* build_indices,
+                                    const uint32_t* probe_indices, uint32_t 
length,
+                                    void* user_data),
+                    void* user_data) {
+    auto* p_ctx = static_cast<context_t*>(context->private_data);
+    // Do not use SafeExecute because this method is thread-safe and we don't 
want to set
+    // last_error for the whole index if one thread encounters an error
+    try {
       auto* rects = reinterpret_cast<const spatial_index_t::box_t*>(buf);
-      auto& buff = static_cast<context_t*>(context->private_data)->payload;
+      auto& buff = p_ctx->payload;
       use_index(self).Probe(rects, n_rects, &buff.build_indices, 
&buff.probe_indices);
-    });
+
+      return callback(buff.build_indices.data(), buff.probe_indices.data(),
+                      buff.build_indices.size(), user_data);
+    } catch (const std::exception& e) {  // user should call 
context_get_last_error
+      p_ctx->last_error = std::string(e.what());
+      return EINVAL;
+    } catch (...) {
+      p_ctx->last_error = "Unknown internal error";
+      return EINVAL;
+    }
   }
 
   static void CGetBuildIndicesBuffer(struct SedonaSpatialIndexContext* context,
diff --git a/c/sedona-libgpuspatial/libgpuspatial/test/c_wrapper_test.cc 
b/c/sedona-libgpuspatial/libgpuspatial/test/c_wrapper_test.cc
index 3de7fadc..35f850f0 100644
--- a/c/sedona-libgpuspatial/libgpuspatial/test/c_wrapper_test.cc
+++ b/c/sedona-libgpuspatial/libgpuspatial/test/c_wrapper_test.cc
@@ -261,19 +261,26 @@ TEST_F(CWrapperTest, InitializeJoiner) {
                            fpoint_t((float)xmax, (float)ymax));
     }
 
-    // Run SUT Probe
+    struct IntersectionIDs {
+      uint32_t* build_indices_ptr;
+      uint32_t* probe_indices_ptr;
+      uint32_t length;
+    };
+    IntersectionIDs intersection_ids{nullptr, nullptr, 0};
+
     SedonaSpatialIndexContext idx_ctx;
     index_.create_context(&idx_ctx);
-    index_.probe(&index_, &idx_ctx, (float*)queries.data(), queries.size());
-
-    // Retrieve SUT Results
-    uint32_t* build_indices_ptr;
-    uint32_t* probe_indices_ptr;
-    uint32_t build_indices_length;
-    uint32_t probe_indices_length;
-
-    index_.get_build_indices_buffer(&idx_ctx, &build_indices_ptr, 
&build_indices_length);
-    index_.get_probe_indices_buffer(&idx_ctx, &probe_indices_ptr, 
&probe_indices_length);
+    index_.probe(
+        &index_, &idx_ctx, (float*)queries.data(), queries.size(),
+        [](const uint32_t* build_indices, const uint32_t* probe_indices, 
uint32_t length,
+           void* user_data) {
+          IntersectionIDs* ids = reinterpret_cast<IntersectionIDs*>(user_data);
+          ids->build_indices_ptr = (uint32_t*)build_indices;
+          ids->probe_indices_ptr = (uint32_t*)probe_indices;
+          ids->length = length;
+          return 0;
+        },
+        &intersection_ids);
 
     refiner_.clear(&refiner_);
     ASSERT_EQ(refiner_.push_build(&refiner_, build_array.get()), 0);
@@ -283,13 +290,14 @@ TEST_F(CWrapperTest, InitializeJoiner) {
     ASSERT_EQ(refiner_.refine(
                   &refiner_, probe_array.get(),
                   
SedonaSpatialRelationPredicate::SedonaSpatialPredicateContains,
-                  build_indices_ptr, probe_indices_ptr, build_indices_length, 
&new_len),
+                  intersection_ids.build_indices_ptr, 
intersection_ids.probe_indices_ptr,
+                  intersection_ids.length, &new_len),
               0);
 
-    std::vector<uint32_t> sut_build_indices(build_indices_ptr,
-                                            build_indices_ptr + new_len);
-    std::vector<uint32_t> sut_stream_indices(probe_indices_ptr,
-                                             probe_indices_ptr + new_len);
+    std::vector<uint32_t> sut_build_indices(intersection_ids.build_indices_ptr,
+                                            intersection_ids.build_indices_ptr 
+ new_len);
+    std::vector<uint32_t> sut_stream_indices(
+        intersection_ids.probe_indices_ptr, intersection_ids.probe_indices_ptr 
+ new_len);
 
     index_.destroy_context(&idx_ctx);
 
diff --git a/c/sedona-libgpuspatial/src/error.rs 
b/c/sedona-libgpuspatial/src/error.rs
new file mode 100644
index 00000000..4ed701e3
--- /dev/null
+++ b/c/sedona-libgpuspatial/src/error.rs
@@ -0,0 +1,65 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+use arrow_schema::ArrowError;
+use std::fmt;
+use thiserror::Error;
+
+/// Errors that can occur during GPU spatial operations.
+#[derive(Error, Debug)]
+pub enum GpuSpatialError {
+    Arrow(ArrowError),
+    Init(String),
+    PushBuild(String),
+    FinishBuild(String),
+    Probe(String),
+    Refine(String),
+    GpuNotAvailable,
+}
+
+impl From<ArrowError> for GpuSpatialError {
+    fn from(value: ArrowError) -> Self {
+        GpuSpatialError::Arrow(value)
+    }
+}
+
+impl fmt::Display for GpuSpatialError {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        match self {
+            GpuSpatialError::Arrow(error) => {
+                write!(f, "{error}")
+            }
+            GpuSpatialError::Init(errmsg) => {
+                write!(f, "Initialization failed: {}", errmsg)
+            }
+            GpuSpatialError::PushBuild(errmsg) => {
+                write!(f, "Push build failed: {}", errmsg)
+            }
+            GpuSpatialError::FinishBuild(errmsg) => {
+                write!(f, "Finish building failed: {}", errmsg)
+            }
+            GpuSpatialError::Probe(errmsg) => {
+                write!(f, "Probe failed: {}", errmsg)
+            }
+            GpuSpatialError::Refine(errmsg) => {
+                write!(f, "Refine failed: {}", errmsg)
+            }
+            GpuSpatialError::GpuNotAvailable => {
+                write!(f, "GPU is not available")
+            }
+        }
+    }
+}
diff --git a/c/sedona-libgpuspatial/src/lib.rs 
b/c/sedona-libgpuspatial/src/lib.rs
index 8d9fd61c..5dc05a60 100644
--- a/c/sedona-libgpuspatial/src/lib.rs
+++ b/c/sedona-libgpuspatial/src/lib.rs
@@ -15,6 +15,330 @@
 // specific language governing permissions and limitations
 // under the License.
 
-// Module declarations
+use arrow_schema::DataType;
+use geo_types::Rect;
+
+mod error;
 #[cfg(gpu_available)]
+mod libgpuspatial;
 mod libgpuspatial_glue_bindgen;
+mod options;
+mod predicate;
+
+pub use error::GpuSpatialError;
+pub use options::GpuSpatialOptions;
+pub use predicate::GpuSpatialRelationPredicate;
+pub use sys::{GpuSpatialIndex, GpuSpatialRefiner};
+
+#[cfg(gpu_available)]
+mod sys {
+    use super::libgpuspatial;
+    use super::*;
+    use libgpuspatial::GpuSpatialRuntimeWrapper;
+    use std::sync::{Arc, Mutex};
+
+    pub type Result<T> = std::result::Result<T, GpuSpatialError>;
+
+    static GLOBAL_GPUSPATIAL_RUNTIME: 
Mutex<Option<Arc<GpuSpatialRuntimeWrapper>>> =
+        Mutex::new(None);
+    /// Handles initialization of the GPU runtime.
+    pub struct SpatialContext {
+        runtime: Arc<GpuSpatialRuntimeWrapper>,
+    }
+
+    impl SpatialContext {
+        pub fn try_new(options: &GpuSpatialOptions) -> Result<Self> {
+            // Lock the mutex globally
+            let mut guard = GLOBAL_GPUSPATIAL_RUNTIME
+                .lock()
+                .map_err(|_| GpuSpatialError::Init("Global mutex 
poisoned".into()))?;
+
+            // Check if it already exists
+            if let Some(existing_runtime) = guard.as_ref() {
+                if existing_runtime.device_id != options.device_id {
+                    return Err(GpuSpatialError::Init(format!(
+                        "Runtime conflict: Initialized on Device {}, requested 
Device {}.",
+                        existing_runtime.device_id, options.device_id
+                    )));
+                }
+                // Return the existing one
+                return Ok(Self {
+                    runtime: existing_runtime.clone(),
+                });
+            }
+
+            let out_path = std::path::PathBuf::from(env!("OUT_DIR"));
+            let ptx_root = out_path.join("share/gpuspatial/shaders");
+            let ptx_root_str = ptx_root
+                .to_str()
+                .ok_or_else(|| GpuSpatialError::Init("Invalid PTX 
path".to_string()))?;
+
+            let wrapper = libgpuspatial::GpuSpatialRuntimeWrapper::try_new(
+                options.device_id,
+                ptx_root_str,
+                options.cuda_use_memory_pool,
+                options.cuda_memory_pool_init_percent,
+            )?;
+
+            let arc_wrapper = Arc::new(wrapper);
+
+            *guard = Some(arc_wrapper.clone());
+
+            Ok(Self {
+                runtime: arc_wrapper,
+            })
+        }
+    }
+
+    /// A GPU-accelerated spatial index for 2D rectangles in FP32.
+    /// Once built, the index is immutable and can be safely shared across 
threads for read-only probe operations.
+    pub struct GpuSpatialIndex {
+        inner: libgpuspatial::FloatIndex2D,
+    }
+
+    impl GpuSpatialIndex {
+        /// Creates a new GPU spatial index with the specified options.
+        /// This initializes the GPU runtime if it hasn't been initialized yet.
+        pub fn try_new(options: &GpuSpatialOptions) -> Result<Self> {
+            let ctx = SpatialContext::try_new(options)?;
+            let inner = libgpuspatial::FloatIndex2D::try_new(ctx.runtime, 
options.concurrency)?;
+            Ok(Self { inner })
+        }
+
+        /// Clears any previously inserted data from the builder, allowing it 
to be reused for building a new index.
+        pub fn clear(&mut self) {
+            self.inner.clear()
+        }
+
+        /// Inserts a batch of bounding boxes into the index.
+        /// Each rectangle is represented as a `Rect<f32>` with minimum and 
maximum x and y coordinates.
+        /// This method accumulates these rectangles until `finish_building` 
is called to finalize the index.
+        /// The method can be called multiple times to insert data in batches 
before finalizing.
+        pub fn push_build(&mut self, rects: &[Rect<f32>]) -> Result<()> {
+            // Re-interpreting Rect<f32> as flat f32 array (xmin, ymin, xmax, 
ymax)
+            let raw_ptr = rects.as_ptr() as *const f32;
+            self.inner.push_build(raw_ptr, rects.len() as u32)
+        }
+
+        /// Finalizes the building process and returns an immutable spatial 
index that can be probed.
+        pub fn finish_building(&mut self) -> Result<()> {
+            self.inner.finish_building()
+        }
+
+        /// Probes the spatial index with a batch of rectangles and returns 
pairs of matching indices from the build and probe sets.
+        pub fn probe(&self, rects: &[Rect<f32>]) -> Result<(Vec<u32>, 
Vec<u32>)> {
+            let raw_ptr = rects.as_ptr() as *const f32;
+            self.inner.probe(raw_ptr, rects.len() as u32)
+        }
+    }
+
+    /// A GPU-accelerated spatial refiner that can perform various spatial 
relation tests (e.g., Intersects, Contains) between two sets of geometries.
+    pub struct GpuSpatialRefiner {
+        inner: libgpuspatial::Refiner,
+    }
+
+    impl GpuSpatialRefiner {
+        /// Creates a new GPU spatial refiner with the specified options.
+        /// This initializes the GPU runtime if it hasn't been initialized yet.
+        pub fn try_new(options: &GpuSpatialOptions) -> Result<Self> {
+            let ctx = SpatialContext::try_new(options)?;
+            let inner = libgpuspatial::Refiner::try_new(
+                ctx.runtime,
+                options.concurrency,
+                options.compress_bvh,
+                options.pipeline_batches,
+            )?;
+            Ok(Self { inner })
+        }
+
+        /// Initializes the schema for the refiner based on the data types of 
the build and probe geometries.
+        /// This allows the refiner to understand how to interpret the 
geometry data for both sets.
+        pub fn init_schema(&mut self, build: &DataType, probe: &DataType) -> 
Result<()> {
+            self.inner.init_schema(build, probe)
+        }
+
+        /// Clears any previously inserted data from the refiner, allowing it 
to be reused for building a new set of geometries.
+        pub fn clear(&mut self) {
+            self.inner.clear()
+        }
+
+        /// Inserts a batch of geometries into the refiner for the build side.
+        /// The geometries are provided as an Arrow array reference, and the 
refiner will process them according to the initialized schema.
+        /// This method accumulates these geometries until `finish_building` 
is called to finalize the refiner.
+        pub fn push_build(&mut self, array: &arrow_array::ArrayRef) -> 
Result<()> {
+            self.inner.push_build(array)
+        }
+
+        /// Finalizes the building process and prepares the refiner for 
refinement operations.
+        /// After this call, the refiner is ready to perform spatial relation 
tests against probe geometries.
+        pub fn finish_building(&mut self) -> Result<()> {
+            self.inner.finish_building()
+        }
+
+        /// Refines the candidate pairs of geometries based on the specified 
spatial relation predicate.
+        /// The probe geometries are provided as an Arrow array reference,
+        /// and the method updates the provided vectors of build and probe 
indices to
+        /// include only those pairs that satisfy the spatial relation 
predicate.
+        pub fn refine(
+            &self,
+            probe: &arrow_array::ArrayRef,
+            pred: GpuSpatialRelationPredicate,
+            build_indices: &mut Vec<u32>,
+            probe_indices: &mut Vec<u32>,
+        ) -> Result<()> {
+            self.inner.refine(probe, pred, build_indices, probe_indices)
+        }
+    }
+}
+
+#[cfg(not(gpu_available))]
+mod sys {
+    use super::*;
+    pub type Result<T> = std::result::Result<T, crate::error::GpuSpatialError>;
+
+    pub struct GpuSpatialIndex;
+    pub struct GpuSpatialRefiner;
+
+    impl GpuSpatialIndex {
+        pub fn try_new(_opts: &GpuSpatialOptions) -> Result<Self> {
+            Err(GpuSpatialError::GpuNotAvailable)
+        }
+        pub fn clear(&mut self) {}
+        pub fn push_build(&mut self, _r: &[Rect<f32>]) -> Result<()> {
+            Err(GpuSpatialError::GpuNotAvailable)
+        }
+        pub fn finish_building(self) -> Result<GpuSpatialIndex> {
+            Err(GpuSpatialError::GpuNotAvailable)
+        }
+        pub fn probe(&self, _r: &[Rect<f32>]) -> Result<(Vec<u32>, Vec<u32>)> {
+            Err(GpuSpatialError::GpuNotAvailable)
+        }
+    }
+
+    impl GpuSpatialRefiner {
+        pub fn try_new(_opts: &GpuSpatialOptions) -> Result<Self> {
+            Err(GpuSpatialError::GpuNotAvailable)
+        }
+        pub fn init_schema(&mut self, _b: &DataType, _p: &DataType) -> 
Result<()> {
+            Err(GpuSpatialError::GpuNotAvailable)
+        }
+        pub fn clear(&mut self) {}
+        pub fn push_build(&mut self, _arr: &arrow_array::ArrayRef) -> 
Result<()> {
+            Err(GpuSpatialError::GpuNotAvailable)
+        }
+        pub fn finish_building(&mut self) -> Result<()> {
+            Err(GpuSpatialError::GpuNotAvailable)
+        }
+        pub fn refine(
+            &self,
+            _p: &arrow_array::ArrayRef,
+            _pr: GpuSpatialRelationPredicate,
+            _build_indices: &mut Vec<u32>,
+            _probe_indices: &mut Vec<u32>,
+        ) -> Result<()> {
+            Err(GpuSpatialError::GpuNotAvailable)
+        }
+    }
+}
+
+#[cfg(gpu_available)]
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use geo::{BoundingRect, Point, Polygon};
+    use sedona_schema::datatypes::WKB_GEOMETRY;
+    use sedona_testing::create::create_array_storage;
+    use wkt::TryFromWkt;
+
+    #[test]
+    fn test_spatial_index() {
+        let options = GpuSpatialOptions {
+            concurrency: 1,
+            device_id: 0,
+            compress_bvh: false,
+            pipeline_batches: 1,
+            cuda_use_memory_pool: true,
+            cuda_memory_pool_init_percent: 10,
+        };
+
+        // 1. Create Builder
+        let mut index = GpuSpatialIndex::try_new(&options).expect("Failed to 
create builder");
+
+        let polygon_values = &[
+            Some("POLYGON ((30 10, 40 40, 20 40, 10 20, 30 10))"),
+            Some("POLYGON ((35 10, 45 45, 15 40, 10 20, 35 10), (20 30, 35 35, 
30 20, 20 30))"),
+        ];
+        let rects: Vec<Rect<f32>> = polygon_values
+            .iter()
+            .map(|w| {
+                Polygon::try_from_wkt_str(w.unwrap())
+                    .unwrap()
+                    .bounding_rect()
+                    .unwrap()
+            })
+            .collect();
+
+        // 2. Insert Data
+        index.push_build(&rects).expect("Failed to insert");
+
+        // 3. Finish (Consumes Builder -> Returns Index)
+        index.finish_building().expect("Failed to finish building");
+
+        // 4. Probe (Index is immutable and safe)
+        let point_values = &[Some("POINT (30 20)")];
+        let points: Vec<Rect<f32>> = point_values
+            .iter()
+            .map(|w| 
Point::try_from_wkt_str(w.unwrap()).unwrap().bounding_rect())
+            .collect();
+
+        let (build_idx, probe_idx) = index.probe(&points).unwrap();
+
+        assert!(!build_idx.is_empty());
+        assert_eq!(build_idx.len(), probe_idx.len());
+    }
+
+    #[test]
+    fn test_spatial_refiner() {
+        let options = GpuSpatialOptions {
+            concurrency: 1,
+            device_id: 0,
+            compress_bvh: false,
+            pipeline_batches: 1,
+            cuda_use_memory_pool: true,
+            cuda_memory_pool_init_percent: 10,
+        };
+
+        // 1. Create Refiner Builder
+        let mut refiner =
+            GpuSpatialRefiner::try_new(&options).expect("Failed to create 
refiner builder");
+
+        let polygon_values = &[Some("POLYGON ((30 10, 40 40, 20 40, 10 20, 30 
10))")];
+        let polygons = create_array_storage(polygon_values, &WKB_GEOMETRY);
+
+        let point_values = &[Some("POINT (30 20)")];
+        let points = create_array_storage(point_values, &WKB_GEOMETRY);
+
+        // 2. Build Refiner
+        refiner
+            .init_schema(polygons.data_type(), points.data_type())
+            .unwrap();
+
+        refiner.push_build(&polygons).unwrap();
+
+        // 3. Finish (Consumes Builder -> Returns Refiner)
+        refiner.finish_building().expect("Failed to finish refiner");
+
+        // 4. Use Refiner
+        let mut build_idx = vec![0];
+        let mut probe_idx = vec![0];
+
+        refiner
+            .refine(
+                &points,
+                GpuSpatialRelationPredicate::Intersects,
+                &mut build_idx,
+                &mut probe_idx,
+            )
+            .unwrap();
+    }
+}
diff --git a/c/sedona-libgpuspatial/src/libgpuspatial.rs 
b/c/sedona-libgpuspatial/src/libgpuspatial.rs
new file mode 100644
index 00000000..8a53b61d
--- /dev/null
+++ b/c/sedona-libgpuspatial/src/libgpuspatial.rs
@@ -0,0 +1,578 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+use crate::error::GpuSpatialError;
+#[cfg(gpu_available)]
+use crate::libgpuspatial_glue_bindgen::*;
+use crate::predicate::GpuSpatialRelationPredicate;
+use arrow_array::{Array, ArrayRef};
+use arrow_schema::ffi::FFI_ArrowSchema;
+use arrow_schema::DataType;
+use std::cell::UnsafeCell;
+use std::convert::TryFrom;
+use std::ffi::{c_void, CStr, CString};
+use std::os::raw::c_char;
+use std::sync::Arc;
+
+/// Public wrapper around the C `GpuSpatialRuntime` struct that manages its 
lifecycle and provides safe Rust methods to interact with it.
+pub struct GpuSpatialRuntimeWrapper {
+    runtime: UnsafeCell<GpuSpatialRuntime>,
+    /// Store which device the runtime is created on
+    pub device_id: i32,
+}
+
+unsafe impl Send for GpuSpatialRuntimeWrapper {}
+unsafe impl Sync for GpuSpatialRuntimeWrapper {}
+
+impl GpuSpatialRuntimeWrapper {
+    /// Creates a new `GpuSpatialRuntimeWrapper` instance by initializing the 
underlying C struct with the provided configuration. It returns a 
`GpuSpatialError` if initialization fails.
+    /// # Arguments
+    /// * `device_id` - The ID of the GPU device to use for the runtime
+    /// * `ptx_root` - The root directory where the PTX files are located
+    /// * `use_cuda_memory_pool` - Whether to use the CUDA memory pool for 
allocations
+    /// * `cuda_memory_pool_init_precent` - The initial percentage of the CUDA 
memory pool to use (0-100)
+    pub fn try_new(
+        device_id: i32,
+        ptx_root: &str,
+        use_cuda_memory_pool: bool,
+        cuda_memory_pool_init_precent: i32,
+    ) -> Result<GpuSpatialRuntimeWrapper, GpuSpatialError> {
+        let mut runtime = GpuSpatialRuntime {
+            init: None,
+            release: None,
+            get_last_error: None,
+            private_data: std::ptr::null_mut(),
+        };
+
+        unsafe {
+            GpuSpatialRuntimeCreate(&mut runtime);
+        }
+
+        if let Some(init_fn) = runtime.init {
+            let c_ptx_root = CString::new(ptx_root).map_err(|_| {
+                GpuSpatialError::Init("Failed to convert ptx_root to 
CString".into())
+            })?;
+
+            let mut config = GpuSpatialRuntimeConfig {
+                device_id,
+                ptx_root: c_ptx_root.as_ptr(),
+                use_cuda_memory_pool,
+                cuda_memory_pool_init_precent,
+            };
+
+            unsafe {
+                let get_last_error = runtime.get_last_error;
+                let runtime_ptr = &mut runtime as *mut GpuSpatialRuntime;
+
+                check_ffi_call(
+                    move || init_fn(runtime_ptr as *mut _, &mut config),
+                    get_last_error,
+                    runtime_ptr,
+                    GpuSpatialError::Init,
+                )?;
+            }
+        } else {
+            return Err(GpuSpatialError::Init("init function is 
None".to_string()));
+        }
+
+        Ok(GpuSpatialRuntimeWrapper {
+            runtime: UnsafeCell::new(runtime),
+            device_id,
+        })
+    }
+}
+
+impl Drop for GpuSpatialRuntimeWrapper {
+    fn drop(&mut self) {
+        let runtime = self.runtime.get_mut();
+        let release_fn = runtime.release.expect("release function is None");
+        unsafe {
+            release_fn(runtime as *mut _);
+        }
+    }
+}
+
+/// Internal wrapper that manages the lifecycle of the C `SedonaFloatIndex2D` 
struct.
+/// It is wrapped in an `Arc` by the public structs to ensure thread safety.
+struct FloatIndex2DWrapper {
+    index: SedonaFloatIndex2D,
+    // Keep a reference to the RT engine to ensure it lives as long as the 
index
+    _runtime: Arc<GpuSpatialRuntimeWrapper>,
+}
+
+impl Drop for FloatIndex2DWrapper {
+    fn drop(&mut self) {
+        let release_fn = self.index.release.expect("release function is None");
+        unsafe {
+            release_fn(&mut self.index as *mut _);
+        }
+    }
+}
+
+/// Public struct representing the 2D float spatial index. It provides safe 
Rust methods to interact with the underlying C implementation.
+pub struct FloatIndex2D {
+    inner: FloatIndex2DWrapper,
+}
+
+unsafe impl Send for FloatIndex2D {}
+unsafe impl Sync for FloatIndex2D {}
+
+impl FloatIndex2D {
+    /// Creates a new `FloatIndex2D` instance by initializing the underlying C 
struct with the provided configuration. It returns a `GpuSpatialError` if 
initialization fails.
+    /// # Arguments
+    /// * `runtime` - An `Arc` to the `GpuSpatialRuntimeWrapper` that the 
index will use for GPU operations
+    /// * `concurrency` - The maximum level of concurrency allowed to use for 
probing the index
+    pub fn try_new(
+        runtime: Arc<GpuSpatialRuntimeWrapper>,
+        concurrency: u32,
+    ) -> Result<Self, GpuSpatialError> {
+        let mut index = SedonaFloatIndex2D {
+            clear: None,
+            create_context: None,
+            destroy_context: None,
+            push_build: None,
+            finish_building: None,
+            probe: None,
+            get_last_error: None,
+            context_get_last_error: None,
+            release: None,
+            private_data: std::ptr::null_mut(),
+        };
+
+        let config = GpuSpatialIndexConfig {
+            runtime: runtime.runtime.get(),
+            concurrency,
+        };
+
+        unsafe {
+            if GpuSpatialIndexFloat2DCreate(&mut index, &config) != 0 {
+                return Err(GpuSpatialError::Init("Index Create 
failed".into()));
+            }
+        }
+
+        Ok(Self {
+            inner: FloatIndex2DWrapper {
+                index,
+                _runtime: runtime.clone(),
+            },
+        })
+    }
+
+    /// Clears the internal state of the index.
+    pub fn clear(&mut self) {
+        if let Some(clear_fn) = self.inner.index.clear {
+            unsafe {
+                clear_fn(&mut self.inner.index as *mut _);
+            }
+        }
+    }
+
+    /// Pushes a batch of rectangles to the index for building.
+    /// # Arguments
+    /// * `buf` - A pointer to a buffer containing the rectangle data in the 
format [x_min, y_min, x_max, y_max] for each rectangle
+    /// * `n_rects` - The number of rectangles in the buffer
+    /// # Returns
+    /// * `Ok(())` if the push operation is successful
+    /// * `Err(GpuSpatialError)` if an error occurs during the push operation, 
with the error message retrieved from the C struct
+    pub fn push_build(&mut self, buf: *const f32, n_rects: u32) -> Result<(), 
GpuSpatialError> {
+        let push_fn =
+            self.inner.index.push_build.ok_or_else(|| {
+                GpuSpatialError::PushBuild("push_build function is 
None".to_string())
+            })?;
+        let get_last_error = self.inner.index.get_last_error;
+        let index_ptr = &mut self.inner.index as *mut SedonaFloatIndex2D;
+
+        unsafe {
+            check_ffi_call(
+                move || push_fn(index_ptr, buf, n_rects),
+                get_last_error,
+                index_ptr,
+                GpuSpatialError::PushBuild,
+            )
+        }
+    }
+
+    /// Finalizes the building process of the index.
+    /// # Returns
+    /// * `Ok(())` if the finish building operation is successful
+    /// * `Err(GpuSpatialError)` if an error occurs during the finish building 
operation, with the error message retrieved from the C struct
+    pub fn finish_building(&mut self) -> Result<(), GpuSpatialError> {
+        let finish_fn = self
+            .inner
+            .index
+            .finish_building
+            .ok_or_else(|| GpuSpatialError::FinishBuild("finish_building 
missing".into()))?;
+        let get_last_error = self.inner.index.get_last_error;
+        let index_ptr = &mut self.inner.index as *mut SedonaFloatIndex2D;
+
+        unsafe {
+            check_ffi_call(
+                move || finish_fn(&mut self.inner.index),
+                get_last_error,
+                index_ptr,
+                GpuSpatialError::FinishBuild,
+            )
+        }
+    }
+
+    /// Probes the index with a batch of rectangles and retrieves the 
candidate pairs.
+    /// # Arguments
+    /// * `buf` - A pointer to a buffer containing the rectangle data in the 
format [x_min, y_min, x_max, y_max] for each rectangle
+    /// * `n_rects` - The number of rectangles in the buffer
+    /// # Returns
+    /// * `Ok((Vec<u32>, Vec<u32>))` containing the build and probe indices of 
the candidate pairs if the probe operation is successful
+    /// * `Err(GpuSpatialError)` if an error occurs during the probe 
operation, with the error message retrieved from the C struct or from the 
callback wrapper
+    pub fn probe(
+        &self,
+        buf: *const f32,
+        n_rects: u32,
+    ) -> Result<(Vec<u32>, Vec<u32>), GpuSpatialError> {
+        let probe_fn = self
+            .inner
+            .index
+            .probe
+            .ok_or_else(|| GpuSpatialError::Probe("probe function is 
None".into()))?;
+        let create_context_fn = self.inner.index.create_context;
+        let destroy_context_fn = self.inner.index.destroy_context;
+        let context_err_fn = self.inner.index.context_get_last_error;
+        let index_ptr = &self.inner.index as *const _ as *mut 
SedonaFloatIndex2D;
+
+        let mut ctx = SedonaSpatialIndexContext {
+            private_data: std::ptr::null_mut(),
+        };
+        let mut state = ProbeState {
+            results: (Vec::new(), Vec::new()),
+            error: None,
+        };
+
+        unsafe {
+            if let Some(create_ctx) = create_context_fn {
+                create_ctx(&mut ctx);
+            }
+
+            let status = probe_fn(
+                index_ptr,
+                &mut ctx,
+                buf,
+                n_rects,
+                Some(probe_callback_wrapper),
+                &mut state as *mut _ as *mut c_void,
+            );
+
+            if status != 0 {
+                // IMPROVEMENT: Check Rust error first!
+                // If the callback returned -1, 'state.error' has the real 
reason (the panic).
+                if let Some(callback_error) = state.error {
+                    if let Some(destroy_ctx) = destroy_context_fn {
+                        destroy_ctx(&mut ctx);
+                    }
+                    return Err(callback_error);
+                }
+
+                // If no Rust error, it was a genuine C-side error
+                let error_string = if let Some(get_ctx_err) = context_err_fn {
+                    CStr::from_ptr(get_ctx_err(&mut ctx))
+                        .to_string_lossy()
+                        .into_owned()
+                } else {
+                    "Unknown context error during probe".to_string()
+                };
+
+                if let Some(destroy_ctx) = destroy_context_fn {
+                    destroy_ctx(&mut ctx);
+                }
+                return Err(GpuSpatialError::Probe(error_string));
+            }
+
+            // Cleanup on success
+            if let Some(destroy_ctx) = destroy_context_fn {
+                destroy_ctx(&mut ctx);
+            }
+        }
+
+        Ok(state.results)
+    }
+}
+
+/// Internal wrapper that manages the lifecycle of the C 
`SedonaSpatialRefiner` struct.
+struct RefinerWrapper {
+    refiner: SedonaSpatialRefiner,
+    _runtime: Arc<GpuSpatialRuntimeWrapper>,
+}
+
+impl Drop for RefinerWrapper {
+    fn drop(&mut self) {
+        let release_fn = self.refiner.release.expect("release function is 
None");
+        unsafe {
+            release_fn(&mut self.refiner as *mut _);
+        }
+    }
+}
+
+/// Public struct representing the spatial refiner. It provides safe Rust 
methods to interact with the underlying C implementation.
+pub struct Refiner {
+    inner: RefinerWrapper,
+}
+
+unsafe impl Send for Refiner {}
+unsafe impl Sync for Refiner {}
+
+impl Refiner {
+    /// Creates a new `Refiner` instance by initializing the underlying C 
struct with the provided configuration. It returns a `GpuSpatialError` if 
initialization fails.
+    /// # Arguments
+    /// * `runtime` - An `Arc` to the `GpuSpatialRuntimeWrapper` that the 
refiner will use for GPU operations
+    /// * `concurrency` - The maximum level of concurrency allowed to use for 
refining candidate pairs
+    /// * `compress_bvh` - Whether to compress the BVH used internally by the 
refiner to save memory at the cost of potentially slower refinement times
+    /// * `pipeline_batches` - The number of batches to use for pipelining the 
refinement process, which can improve performance by overlapping GPU 
computation with WKB parsing. A value of 1 means no pipelining.
+    pub fn try_new(
+        runtime: Arc<GpuSpatialRuntimeWrapper>,
+        concurrency: u32,
+        compress_bvh: bool,
+        pipeline_batches: u32,
+    ) -> Result<Self, GpuSpatialError> {
+        let mut refiner = SedonaSpatialRefiner {
+            clear: None,
+            init_schema: None,
+            push_build: None,
+            finish_building: None,
+            refine: None,
+            get_last_error: None,
+            release: None,
+            private_data: std::ptr::null_mut(),
+        };
+
+        let config = GpuSpatialRefinerConfig {
+            runtime: runtime.runtime.get(),
+            concurrency,
+            compress_bvh,
+            pipeline_batches,
+        };
+
+        unsafe {
+            GpuSpatialRefinerCreate(&mut refiner, &config);
+        }
+
+        Ok(Self {
+            inner: RefinerWrapper {
+                refiner,
+                _runtime: runtime.clone(),
+            },
+        })
+    }
+
+    /// Initializes the schema for the refiner using the provided build and 
probe data types.
+    /// It converts the Arrow `DataType` to the C-compatible `FFI_ArrowSchema` 
and calls the underlying C function.
+    /// If initialization fails, it retrieves the error message from the C 
struct and returns a `GpuSpatialError`.
+    pub fn init_schema(
+        &mut self,
+        build_dt: &DataType,
+        probe_dt: &DataType,
+    ) -> Result<(), GpuSpatialError> {
+        let build_ffi = FFI_ArrowSchema::try_from(build_dt)?;
+        let probe_ffi = FFI_ArrowSchema::try_from(probe_dt)?;
+        let init_fn = self.inner.refiner.init_schema.unwrap();
+        let get_last_error = self.inner.refiner.get_last_error;
+        let refiner_ptr = &mut self.inner.refiner as *mut SedonaSpatialRefiner;
+
+        unsafe {
+            check_ffi_call(
+                || {
+                    init_fn(
+                        &mut self.inner.refiner,
+                        &build_ffi as *const _ as *const _,
+                        &probe_ffi as *const _ as *const _,
+                    )
+                },
+                get_last_error,
+                refiner_ptr,
+                GpuSpatialError::Init,
+            )
+        }
+    }
+
+    /// Pushes a batch of data to the refiner for building.
+    /// It converts the provided Arrow array to its FFI representation and 
calls the underlying C function.
+    /// If the push operation fails, it retrieves the error message from the C 
struct and returns a `GpuSpatialError`.
+    pub fn push_build(&mut self, array: &ArrayRef) -> Result<(), 
GpuSpatialError> {
+        let (ffi_array, _) = arrow_array::ffi::to_ffi(&array.to_data())?;
+        let push_fn = self.inner.refiner.push_build.unwrap();
+        let get_last_error = self.inner.refiner.get_last_error;
+        let refiner_ptr = &mut self.inner.refiner as *mut SedonaSpatialRefiner;
+
+        unsafe {
+            check_ffi_call(
+                || push_fn(&mut self.inner.refiner, &ffi_array as *const _ as 
*const _),
+                get_last_error,
+                refiner_ptr,
+                GpuSpatialError::PushBuild,
+            )
+        }
+    }
+
+    /// Clears the internal state of the refiner.
+    pub fn clear(&mut self) {
+        if let Some(clear_fn) = self.inner.refiner.clear {
+            unsafe {
+                clear_fn(&mut self.inner.refiner as *mut _);
+            }
+        }
+    }
+
+    /// Finalizes the building process of the refiner.
+    pub fn finish_building(&mut self) -> Result<(), GpuSpatialError> {
+        let finish_fn = self.inner.refiner.finish_building.unwrap();
+        let get_last_error = self.inner.refiner.get_last_error;
+        let refiner_ptr = &mut self.inner.refiner as *mut SedonaSpatialRefiner;
+
+        unsafe {
+            check_ffi_call(
+                || finish_fn(&mut self.inner.refiner),
+                get_last_error,
+                refiner_ptr,
+                GpuSpatialError::FinishBuild,
+            )
+        }
+    }
+
+    /// Refines the candidate pairs based on the provided predicate.
+    /// # Arguments
+    /// * `array` - The probe array containing the geometries to be refined.
+    /// * `predicate` - The spatial relation predicate to apply for refinement.
+    /// * `build_indices` - A mutable vector of build indices corresponding to 
the candidate pairs
+    /// * `probe_indices` - A mutable vector of probe indices corresponding to 
the candidate pairs
+    /// # Returns
+    /// * `Ok(())` if the refinement is successful, with `build_indices` and 
`probe_indices` updated to contain only the pairs that satisfy the predicate.
+    /// * `Err(GpuSpatialError)` if an error occurs during refinement, with 
the error message retrieved from the C struct.
+    pub fn refine(
+        &self,
+        array: &ArrayRef,
+        predicate: GpuSpatialRelationPredicate,
+        build_indices: &mut Vec<u32>,
+        probe_indices: &mut Vec<u32>,
+    ) -> Result<(), GpuSpatialError> {
+        let (ffi_array, _) = arrow_array::ffi::to_ffi(&array.to_data())?;
+        let refine_fn = self.inner.refiner.refine.unwrap();
+        let mut new_len: u32 = 0;
+
+        unsafe {
+            check_ffi_call(
+                || {
+                    refine_fn(
+                        &self.inner.refiner as *const _ as *mut _,
+                        &ffi_array as *const _ as *mut _,
+                        predicate.as_c_uint(),
+                        build_indices.as_mut_ptr(),
+                        probe_indices.as_mut_ptr(),
+                        build_indices.len() as u32,
+                        &mut new_len,
+                    )
+                },
+                self.inner.refiner.get_last_error,
+                &self.inner.refiner as *const _ as *mut _,
+                GpuSpatialError::Refine,
+            )?;
+        }
+        build_indices.truncate(new_len as usize);
+        probe_indices.truncate(new_len as usize);
+        Ok(())
+    }
+}
+
+// ----------------------------------------------------------------------
+// Helper Functions
+// ----------------------------------------------------------------------
+
+// Define the exact signature of the C error-getting function
+type ErrorFn<T> = unsafe extern "C" fn(*mut T) -> *const c_char;
+struct ProbeState {
+    results: (Vec<u32>, Vec<u32>),
+    error: Option<GpuSpatialError>,
+}
+/// Helper to handle the common pattern of calling a C function returning an 
int status,
+/// checking if it failed, and retrieving the error message if so.
+unsafe fn check_ffi_call<T, F, ErrMap>(
+    call_fn: F,
+    get_error_fn: Option<ErrorFn<T>>,
+    obj_ptr: *mut T,
+    err_mapper: ErrMap,
+) -> Result<(), GpuSpatialError>
+where
+    F: FnOnce() -> i32,
+    ErrMap: FnOnce(String) -> GpuSpatialError,
+{
+    if call_fn() != 0 {
+        let error_string = if let Some(get_err) = get_error_fn {
+            let err_ptr = get_err(obj_ptr);
+            if !err_ptr.is_null() {
+                CStr::from_ptr(err_ptr).to_string_lossy().into_owned()
+            } else {
+                "Unknown error (null error message)".to_string()
+            }
+        } else {
+            "Unknown error (get_last_error not available)".to_string()
+        };
+
+        return Err(err_mapper(error_string));
+    }
+    Ok(())
+}
+
+/// Wrapper for the probe callback that C will call.
+/// It safely converts the raw pointers to Rust slices and updates the 
`ProbeState` with the results.
+/// It also catches any panics that occur within the callback and stores the 
error message in the `ProbeState`,
+/// returning an error code to C to indicate that the callback failed.
+unsafe extern "C" fn probe_callback_wrapper(
+    build_indices: *const u32,
+    probe_indices: *const u32,
+    length: u32,
+    user_data: *mut c_void,
+) -> i32 {
+    let state = &mut *(user_data as *mut ProbeState);
+
+    // 1. Short-circuit: If previous error exists, tell C to stop immediately.
+    if state.error.is_some() {
+        return -1;
+    }
+
+    let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
+        if length > 0 {
+            let build_slice = std::slice::from_raw_parts(build_indices, length 
as usize);
+            let probe_slice = std::slice::from_raw_parts(probe_indices, length 
as usize);
+
+            state.results.0.extend_from_slice(build_slice);
+            state.results.1.extend_from_slice(probe_slice);
+        }
+    }));
+
+    match result {
+        Ok(_) => 0, // Success code
+        Err(payload) => {
+            // Extract panic message
+            let msg = if let Some(s) = payload.downcast_ref::<&str>() {
+                format!("Panic in callback: {}", s)
+            } else if let Some(s) = payload.downcast_ref::<String>() {
+                format!("Panic in callback: {}", s)
+            } else {
+                "Unknown panic in callback".to_string()
+            };
+
+            // Set state and return error code to C
+            state.error = Some(GpuSpatialError::Probe(msg));
+            -1
+        }
+    }
+}
diff --git a/c/sedona-libgpuspatial/src/libgpuspatial_glue_bindgen.rs 
b/c/sedona-libgpuspatial/src/libgpuspatial_glue_bindgen.rs
index ce5f4aad..8eac3563 100644
--- a/c/sedona-libgpuspatial/src/libgpuspatial_glue_bindgen.rs
+++ b/c/sedona-libgpuspatial/src/libgpuspatial_glue_bindgen.rs
@@ -19,5 +19,11 @@
 #![allow(non_camel_case_types)]
 #![allow(non_snake_case)]
 #![allow(dead_code)]
+#![allow(clippy::type_complexity)]
 
-include!(concat!(env!("OUT_DIR"), "/bindings.rs"));
+mod sys {
+    include!(concat!(env!("OUT_DIR"), "/bindings.rs"));
+}
+
+#[cfg(gpu_available)]
+pub use sys::*;
diff --git a/c/sedona-libgpuspatial/src/lib.rs 
b/c/sedona-libgpuspatial/src/options.rs
similarity index 51%
copy from c/sedona-libgpuspatial/src/lib.rs
copy to c/sedona-libgpuspatial/src/options.rs
index 8d9fd61c..f8c5d715 100644
--- a/c/sedona-libgpuspatial/src/lib.rs
+++ b/c/sedona-libgpuspatial/src/options.rs
@@ -15,6 +15,18 @@
 // specific language governing permissions and limitations
 // under the License.
 
-// Module declarations
-#[cfg(gpu_available)]
-mod libgpuspatial_glue_bindgen;
+/// Options for GPU-accelerated index and refiner.
+pub struct GpuSpatialOptions {
+    /// Whether to use CUDA memory pool for allocations
+    pub cuda_use_memory_pool: bool,
+    /// Ratio of initial memory pool size to total GPU memory, between 0 and 
100
+    pub cuda_memory_pool_init_percent: i32,
+    /// How many threads will concurrently use the library
+    pub concurrency: u32,
+    /// The device id to use
+    pub device_id: i32,
+    /// Whether to build a compressed BVH, which can reduce memory usage, but 
may increase build time
+    pub compress_bvh: bool,
+    /// The number of batches for pipelined refinement that overlaps the WKB 
loading and refinement. Setting 1 effectively disables pipelining.
+    pub pipeline_batches: u32,
+}
diff --git a/c/sedona-libgpuspatial/src/predicate.rs 
b/c/sedona-libgpuspatial/src/predicate.rs
new file mode 100644
index 00000000..1287b9b2
--- /dev/null
+++ b/c/sedona-libgpuspatial/src/predicate.rs
@@ -0,0 +1,61 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+#[cfg(gpu_available)]
+use std::os::raw::c_uint;
+
+#[derive(Debug, PartialEq, Copy, Clone)]
+pub enum GpuSpatialRelationPredicate {
+    Equals,
+    Disjoint,
+    Touches,
+    Contains,
+    Covers,
+    Intersects,
+    Within,
+    CoveredBy,
+}
+
+#[cfg(gpu_available)] // not used if the GPU feature is disabled
+impl GpuSpatialRelationPredicate {
+    /// Internal helper to convert the Rust enum to the C-compatible integer.
+    pub(crate) fn as_c_uint(self) -> c_uint {
+        match self {
+            Self::Equals => 0,
+            Self::Disjoint => 1,
+            Self::Touches => 2,
+            Self::Contains => 3,
+            Self::Covers => 4,
+            Self::Intersects => 5,
+            Self::Within => 6,
+            Self::CoveredBy => 7,
+        }
+    }
+}
+impl std::fmt::Display for GpuSpatialRelationPredicate {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        match self {
+            GpuSpatialRelationPredicate::Equals => write!(f, "equals"),
+            GpuSpatialRelationPredicate::Disjoint => write!(f, "disjoint"),
+            GpuSpatialRelationPredicate::Touches => write!(f, "touches"),
+            GpuSpatialRelationPredicate::Contains => write!(f, "contains"),
+            GpuSpatialRelationPredicate::Covers => write!(f, "covers"),
+            GpuSpatialRelationPredicate::Intersects => write!(f, "intersects"),
+            GpuSpatialRelationPredicate::Within => write!(f, "within"),
+            GpuSpatialRelationPredicate::CoveredBy => write!(f, "coveredby"),
+        }
+    }
+}

Reply via email to