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

kontinuation 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 3d547639 feat(c/sedona-gdal): add crate with dynamically loaded GDAL 
bindings (#681)
3d547639 is described below

commit 3d5476399667fcac7d62ebac90c337977b3017d0
Author: Kristin Cowalcijk <[email protected]>
AuthorDate: Mon Mar 9 23:25:55 2026 +0800

    feat(c/sedona-gdal): add crate with dynamically loaded GDAL bindings (#681)
    
    ## Summary
    
    - Add the `sedona-gdal` crate providing runtime-loaded GDAL FFI bindings 
via `libloading`, following the same pattern as `sedona-proj`.
    - Contains the `SedonaGdalApi` function-pointer struct, dynamic symbol 
loading (`dyn_load`), the `GdalApi` handle with `call_gdal_api!` macro, error 
types, and global API registration.
    - Integrates into the workspace (`Cargo.toml`, `rust/sedona/Cargo.toml`) 
and CI (`.github/workflows/rust.yml` adds `libgdal-dev`).
---
 .github/workflows/rust.yml            |   2 +-
 Cargo.lock                            |  21 ++
 Cargo.toml                            |   2 +
 c/sedona-gdal/Cargo.toml              |  42 +++
 c/sedona-gdal/src/dyn_load.rs         | 315 ++++++++++++++++++++
 c/sedona-gdal/src/errors.rs           |  42 +++
 c/sedona-gdal/src/gdal_api.rs         | 116 ++++++++
 c/sedona-gdal/src/gdal_dyn_bindgen.rs | 533 ++++++++++++++++++++++++++++++++++
 c/sedona-gdal/src/global.rs           | 313 ++++++++++++++++++++
 c/sedona-gdal/src/lib.rs              |  27 ++
 docs/contributors-guide.md            |  12 +-
 rust/sedona/Cargo.toml                |   2 +
 12 files changed, 1422 insertions(+), 5 deletions(-)

diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml
index 03b6b5f7..d79d7a23 100644
--- a/.github/workflows/rust.yml
+++ b/.github/workflows/rust.yml
@@ -128,7 +128,7 @@ jobs:
       - name: Install dependencies
         shell: bash
         run: |
-          sudo apt-get update && sudo apt-get install -y libgeos-dev
+          sudo apt-get update && sudo apt-get install -y libgeos-dev 
libgdal-dev
 
       - name: Check
         if: matrix.name == 'check'
diff --git a/Cargo.lock b/Cargo.lock
index 6cbb4d3e..67c17af7 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2736,6 +2736,16 @@ dependencies = [
  "slab",
 ]
 
+[[package]]
+name = "gdal-sys"
+version = "0.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index";
+checksum = "cceef1cc08a1f031c5717cb645bb361a3114470cc142cc96bc5e62b79695632e"
+dependencies = [
+ "pkg-config",
+ "semver",
+]
+
 [[package]]
 name = "generational-arena"
 version = "0.2.9"
@@ -5127,6 +5137,7 @@ dependencies = [
  "sedona-datasource",
  "sedona-expr",
  "sedona-functions",
+ "sedona-gdal",
  "sedona-geo",
  "sedona-geometry",
  "sedona-geoparquet",
@@ -5281,6 +5292,16 @@ dependencies = [
  "wkt 0.14.0",
 ]
 
+[[package]]
+name = "sedona-gdal"
+version = "0.3.0"
+dependencies = [
+ "gdal-sys",
+ "libloading 0.9.0",
+ "sedona-testing",
+ "thiserror 2.0.17",
+]
+
 [[package]]
 name = "sedona-geo"
 version = "0.3.0"
diff --git a/Cargo.toml b/Cargo.toml
index df7f8757..134275fd 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -20,6 +20,7 @@ members = [
     "c/sedona-geoarrow-c",
     "c/sedona-geos",
     "c/sedona-libgpuspatial",
+    "c/sedona-gdal",
     "c/sedona-proj",
     "c/sedona-s2geography",
     "c/sedona-tg",
@@ -150,6 +151,7 @@ sedona-testing = { version = "0.3.0", path = 
"rust/sedona-testing" }
 # C wrapper crates
 sedona-geoarrow-c = { version = "0.3.0", path = "c/sedona-geoarrow-c" }
 sedona-geos = { version = "0.3.0", path = "c/sedona-geos" }
+sedona-gdal = { version = "0.3.0", path = "c/sedona-gdal", default-features = 
false }
 sedona-proj = { version = "0.3.0", path = "c/sedona-proj", default-features = 
false }
 sedona-s2geography = { version = "0.3.0", path = "c/sedona-s2geography" }
 sedona-tg = { version = "0.3.0", path = "c/sedona-tg" }
diff --git a/c/sedona-gdal/Cargo.toml b/c/sedona-gdal/Cargo.toml
new file mode 100644
index 00000000..115bdab6
--- /dev/null
+++ b/c/sedona-gdal/Cargo.toml
@@ -0,0 +1,42 @@
+# 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.
+
+[package]
+name = "sedona-gdal"
+version.workspace = true
+license.workspace = true
+keywords.workspace = true
+categories.workspace = true
+authors.workspace = true
+homepage.workspace = true
+repository.workspace = true
+description.workspace = true
+readme.workspace = true
+edition.workspace = true
+rust-version.workspace = true
+
+[dependencies]
+gdal-sys = { version = "0.12.0", optional = true }
+libloading = { workspace = true }
+thiserror = { workspace = true }
+
+[features]
+default = ["gdal-sys"]
+gdal-sys = ["dep:gdal-sys"]
+
+[dev-dependencies]
+sedona-testing = { workspace = true }
diff --git a/c/sedona-gdal/src/dyn_load.rs b/c/sedona-gdal/src/dyn_load.rs
new file mode 100644
index 00000000..f85f05b0
--- /dev/null
+++ b/c/sedona-gdal/src/dyn_load.rs
@@ -0,0 +1,315 @@
+// 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 std::path::Path;
+
+use libloading::Library;
+
+use crate::errors::GdalInitLibraryError;
+use crate::gdal_dyn_bindgen::SedonaGdalApi;
+
+/// Load a single symbol from the library and write it into the given field.
+///
+/// We load as a raw `*const ()` pointer and transmute to the target function 
pointer
+/// type. This is the standard pattern for dynamic symbol loading where the 
loaded
+/// symbol's signature is known but cannot be expressed as a generic parameter 
to
+/// `Library::get` (because each field has a different signature).
+///
+/// On failure returns a `GdalInitLibraryError` with the symbol name and the
+/// underlying OS error message.
+macro_rules! load_fn {
+    ($lib:expr, $api:expr, $name:ident) => {
+        // The target types here are too verbose to annotate for each call site
+        #[allow(clippy::missing_transmute_annotations)]
+        {
+            $api.$name = Some(unsafe {
+                let sym = $lib
+                    .get::<*const ()>(concat!(stringify!($name), 
"\0").as_bytes())
+                    .map_err(|e| {
+                        GdalInitLibraryError::LibraryError(format!(
+                            "Failed to load symbol {}: {}",
+                            stringify!($name),
+                            e
+                        ))
+                    })?;
+                std::mem::transmute(sym.into_raw().into_raw())
+            });
+        }
+    };
+}
+
+/// Try to load a symbol under one of several names (e.g. for C++ mangled 
symbols).
+/// Writes the first successful match into `$api.$field`. Returns an error 
only if
+/// *none* of the names resolve.
+macro_rules! load_fn_any {
+    ($lib:expr, $api:expr, $field:ident, [$($name:expr),+ $(,)?]) => {{
+        let mut found = false;
+        $(
+            if !found {
+                if let Ok(sym) = unsafe { $lib.get::<*const ()>($name) } {
+                    // The target types here are too verbose to annotate for 
each call site
+                    #[allow(clippy::missing_transmute_annotations)]
+                    {
+                        $api.$field = Some(unsafe { 
std::mem::transmute(sym.into_raw().into_raw()) });
+                    }
+                    found = true;
+                }
+            }
+        )+
+        if !found {
+            return Err(GdalInitLibraryError::LibraryError(format!(
+                "Failed to resolve {} under any known mangled name",
+                stringify!($field),
+            )));
+        }
+    }};
+}
+
+/// Populate all function-pointer fields of [`SedonaGdalApi`] from the given
+/// [`Library`] handle.
+fn load_all_symbols(lib: &Library, api: &mut SedonaGdalApi) -> Result<(), 
GdalInitLibraryError> {
+    // --- Dataset ---
+    load_fn!(lib, api, GDALOpenEx);
+    load_fn!(lib, api, GDALClose);
+    load_fn!(lib, api, GDALGetRasterXSize);
+    load_fn!(lib, api, GDALGetRasterYSize);
+    load_fn!(lib, api, GDALGetRasterCount);
+    load_fn!(lib, api, GDALGetRasterBand);
+    load_fn!(lib, api, GDALGetGeoTransform);
+    load_fn!(lib, api, GDALSetGeoTransform);
+    load_fn!(lib, api, GDALGetProjectionRef);
+    load_fn!(lib, api, GDALSetProjection);
+    load_fn!(lib, api, GDALGetSpatialRef);
+    load_fn!(lib, api, GDALSetSpatialRef);
+    load_fn!(lib, api, GDALCreateCopy);
+    load_fn!(lib, api, GDALDatasetCreateLayer);
+
+    // --- Driver ---
+    load_fn!(lib, api, GDALAllRegister);
+    load_fn!(lib, api, GDALGetDriverByName);
+    load_fn!(lib, api, GDALCreate);
+
+    // --- Band ---
+    load_fn!(lib, api, GDALAddBand);
+    load_fn!(lib, api, GDALRasterIO);
+    load_fn!(lib, api, GDALRasterIOEx);
+    load_fn!(lib, api, GDALGetRasterDataType);
+    load_fn!(lib, api, GDALGetRasterBandXSize);
+    load_fn!(lib, api, GDALGetRasterBandYSize);
+    load_fn!(lib, api, GDALGetBlockSize);
+    load_fn!(lib, api, GDALGetRasterNoDataValue);
+    load_fn!(lib, api, GDALSetRasterNoDataValue);
+    load_fn!(lib, api, GDALDeleteRasterNoDataValue);
+    load_fn!(lib, api, GDALSetRasterNoDataValueAsUInt64);
+    load_fn!(lib, api, GDALSetRasterNoDataValueAsInt64);
+
+    // --- SpatialRef ---
+    load_fn!(lib, api, OSRNewSpatialReference);
+    load_fn!(lib, api, OSRDestroySpatialReference);
+    load_fn!(lib, api, OSRExportToPROJJSON);
+    load_fn!(lib, api, OSRClone);
+    load_fn!(lib, api, OSRRelease);
+
+    // --- Geometry ---
+    load_fn!(lib, api, OGR_G_CreateFromWkb);
+    load_fn!(lib, api, OGR_G_CreateFromWkt);
+    load_fn!(lib, api, OGR_G_ExportToIsoWkb);
+    load_fn!(lib, api, OGR_G_WkbSize);
+    load_fn!(lib, api, OGR_G_GetEnvelope);
+    load_fn!(lib, api, OGR_G_DestroyGeometry);
+
+    // --- Vector / Layer ---
+    load_fn!(lib, api, OGR_L_ResetReading);
+    load_fn!(lib, api, OGR_L_GetNextFeature);
+    load_fn!(lib, api, OGR_L_CreateField);
+    load_fn!(lib, api, OGR_L_GetFeatureCount);
+    load_fn!(lib, api, OGR_F_GetGeometryRef);
+    load_fn!(lib, api, OGR_F_GetFieldIndex);
+    load_fn!(lib, api, OGR_F_GetFieldAsDouble);
+    load_fn!(lib, api, OGR_F_GetFieldAsInteger);
+    load_fn!(lib, api, OGR_F_IsFieldSetAndNotNull);
+    load_fn!(lib, api, OGR_F_Destroy);
+    load_fn!(lib, api, OGR_Fld_Create);
+    load_fn!(lib, api, OGR_Fld_Destroy);
+
+    // --- VSI ---
+    load_fn!(lib, api, VSIFileFromMemBuffer);
+    load_fn!(lib, api, VSIFCloseL);
+    load_fn!(lib, api, VSIUnlink);
+    load_fn!(lib, api, VSIGetMemFileBuffer);
+    load_fn!(lib, api, VSIFree);
+    load_fn!(lib, api, VSIMalloc);
+
+    // --- VRT ---
+    load_fn!(lib, api, VRTCreate);
+    load_fn!(lib, api, VRTAddSimpleSource);
+
+    // --- Rasterize / Polygonize ---
+    load_fn!(lib, api, GDALRasterizeGeometries);
+    load_fn!(lib, api, GDALFPolygonize);
+    load_fn!(lib, api, GDALPolygonize);
+
+    // --- Version ---
+    load_fn!(lib, api, GDALVersionInfo);
+
+    // --- Config ---
+    load_fn!(lib, api, CPLSetThreadLocalConfigOption);
+
+    // --- Error ---
+    load_fn!(lib, api, CPLGetLastErrorNo);
+    load_fn!(lib, api, CPLGetLastErrorMsg);
+    load_fn!(lib, api, CPLErrorReset);
+
+    // --- Data Type ---
+    load_fn!(lib, api, GDALGetDataTypeSizeBytes);
+
+    // --- C++ API: MEMDataset::Create (resolved via mangled symbol names) ---
+    // The symbol is mangled differently on Linux, macOS, and MSVC, and the
+    // `char**` vs `const char**` parameter also affects the mangling.
+    load_fn_any!(
+        lib,
+        api,
+        MEMDatasetCreate,
+        [
+            // Linux and macOS
+            b"_ZN10MEMDataset6CreateEPKciii12GDALDataTypePPc\0",
+            // MSVC
+            b"?Create@MEMDataset@@SAPEAV1@PEBDHHHW4GDALDataType@@PEAPEAD@Z\0",
+        ]
+    );
+
+    Ok(())
+}
+
+/// Load a GDAL shared library from `path` and populate a [`SedonaGdalApi`] 
struct.
+///
+/// Returns the `(Library, SedonaGdalApi)` pair. The caller is responsible for
+/// keeping the `Library` alive for the lifetime of the function pointers.
+pub(crate) fn load_gdal_from_path(
+    path: &Path,
+) -> Result<(Library, SedonaGdalApi), GdalInitLibraryError> {
+    let lib = unsafe { Library::new(path.as_os_str()) }.map_err(|e| {
+        GdalInitLibraryError::LibraryError(format!(
+            "Failed to load GDAL library from {}: {}",
+            path.display(),
+            e
+        ))
+    })?;
+
+    let mut api = SedonaGdalApi::default();
+    load_all_symbols(&lib, &mut api)?;
+    Ok((lib, api))
+}
+
+/// Load GDAL symbols from the current process image (equivalent to 
`dlopen(NULL)`).
+///
+/// Returns the `(Library, SedonaGdalApi)` pair. The caller is responsible for
+/// keeping the `Library` alive for the lifetime of the function pointers.
+pub(crate) fn load_gdal_from_current_process(
+) -> Result<(Library, SedonaGdalApi), GdalInitLibraryError> {
+    let lib = current_process_library()?;
+    let mut api = SedonaGdalApi::default();
+    load_all_symbols(&lib, &mut api)?;
+    Ok((lib, api))
+}
+
+/// Open a handle to the current process image.
+#[cfg(unix)]
+fn current_process_library() -> Result<Library, GdalInitLibraryError> {
+    Ok(libloading::os::unix::Library::this().into())
+}
+
+#[cfg(windows)]
+fn current_process_library() -> Result<Library, GdalInitLibraryError> {
+    // Safety: loading symbols from the current process is safe.
+    Ok(unsafe { libloading::os::windows::Library::this() }
+        .map_err(|e| {
+            GdalInitLibraryError::LibraryError(format!(
+                "Failed to open current process handle: {}",
+                e
+            ))
+        })?
+        .into())
+}
+
+#[cfg(not(any(unix, windows)))]
+fn current_process_library() -> Result<Library, GdalInitLibraryError> {
+    Err(GdalInitLibraryError::Invalid(
+        "current_process_library() is not implemented for this platform. \
+    Only Unix and Windows are supported.",
+    ))
+}
+
+#[cfg(test)]
+mod test {
+    use super::*;
+
+    /// Loading from an invalid path should return a `LibraryError`.
+    #[test]
+    fn test_shared_library_error() {
+        let err = 
load_gdal_from_path(Path::new("/not/a/valid/gdal/library.so")).unwrap_err();
+        assert!(
+            matches!(err, GdalInitLibraryError::LibraryError(_)),
+            "Expected LibraryError, got: {err:?}"
+        );
+        assert!(
+            !err.to_string().is_empty(),
+            "Error message should not be empty"
+        );
+    }
+
+    /// Verify that loading from the current process succeeds when GDAL symbols
+    /// are linked in (i.e. the `gdal-sys` feature is enabled).
+    #[cfg(feature = "gdal-sys")]
+    #[test]
+    fn test_load_from_current_process() {
+        let (_lib, api) = load_gdal_from_current_process()
+            .expect("load_gdal_from_current_process should succeed when 
gdal-sys is linked");
+
+        // Spot-check that key function pointers were resolved.
+        assert!(
+            api.GDALAllRegister.is_some(),
+            "GDALAllRegister should be loaded"
+        );
+        assert!(api.GDALOpenEx.is_some(), "GDALOpenEx should be loaded");
+        assert!(api.GDALClose.is_some(), "GDALClose should be loaded");
+    }
+
+    /// Test loading from an explicit shared library path, gated behind an
+    /// environment variable. Skips gracefully when the variable is unset.
+    #[test]
+    fn test_load_from_shared_library() {
+        if let Ok(gdal_library) = 
std::env::var("SEDONA_GDAL_TEST_SHARED_LIBRARY") {
+            if !gdal_library.is_empty() {
+                let (_lib, api) = load_gdal_from_path(Path::new(&gdal_library))
+                    .expect("Should load GDAL from 
SEDONA_GDAL_TEST_SHARED_LIBRARY");
+
+                assert!(
+                    api.GDALAllRegister.is_some(),
+                    "GDALAllRegister should be loaded"
+                );
+                assert!(api.GDALOpenEx.is_some(), "GDALOpenEx should be 
loaded");
+                return;
+            }
+        }
+
+        println!(
+            "Skipping test_load_from_shared_library - \
+             SEDONA_GDAL_TEST_SHARED_LIBRARY environment variable not set"
+        );
+    }
+}
diff --git a/c/sedona-gdal/src/errors.rs b/c/sedona-gdal/src/errors.rs
new file mode 100644
index 00000000..166e0423
--- /dev/null
+++ b/c/sedona-gdal/src/errors.rs
@@ -0,0 +1,42 @@
+// 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.
+
+//! Ported (and contains copied code) from georust/gdal:
+//! <https://github.com/georust/gdal/blob/v0.19.0/src/errors.rs>.
+//! Original code is licensed under MIT.
+
+use thiserror::Error;
+
+/// Error type for the sedona-gdal crate initialization and library loading.
+#[derive(Error, Debug)]
+pub enum GdalInitLibraryError {
+    #[error("{0}")]
+    Invalid(String),
+    #[error("{0}")]
+    LibraryError(String),
+}
+
+/// Error type compatible with the georust/gdal error variants used in this 
codebase.
+#[derive(Clone, Debug, Error)]
+pub enum GdalError {
+    #[error("CPL error class: '{class:?}', error number: '{number}', error 
msg: '{msg}'")]
+    CplError {
+        class: u32,
+        number: i32,
+        msg: String,
+    },
+}
diff --git a/c/sedona-gdal/src/gdal_api.rs b/c/sedona-gdal/src/gdal_api.rs
new file mode 100644
index 00000000..d0c07f6f
--- /dev/null
+++ b/c/sedona-gdal/src/gdal_api.rs
@@ -0,0 +1,116 @@
+// 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 std::ffi::CStr;
+use std::path::PathBuf;
+
+use libloading::Library;
+
+use crate::dyn_load;
+use crate::errors::{GdalError, GdalInitLibraryError};
+use crate::gdal_dyn_bindgen::SedonaGdalApi;
+
+/// Invoke a function pointer from the `SedonaGdalApi` struct.
+///
+/// # Panics
+///
+/// Panics if the function pointer is `None`. This is unreachable in correct 
usage
+/// because all function pointers are guaranteed to be `Some` after successful
+/// initialization of [`GdalApi`] via [`GdalApi::try_from_shared_library`] or
+/// [`GdalApi::try_from_current_process`], and you cannot obtain a `&GdalApi`
+/// without successful initialization.
+macro_rules! call_gdal_api {
+    ($api:expr, $func:ident $(, $arg:expr)*) => {
+        if let Some(func) = $api.inner.$func {
+            func($($arg),*)
+        } else {
+            panic!("{} function not available", stringify!($func))
+        }
+    };
+}
+
+#[derive(Debug)]
+pub struct GdalApi {
+    pub(crate) inner: SedonaGdalApi,
+    /// The dynamically loaded GDAL library. Kept alive for the lifetime of the
+    /// function pointers in `inner`. This is never dropped because the 
`GdalApi`
+    /// lives in a `static OnceLock` (see `global.rs`).
+    _lib: Library,
+    name: String,
+}
+
+impl GdalApi {
+    pub fn try_from_shared_library(shared_library: PathBuf) -> Result<Self, 
GdalInitLibraryError> {
+        let (lib, inner) = dyn_load::load_gdal_from_path(&shared_library)?;
+        Ok(Self {
+            inner,
+            _lib: lib,
+            name: shared_library.to_string_lossy().into_owned(),
+        })
+    }
+
+    pub fn try_from_current_process() -> Result<Self, GdalInitLibraryError> {
+        let (lib, inner) = dyn_load::load_gdal_from_current_process()?;
+        Ok(Self {
+            inner,
+            _lib: lib,
+            name: "current_process".to_string(),
+        })
+    }
+
+    pub fn name(&self) -> &str {
+        &self.name
+    }
+
+    /// Query GDAL version information.
+    ///
+    /// `request` is one of the standard `GDALVersionInfo` keys:
+    /// - `"RELEASE_NAME"` — e.g. `"3.8.4"`
+    /// - `"VERSION_NUM"` — e.g. `"3080400"`
+    /// - `"BUILD_INFO"` — multi-line build details
+    pub fn version_info(&self, request: &str) -> String {
+        let c_request = std::ffi::CString::new(request).unwrap();
+        let ptr = unsafe { call_gdal_api!(self, GDALVersionInfo, 
c_request.as_ptr()) };
+        if ptr.is_null() {
+            String::new()
+        } else {
+            unsafe { CStr::from_ptr(ptr) }
+                .to_string_lossy()
+                .into_owned()
+        }
+    }
+
+    /// Check the last CPL error and return a `GdalError`, it always returns 
an error struct
+    /// (even when the error number is 0).
+    pub fn last_cpl_err(&self, default_err_class: u32) -> GdalError {
+        let err_no = unsafe { call_gdal_api!(self, CPLGetLastErrorNo) };
+        let err_msg = unsafe {
+            let msg_ptr = call_gdal_api!(self, CPLGetLastErrorMsg);
+            if msg_ptr.is_null() {
+                String::new()
+            } else {
+                CStr::from_ptr(msg_ptr).to_string_lossy().into_owned()
+            }
+        };
+        unsafe { call_gdal_api!(self, CPLErrorReset) };
+        GdalError::CplError {
+            class: default_err_class,
+            number: err_no,
+            msg: err_msg,
+        }
+    }
+}
diff --git a/c/sedona-gdal/src/gdal_dyn_bindgen.rs 
b/c/sedona-gdal/src/gdal_dyn_bindgen.rs
new file mode 100644
index 00000000..1b05a2c1
--- /dev/null
+++ b/c/sedona-gdal/src/gdal_dyn_bindgen.rs
@@ -0,0 +1,533 @@
+// 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.
+#![allow(non_camel_case_types)]
+#![allow(non_snake_case)]
+#![allow(non_upper_case_globals)]
+#![allow(dead_code)]
+#![allow(clippy::type_complexity)]
+
+use std::os::raw::{c_char, c_double, c_int, c_uchar, c_uint, c_void};
+
+// --- Scalar type aliases ---
+
+pub type GSpacing = i64;
+pub type CPLErr = c_int;
+pub type OGRErr = c_int;
+pub type GDALRWFlag = c_int;
+pub type OGRwkbByteOrder = c_int;
+pub type GDALOpenFlags = c_uint;
+pub type GDALRIOResampleAlg = c_int;
+
+// --- Opaque handle types ---
+
+pub type GDALDatasetH = *mut c_void;
+pub type GDALDriverH = *mut c_void;
+pub type GDALRasterBandH = *mut c_void;
+pub type OGRSpatialReferenceH = *mut c_void;
+pub type OGRGeometryH = *mut c_void;
+pub type OGRLayerH = *mut c_void;
+pub type OGRFeatureH = *mut c_void;
+pub type OGRFieldDefnH = *mut c_void;
+pub type VSILFILE = *mut c_void;
+
+// --- Enum types ---
+
+#[repr(C)]
+#[derive(Debug, Copy, Clone, Eq, PartialEq)]
+pub enum GDALDataType {
+    GDT_Unknown = 0,
+    GDT_Byte = 1,
+    GDT_Int8 = 14,
+    GDT_UInt16 = 2,
+    GDT_Int16 = 3,
+    GDT_UInt32 = 4,
+    GDT_Int32 = 5,
+    GDT_UInt64 = 12,
+    GDT_Int64 = 13,
+    GDT_Float16 = 15,
+    GDT_Float32 = 6,
+    GDT_Float64 = 7,
+    GDT_CInt16 = 8,
+    GDT_CInt32 = 9,
+    GDT_CFloat16 = 16,
+    GDT_CFloat32 = 10,
+    GDT_CFloat64 = 11,
+}
+
+impl GDALDataType {
+    #[allow(clippy::result_unit_err)]
+    pub fn try_from_ordinal(value: i32) -> Result<Self, ()> {
+        match value {
+            0 => Ok(GDALDataType::GDT_Unknown),
+            1 => Ok(GDALDataType::GDT_Byte),
+            2 => Ok(GDALDataType::GDT_UInt16),
+            3 => Ok(GDALDataType::GDT_Int16),
+            4 => Ok(GDALDataType::GDT_UInt32),
+            5 => Ok(GDALDataType::GDT_Int32),
+            6 => Ok(GDALDataType::GDT_Float32),
+            7 => Ok(GDALDataType::GDT_Float64),
+            8 => Ok(GDALDataType::GDT_CInt16),
+            9 => Ok(GDALDataType::GDT_CInt32),
+            10 => Ok(GDALDataType::GDT_CFloat32),
+            11 => Ok(GDALDataType::GDT_CFloat64),
+            12 => Ok(GDALDataType::GDT_UInt64),
+            13 => Ok(GDALDataType::GDT_Int64),
+            14 => Ok(GDALDataType::GDT_Int8),
+            15 => Ok(GDALDataType::GDT_Float16),
+            16 => Ok(GDALDataType::GDT_CFloat16),
+            _ => Err(()),
+        }
+    }
+}
+
+#[repr(C)]
+#[derive(Debug, Copy, Clone, Eq, PartialEq)]
+pub enum OGRwkbGeometryType {
+    wkbUnknown = 0,
+    wkbPoint = 1,
+    wkbLineString = 2,
+    wkbPolygon = 3,
+    wkbMultiPoint = 4,
+    wkbMultiLineString = 5,
+    wkbMultiPolygon = 6,
+    wkbGeometryCollection = 7,
+}
+
+#[repr(C)]
+#[derive(Debug, Copy, Clone, Eq, PartialEq)]
+pub enum OGRFieldType {
+    OFTInteger = 0,
+    OFTIntegerList = 1,
+    OFTReal = 2,
+    OFTRealList = 3,
+    OFTString = 4,
+    OFTStringList = 5,
+    OFTWideString = 6,
+    OFTWideStringList = 7,
+    OFTBinary = 8,
+    OFTDate = 9,
+    OFTTime = 10,
+    OFTDateTime = 11,
+    OFTInteger64 = 12,
+    OFTInteger64List = 13,
+}
+
+// --- Function pointer type aliases ---
+
+/// Type alias for the GDAL transformer callback (`GDALTransformerFunc`).
+///
+/// Signature: `(pTransformerArg, bDstToSrc, nPointCount, x, y, z, panSuccess) 
-> c_int`
+pub type GDALTransformerFunc = unsafe extern "C" fn(
+    pTransformerArg: *mut c_void,
+    bDstToSrc: c_int,
+    nPointCount: c_int,
+    x: *mut c_double,
+    y: *mut c_double,
+    z: *mut c_double,
+    panSuccess: *mut c_int,
+) -> c_int;
+
+// --- Structs ---
+
+#[repr(C)]
+#[derive(Debug, Copy, Clone)]
+pub struct OGREnvelope {
+    pub MinX: c_double,
+    pub MaxX: c_double,
+    pub MinY: c_double,
+    pub MaxY: c_double,
+}
+
+/// GDAL progress callback type.
+pub type GDALProgressFunc = Option<
+    unsafe extern "C" fn(
+        dfComplete: c_double,
+        pszMessage: *const c_char,
+        pProgressArg: *mut c_void,
+    ) -> c_int,
+>;
+
+/// Extra arguments for `GDALRasterIOEx`.
+#[repr(C)]
+#[derive(Debug, Copy, Clone)]
+pub struct GDALRasterIOExtraArg {
+    pub nVersion: c_int,
+    pub eResampleAlg: GDALRIOResampleAlg,
+    pub pfnProgress: GDALProgressFunc,
+    pub pProgressData: *mut c_void,
+    pub bFloatingPointWindowValidity: c_int,
+    pub dfXOff: c_double,
+    pub dfYOff: c_double,
+    pub dfXSize: c_double,
+    pub dfYSize: c_double,
+}
+
+impl Default for GDALRasterIOExtraArg {
+    fn default() -> Self {
+        Self {
+            nVersion: 1,
+            eResampleAlg: GRIORA_NearestNeighbour,
+            pfnProgress: None,
+            pProgressData: std::ptr::null_mut(),
+            bFloatingPointWindowValidity: 0,
+            dfXOff: 0.0,
+            dfYOff: 0.0,
+            dfXSize: 0.0,
+            dfYSize: 0.0,
+        }
+    }
+}
+
+// --- GDALRIOResampleAlg constants ---
+
+pub const GRIORA_NearestNeighbour: GDALRIOResampleAlg = 0;
+pub const GRIORA_Bilinear: GDALRIOResampleAlg = 1;
+pub const GRIORA_Cubic: GDALRIOResampleAlg = 2;
+pub const GRIORA_CubicSpline: GDALRIOResampleAlg = 3;
+pub const GRIORA_Lanczos: GDALRIOResampleAlg = 4;
+pub const GRIORA_Average: GDALRIOResampleAlg = 5;
+pub const GRIORA_Mode: GDALRIOResampleAlg = 6;
+pub const GRIORA_Gauss: GDALRIOResampleAlg = 7;
+
+// --- GDAL open flags constants ---
+
+pub const GDAL_OF_READONLY: GDALOpenFlags = 0x00;
+pub const GDAL_OF_UPDATE: GDALOpenFlags = 0x01;
+pub const GDAL_OF_RASTER: GDALOpenFlags = 0x02;
+pub const GDAL_OF_VECTOR: GDALOpenFlags = 0x04;
+pub const GDAL_OF_VERBOSE_ERROR: GDALOpenFlags = 0x40;
+
+// --- GDALRWFlag constants ---
+
+pub const GF_Read: GDALRWFlag = 0;
+pub const GF_Write: GDALRWFlag = 1;
+
+// --- CPLErr constants ---
+
+pub const CE_None: CPLErr = 0;
+pub const CE_Debug: CPLErr = 1;
+pub const CE_Warning: CPLErr = 2;
+pub const CE_Failure: CPLErr = 3;
+pub const CE_Fatal: CPLErr = 4;
+
+// --- OGRErr constants ---
+
+pub const OGRERR_NONE: OGRErr = 0;
+
+// --- OGRwkbByteOrder constants ---
+
+pub const wkbXDR: OGRwkbByteOrder = 0; // Big endian
+pub const wkbNDR: OGRwkbByteOrder = 1; // Little endian
+
+// --- The main API struct mirroring C SedonaGdalApi ---
+
+#[repr(C)]
+#[derive(Debug, Copy, Clone, Default)]
+pub(crate) struct SedonaGdalApi {
+    // --- Dataset ---
+    pub GDALOpenEx: Option<
+        unsafe extern "C" fn(
+            pszFilename: *const c_char,
+            nOpenFlags: c_uint,
+            papszAllowedDrivers: *const *const c_char,
+            papszOpenOptions: *const *const c_char,
+            papszSiblingFiles: *const *const c_char,
+        ) -> GDALDatasetH,
+    >,
+    pub GDALClose: Option<unsafe extern "C" fn(hDS: GDALDatasetH)>,
+    pub GDALGetRasterXSize: Option<unsafe extern "C" fn(hDataset: 
GDALDatasetH) -> c_int>,
+    pub GDALGetRasterYSize: Option<unsafe extern "C" fn(hDataset: 
GDALDatasetH) -> c_int>,
+    pub GDALGetRasterCount: Option<unsafe extern "C" fn(hDataset: 
GDALDatasetH) -> c_int>,
+    pub GDALGetRasterBand:
+        Option<unsafe extern "C" fn(hDS: GDALDatasetH, nBandId: c_int) -> 
GDALRasterBandH>,
+    pub GDALGetGeoTransform:
+        Option<unsafe extern "C" fn(hDS: GDALDatasetH, padfTransform: *mut 
c_double) -> CPLErr>,
+    pub GDALSetGeoTransform:
+        Option<unsafe extern "C" fn(hDS: GDALDatasetH, padfTransform: *mut 
c_double) -> CPLErr>,
+    pub GDALGetProjectionRef: Option<unsafe extern "C" fn(hDS: GDALDatasetH) 
-> *const c_char>,
+    pub GDALSetProjection:
+        Option<unsafe extern "C" fn(hDS: GDALDatasetH, pszProjection: *const 
c_char) -> CPLErr>,
+    pub GDALGetSpatialRef: Option<unsafe extern "C" fn(hDS: GDALDatasetH) -> 
OGRSpatialReferenceH>,
+    pub GDALSetSpatialRef:
+        Option<unsafe extern "C" fn(hDS: GDALDatasetH, hSRS: 
OGRSpatialReferenceH) -> CPLErr>,
+    pub GDALCreateCopy: Option<
+        unsafe extern "C" fn(
+            hDriver: GDALDriverH,
+            pszFilename: *const c_char,
+            hSrcDS: GDALDatasetH,
+            bStrict: c_int,
+            papszOptions: *mut *mut c_char,
+            pfnProgress: *mut c_void,
+            pProgressData: *mut c_void,
+        ) -> GDALDatasetH,
+    >,
+    pub GDALDatasetCreateLayer: Option<
+        unsafe extern "C" fn(
+            hDS: GDALDatasetH,
+            pszName: *const c_char,
+            hSpatialRef: OGRSpatialReferenceH,
+            eGType: OGRwkbGeometryType,
+            papszOptions: *mut *mut c_char,
+        ) -> OGRLayerH,
+    >,
+
+    // --- Driver ---
+    pub GDALAllRegister: Option<unsafe extern "C" fn()>,
+    pub GDALGetDriverByName: Option<unsafe extern "C" fn(pszName: *const 
c_char) -> GDALDriverH>,
+    pub GDALCreate: Option<
+        unsafe extern "C" fn(
+            hDriver: GDALDriverH,
+            pszFilename: *const c_char,
+            nXSize: c_int,
+            nYSize: c_int,
+            nBands: c_int,
+            eType: GDALDataType,
+            papszOptions: *mut *mut c_char,
+        ) -> GDALDatasetH,
+    >,
+
+    // --- Band ---
+    pub GDALAddBand: Option<
+        unsafe extern "C" fn(
+            hDS: GDALDatasetH,
+            eType: GDALDataType,
+            papszOptions: *mut *mut c_char,
+        ) -> CPLErr,
+    >,
+    pub GDALRasterIO: Option<
+        unsafe extern "C" fn(
+            hRBand: GDALRasterBandH,
+            eRWFlag: GDALRWFlag,
+            nDSXOff: c_int,
+            nDSYOff: c_int,
+            nDSXSize: c_int,
+            nDSYSize: c_int,
+            pBuffer: *mut c_void,
+            nBXSize: c_int,
+            nBYSize: c_int,
+            eBDataType: GDALDataType,
+            nPixelSpace: GSpacing,
+            nLineSpace: GSpacing,
+        ) -> CPLErr,
+    >,
+    pub GDALRasterIOEx: Option<
+        unsafe extern "C" fn(
+            hRBand: GDALRasterBandH,
+            eRWFlag: GDALRWFlag,
+            nDSXOff: c_int,
+            nDSYOff: c_int,
+            nDSXSize: c_int,
+            nDSYSize: c_int,
+            pBuffer: *mut c_void,
+            nBXSize: c_int,
+            nBYSize: c_int,
+            eBDataType: GDALDataType,
+            nPixelSpace: GSpacing,
+            nLineSpace: GSpacing,
+            psExtraArg: *mut GDALRasterIOExtraArg,
+        ) -> CPLErr,
+    >,
+    pub GDALGetRasterDataType: Option<unsafe extern "C" fn(hBand: 
GDALRasterBandH) -> GDALDataType>,
+    pub GDALGetRasterBandXSize: Option<unsafe extern "C" fn(hBand: 
GDALRasterBandH) -> c_int>,
+    pub GDALGetRasterBandYSize: Option<unsafe extern "C" fn(hBand: 
GDALRasterBandH) -> c_int>,
+    pub GDALGetBlockSize: Option<
+        unsafe extern "C" fn(hBand: GDALRasterBandH, pnXSize: *mut c_int, 
pnYSize: *mut c_int),
+    >,
+    pub GDALGetRasterNoDataValue:
+        Option<unsafe extern "C" fn(hBand: GDALRasterBandH, pbSuccess: *mut 
c_int) -> c_double>,
+    pub GDALSetRasterNoDataValue:
+        Option<unsafe extern "C" fn(hBand: GDALRasterBandH, dfValue: c_double) 
-> CPLErr>,
+    pub GDALDeleteRasterNoDataValue: Option<unsafe extern "C" fn(hBand: 
GDALRasterBandH) -> CPLErr>,
+    pub GDALSetRasterNoDataValueAsUInt64:
+        Option<unsafe extern "C" fn(hBand: GDALRasterBandH, nValue: u64) -> 
CPLErr>,
+    pub GDALSetRasterNoDataValueAsInt64:
+        Option<unsafe extern "C" fn(hBand: GDALRasterBandH, nValue: i64) -> 
CPLErr>,
+
+    // --- SpatialRef ---
+    pub OSRNewSpatialReference:
+        Option<unsafe extern "C" fn(pszWKT: *const c_char) -> 
OGRSpatialReferenceH>,
+    pub OSRDestroySpatialReference: Option<unsafe extern "C" fn(hSRS: 
OGRSpatialReferenceH)>,
+    pub OSRExportToPROJJSON: Option<
+        unsafe extern "C" fn(
+            hSRS: OGRSpatialReferenceH,
+            ppszResult: *mut *mut c_char,
+            papszOptions: *const *const c_char,
+        ) -> OGRErr,
+    >,
+    pub OSRClone: Option<unsafe extern "C" fn(hSRS: OGRSpatialReferenceH) -> 
OGRSpatialReferenceH>,
+    pub OSRRelease: Option<unsafe extern "C" fn(hSRS: OGRSpatialReferenceH)>,
+
+    // --- Geometry ---
+    pub OGR_G_CreateFromWkb: Option<
+        unsafe extern "C" fn(
+            pabyData: *const c_void,
+            hSRS: OGRSpatialReferenceH,
+            phGeometry: *mut OGRGeometryH,
+            nBytes: c_int,
+        ) -> OGRErr,
+    >,
+    pub OGR_G_CreateFromWkt: Option<
+        unsafe extern "C" fn(
+            ppszData: *mut *mut c_char,
+            hSRS: OGRSpatialReferenceH,
+            phGeometry: *mut OGRGeometryH,
+        ) -> OGRErr,
+    >,
+    pub OGR_G_ExportToIsoWkb: Option<
+        unsafe extern "C" fn(
+            hGeom: OGRGeometryH,
+            eOrder: OGRwkbByteOrder,
+            pabyData: *mut c_uchar,
+        ) -> OGRErr,
+    >,
+    pub OGR_G_WkbSize: Option<unsafe extern "C" fn(hGeom: OGRGeometryH) -> 
c_int>,
+    pub OGR_G_GetEnvelope:
+        Option<unsafe extern "C" fn(hGeom: OGRGeometryH, psEnvelope: *mut 
OGREnvelope)>,
+    pub OGR_G_DestroyGeometry: Option<unsafe extern "C" fn(hGeom: 
OGRGeometryH)>,
+
+    // --- Vector / Layer ---
+    pub OGR_L_ResetReading: Option<unsafe extern "C" fn(hLayer: OGRLayerH)>,
+    pub OGR_L_GetNextFeature: Option<unsafe extern "C" fn(hLayer: OGRLayerH) 
-> OGRFeatureH>,
+    pub OGR_L_CreateField: Option<
+        unsafe extern "C" fn(
+            hLayer: OGRLayerH,
+            hFieldDefn: OGRFieldDefnH,
+            bApproxOK: c_int,
+        ) -> OGRErr,
+    >,
+    pub OGR_L_GetFeatureCount:
+        Option<unsafe extern "C" fn(hLayer: OGRLayerH, bForce: c_int) -> i64>,
+    pub OGR_F_GetGeometryRef: Option<unsafe extern "C" fn(hFeat: OGRFeatureH) 
-> OGRGeometryH>,
+    pub OGR_F_GetFieldIndex:
+        Option<unsafe extern "C" fn(hFeat: OGRFeatureH, pszName: *const 
c_char) -> c_int>,
+    pub OGR_F_GetFieldAsDouble:
+        Option<unsafe extern "C" fn(hFeat: OGRFeatureH, iField: c_int) -> 
c_double>,
+    pub OGR_F_GetFieldAsInteger:
+        Option<unsafe extern "C" fn(hFeat: OGRFeatureH, iField: c_int) -> 
c_int>,
+    pub OGR_F_IsFieldSetAndNotNull:
+        Option<unsafe extern "C" fn(hFeat: OGRFeatureH, iField: c_int) -> 
c_int>,
+    pub OGR_F_Destroy: Option<unsafe extern "C" fn(hFeat: OGRFeatureH)>,
+    pub OGR_Fld_Create:
+        Option<unsafe extern "C" fn(pszName: *const c_char, eType: 
OGRFieldType) -> OGRFieldDefnH>,
+    pub OGR_Fld_Destroy: Option<unsafe extern "C" fn(hDefn: OGRFieldDefnH)>,
+
+    // --- VSI ---
+    pub VSIFileFromMemBuffer: Option<
+        unsafe extern "C" fn(
+            pszFilename: *const c_char,
+            pabyData: *mut c_uchar,
+            nDataLength: i64,
+            bTakeOwnership: c_int,
+        ) -> VSILFILE,
+    >,
+    pub VSIFCloseL: Option<unsafe extern "C" fn(fp: VSILFILE) -> c_int>,
+    pub VSIUnlink: Option<unsafe extern "C" fn(pszFilename: *const c_char) -> 
c_int>,
+    pub VSIGetMemFileBuffer: Option<
+        unsafe extern "C" fn(
+            pszFilename: *const c_char,
+            pnDataLength: *mut i64,
+            bUnlinkAndSeize: c_int,
+        ) -> *mut c_uchar,
+    >,
+    pub VSIFree: Option<unsafe extern "C" fn(pData: *mut c_void)>,
+    pub VSIMalloc: Option<unsafe extern "C" fn(nSize: usize) -> *mut c_void>,
+
+    // --- VRT ---
+    pub VRTCreate: Option<unsafe extern "C" fn(nXSize: c_int, nYSize: c_int) 
-> GDALDatasetH>,
+    pub VRTAddSimpleSource: Option<
+        unsafe extern "C" fn(
+            hVRTBand: GDALRasterBandH,
+            hSrcBand: GDALRasterBandH,
+            nSrcXOff: c_int,
+            nSrcYOff: c_int,
+            nSrcXSize: c_int,
+            nSrcYSize: c_int,
+            nDstXOff: c_int,
+            nDstYOff: c_int,
+            nDstXSize: c_int,
+            nDstYSize: c_int,
+            pszResampling: *const c_char,
+            dfNoDataValue: c_double,
+        ) -> CPLErr,
+    >,
+
+    // --- Rasterize / Polygonize ---
+    pub GDALRasterizeGeometries: Option<
+        unsafe extern "C" fn(
+            hDS: GDALDatasetH,
+            nBandCount: c_int,
+            panBandList: *const c_int,
+            nGeomCount: c_int,
+            pahGeometries: *const OGRGeometryH,
+            pfnTransformer: *mut c_void,
+            pTransformArg: *mut c_void,
+            padfGeomBurnValues: *const c_double,
+            papszOptions: *mut *mut c_char,
+            pfnProgress: *mut c_void,
+            pProgressData: *mut c_void,
+        ) -> CPLErr,
+    >,
+    pub GDALFPolygonize: Option<
+        unsafe extern "C" fn(
+            hSrcBand: GDALRasterBandH,
+            hMaskBand: GDALRasterBandH,
+            hOutLayer: OGRLayerH,
+            iPixValField: c_int,
+            papszOptions: *mut *mut c_char,
+            pfnProgress: *mut c_void,
+            pProgressData: *mut c_void,
+        ) -> CPLErr,
+    >,
+    pub GDALPolygonize: Option<
+        unsafe extern "C" fn(
+            hSrcBand: GDALRasterBandH,
+            hMaskBand: GDALRasterBandH,
+            hOutLayer: OGRLayerH,
+            iPixValField: c_int,
+            papszOptions: *mut *mut c_char,
+            pfnProgress: *mut c_void,
+            pProgressData: *mut c_void,
+        ) -> CPLErr,
+    >,
+
+    // --- Version ---
+    pub GDALVersionInfo: Option<unsafe extern "C" fn(pszRequest: *const 
c_char) -> *const c_char>,
+
+    // --- Config ---
+    pub CPLSetThreadLocalConfigOption:
+        Option<unsafe extern "C" fn(pszKey: *const c_char, pszValue: *const 
c_char)>,
+
+    // --- Error ---
+    pub CPLGetLastErrorNo: Option<unsafe extern "C" fn() -> c_int>,
+    pub CPLGetLastErrorMsg: Option<unsafe extern "C" fn() -> *const c_char>,
+    pub CPLErrorReset: Option<unsafe extern "C" fn()>,
+
+    // --- Data Type ---
+    pub GDALGetDataTypeSizeBytes: Option<unsafe extern "C" fn(eDataType: 
GDALDataType) -> c_int>,
+
+    // --- C++ API ---
+    pub MEMDatasetCreate: Option<
+        unsafe extern "C" fn(
+            pszFilename: *const c_char,
+            nXSize: c_int,
+            nYSize: c_int,
+            nBandsIn: c_int,
+            eType: GDALDataType,
+            papszOptions: *mut *mut c_char,
+        ) -> GDALDatasetH,
+    >,
+}
diff --git a/c/sedona-gdal/src/global.rs b/c/sedona-gdal/src/global.rs
new file mode 100644
index 00000000..9365ef89
--- /dev/null
+++ b/c/sedona-gdal/src/global.rs
@@ -0,0 +1,313 @@
+// 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::errors::GdalInitLibraryError;
+use crate::gdal_api::GdalApi;
+use std::path::PathBuf;
+use std::sync::{Mutex, OnceLock};
+
+/// Minimum GDAL version required by sedona-gdal.
+const MIN_GDAL_VERSION_MAJOR: i32 = 3;
+const MIN_GDAL_VERSION_MINOR: i32 = 5;
+const MIN_GDAL_VERSION_NUM: i32 =
+    MIN_GDAL_VERSION_MAJOR * 1_000_000 + MIN_GDAL_VERSION_MINOR * 10_000;
+
+/// Builder for the global [`GdalApi`].
+///
+/// Provides a way to configure how GDAL is loaded before the first use.
+/// Use [`configure_global_gdal_api`] to install a builder, then the actual
+/// loading happens lazily on the first call to [`get_global_gdal_api`].
+///
+/// # Examples
+///
+/// ```no_run
+/// use sedona_gdal::global::{GdalApiBuilder, configure_global_gdal_api};
+///
+/// // Configure with a specific shared library path
+/// let builder = GdalApiBuilder::default()
+///     .with_shared_library("/usr/lib/libgdal.so".into());
+/// configure_global_gdal_api(builder).unwrap();
+/// ```
+#[derive(Default)]
+pub struct GdalApiBuilder {
+    shared_library: Option<PathBuf>,
+}
+
+impl GdalApiBuilder {
+    /// Set the path to the GDAL shared library.
+    ///
+    /// If unset, GDAL symbols will be resolved from the current process image
+    /// (equivalent to `dlopen(NULL)`), which requires GDAL to already be 
linked
+    /// into the process (e.g. via `gdal-sys`).
+    ///
+    /// Note that the path is passed directly to `dlopen()`/`LoadLibrary()`,
+    /// which takes into account the working directory. As a security measure,
+    /// applications may wish to verify that the path is absolute. This should
+    /// not be specified from untrusted input.
+    pub fn with_shared_library(self, path: PathBuf) -> Self {
+        Self {
+            shared_library: Some(path),
+        }
+    }
+
+    /// Build a [`GdalApi`] with the configured options.
+    ///
+    /// When `shared_library` is set, loads GDAL from that path. Otherwise,
+    /// resolves symbols from the current process (with an optional
+    /// compile-time version check when the `gdal-sys` feature is enabled).
+    pub fn build(&self) -> Result<GdalApi, GdalInitLibraryError> {
+        let api = if let Some(shared_library) = &self.shared_library {
+            GdalApi::try_from_shared_library(shared_library.clone())?
+        } else {
+            GdalApi::try_from_current_process()?
+        };
+
+        #[cfg(feature = "gdal-sys")]
+        let get_gdal_version_info = |arg: &str| unsafe {
+            // Calling into `gdal-sys` also forces the linker to include GDAL
+            // symbols, so that `try_from_current_process` (which resolves 
function pointers
+            // via `dlsym` on the current process) can find them at runtime.
+            let c_arg = std::ffi::CString::new(arg).unwrap();
+            let c_version = gdal_sys::GDALVersionInfo(c_arg.as_ptr());
+            std::ffi::CStr::from_ptr(c_version)
+                .to_string_lossy()
+                .into_owned()
+        };
+
+        #[cfg(not(feature = "gdal-sys"))]
+        let get_gdal_version_info = |arg: &str| api.version_info(arg);
+
+        check_gdal_version(get_gdal_version_info)?;
+        Ok(api)
+    }
+}
+
+/// Global builder configuration, protected by a [`Mutex`].
+///
+/// Set via [`configure_global_gdal_api`] before the first call to
+/// [`get_global_gdal_api`]. Multiple calls to `configure_global_gdal_api`
+/// are allowed as long as the API has not been initialized yet.
+/// The same mutex also serves as the initialization guard for
+/// [`get_global_gdal_api`], eliminating the need for a separate lock.
+static GDAL_API_BUILDER: Mutex<Option<GdalApiBuilder>> = Mutex::new(None);
+
+static GDAL_API: OnceLock<GdalApi> = OnceLock::new();
+
+/// Get a reference to the global GDAL API, initializing it if not already 
done.
+///
+/// On first call, reads the builder set by [`configure_global_gdal_api`] (or 
uses
+/// [`GdalApiBuilder::default()`] if none was configured) and calls its 
`build()`
+/// method to create the [`GdalApi`]. The result is stored in a process-global
+/// `OnceLock` and reused for all subsequent calls.
+fn get_global_gdal_api() -> Result<&'static GdalApi, GdalInitLibraryError> {
+    if let Some(api) = GDAL_API.get() {
+        return Ok(api);
+    }
+
+    let guard = GDAL_API_BUILDER
+        .lock()
+        .map_err(|_| GdalInitLibraryError::Invalid("GDAL API builder lock 
poisoned".to_string()))?;
+
+    if let Some(api) = GDAL_API.get() {
+        return Ok(api);
+    }
+
+    let api = guard
+        .as_ref()
+        .unwrap_or(&GdalApiBuilder::default())
+        .build()?;
+
+    // Register all GDAL drivers once, immediately after loading symbols.
+    // This mirrors georust/gdal's `_register_drivers()` pattern where
+    // `GDALAllRegister` is called via `std::sync::Once` before any driver
+    // lookup or dataset open. Here the `OnceLock` + `Mutex` already
+    // guarantees this runs exactly once.
+    unsafe {
+        let Some(gdal_all_register) = api.inner.GDALAllRegister else {
+            return Err(GdalInitLibraryError::LibraryError(
+                "GDALAllRegister symbol not loaded".to_string(),
+            ));
+        };
+        gdal_all_register();
+    }
+
+    let _ = GDAL_API.set(api);
+    Ok(GDAL_API.get().expect("GDAL API should be set"))
+}
+
+/// Configure the global GDAL API.
+///
+/// Stores the given [`GdalApiBuilder`] for use when the global [`GdalApi`] is
+/// first initialized (lazily, on the first call to [`get_global_gdal_api`]).
+///
+/// This can be called multiple times before the API is initialized — each call
+/// replaces the previous builder. However, once [`get_global_gdal_api`] has 
been
+/// called and the API has been successfully initialized, subsequent 
configurations
+/// are accepted but will have no effect (the `OnceLock` ensures the API is 
created
+/// only once).
+///
+/// # Typical usage
+///
+/// 1. The application (e.g. sedona-db) calls `configure_global_gdal_api` with 
its
+///    default builder early in startup.
+/// 2. User code may call `configure_global_gdal_api` again to override the
+///    configuration before the first query that uses GDAL.
+/// 3. On the first actual GDAL operation, [`get_global_gdal_api`] reads the
+///    builder and initializes the API.
+pub fn configure_global_gdal_api(builder: GdalApiBuilder) -> Result<(), 
GdalInitLibraryError> {
+    let mut global_builder = GDAL_API_BUILDER.lock().map_err(|_| {
+        GdalInitLibraryError::Invalid(
+            "Failed to acquire lock for global GDAL configuration".to_string(),
+        )
+    })?;
+    global_builder.replace(builder);
+    Ok(())
+}
+
+/// Return whether the global [`GdalApi`] has been initialized.
+///
+/// This returns `true` only after [`get_global_gdal_api`] (directly or via
+/// [`with_global_gdal_api`]) has successfully initialized and stored the API.
+/// It does not indicate whether a builder was previously set through
+/// [`configure_global_gdal_api`].
+pub fn is_gdal_api_configured() -> bool {
+    GDAL_API.get().is_some()
+}
+
+/// Execute a closure with the process-global [`GdalApi`].
+///
+/// This helper ensures the global API is initialized (lazily) and then passes 
a
+/// shared `'static` reference to the provided closure.
+///
+/// If initialization succeeds, the closure's result is returned unchanged; 
otherwise
+/// returns an error from the initialization attempt.
+pub fn with_global_gdal_api<F, R>(func: F) -> Result<R, GdalInitLibraryError>
+where
+    F: FnOnce(&'static GdalApi) -> R,
+{
+    let api = get_global_gdal_api()?;
+    Ok(func(api))
+}
+
+/// Verify that the GDAL library meets the minimum version requirement.
+///
+/// We use `GDALVersionInfo("VERSION_NUM")` instead of `GDALCheckVersion` 
because
+/// the latter performs an **exact** major.minor match and rejects newer 
versions
+/// (e.g. GDAL 3.12 fails a check for 3.4), whereas we need a **minimum** 
version
+/// check (>=).
+fn check_gdal_version(
+    mut gdal_version_info: impl FnMut(&str) -> String,
+) -> Result<(), GdalInitLibraryError> {
+    let version_str = gdal_version_info("VERSION_NUM");
+    let version_num: i32 = version_str.trim().parse().map_err(|e| {
+        GdalInitLibraryError::LibraryError(format!(
+            "Failed to parse GDAL version number {:?}: {e}",
+            version_str
+        ))
+    })?;
+
+    if version_num < MIN_GDAL_VERSION_NUM {
+        // Get the human-readable release name for the error message.
+        let release_name = gdal_version_info("RELEASE_NAME");
+        return Err(GdalInitLibraryError::LibraryError(format!(
+            "GDAL >= {MIN_GDAL_VERSION_MAJOR}.{MIN_GDAL_VERSION_MINOR} 
required \
+             for sedona-gdal (found {release_name})"
+        )));
+    }
+
+    Ok(())
+}
+
+#[cfg(test)]
+mod test {
+    use super::*;
+
+    /// Building with default options (no shared library) should succeed when
+    /// `gdal-sys` is linked, loading symbols from the current process.
+    #[test]
+    fn test_builder_default() {
+        let api = GdalApiBuilder::default()
+            .build()
+            .expect("GdalApiBuilder::default().build() should succeed with 
gdal-sys");
+        assert_eq!(api.name(), "current_process");
+    }
+
+    /// Building with an explicit shared library path should use that path.
+    /// Gated behind an environment variable; skips gracefully if unset.
+    #[test]
+    fn test_builder_with_shared_library() {
+        if let Ok(gdal_library) = 
std::env::var("SEDONA_GDAL_TEST_SHARED_LIBRARY") {
+            if !gdal_library.is_empty() {
+                let api = GdalApiBuilder::default()
+                    .with_shared_library(gdal_library.clone().into())
+                    .build()
+                    .expect("Should build GdalApi from 
SEDONA_GDAL_TEST_SHARED_LIBRARY");
+                assert_eq!(api.name(), gdal_library);
+                return;
+            }
+        }
+
+        println!(
+            "Skipping test_builder_with_shared_library - \
+             SEDONA_GDAL_TEST_SHARED_LIBRARY environment variable not set"
+        );
+    }
+
+    /// Building with an invalid shared library path should return an error.
+    #[test]
+    fn test_builder_invalid_path() {
+        let err = GdalApiBuilder::default()
+            .with_shared_library("/nonexistent/libgdal.so".into())
+            .build()
+            .unwrap_err();
+        assert!(
+            matches!(err, GdalInitLibraryError::LibraryError(_)),
+            "Expected LibraryError, got: {err:?}"
+        );
+    }
+
+    /// `get_global_gdal_api` should succeed and return a valid API reference.
+    ///
+    /// Note: this test touches the process-global `OnceLock` and cannot be
+    /// "undone", so it effectively tests the first-call initialization path.
+    /// Subsequent tests in the same process will hit the fast path.
+    #[test]
+    fn test_get_global_gdal_api() {
+        let api = get_global_gdal_api().expect("get_global_gdal_api should 
succeed");
+        assert!(!api.name().is_empty(), "API name should not be empty");
+    }
+
+    /// After `get_global_gdal_api` succeeds, `is_gdal_api_configured` should
+    /// return true.
+    #[test]
+    fn test_is_gdal_api_configured() {
+        // Ensure the API is initialized.
+        let _ = get_global_gdal_api().expect("get_global_gdal_api should 
succeed");
+        assert!(
+            is_gdal_api_configured(),
+            "is_gdal_api_configured should return true after initialization"
+        );
+    }
+
+    /// `with_global_gdal_api` should pass a valid reference to the closure.
+    #[test]
+    fn test_with_global_gdal_api() {
+        let name = with_global_gdal_api(|api| api.name().to_string())
+            .expect("with_global_gdal_api should succeed");
+        assert!(!name.is_empty(), "API name should not be empty");
+    }
+}
diff --git a/c/sedona-gdal/src/lib.rs b/c/sedona-gdal/src/lib.rs
new file mode 100644
index 00000000..e05a3bd0
--- /dev/null
+++ b/c/sedona-gdal/src/lib.rs
@@ -0,0 +1,27 @@
+// 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.
+
+// --- FFI layer ---
+pub(crate) mod dyn_load;
+pub mod gdal_dyn_bindgen;
+
+// --- Error types ---
+pub mod errors;
+
+// --- Core API ---
+pub mod gdal_api;
+pub mod global;
diff --git a/docs/contributors-guide.md b/docs/contributors-guide.md
index 1af286ab..f215fc80 100644
--- a/docs/contributors-guide.md
+++ b/docs/contributors-guide.md
@@ -66,12 +66,16 @@ Your first step is to create a personal copy of the 
repository and connect it to
         upstream  https://github.com/apache/sedona-db.git (fetch)
         upstream  https://github.com/apache/sedona-db.git (push)
         ```
+
 ## System dependencies
 
-Some crates in the workspace wrap native libraries and require system 
dependencies (GEOS, PROJ, Abseil, OpenSSL, CMake, etc.). We recommend using:
+Some crates in the workspace wrap native libraries and require system 
dependencies (GEOS, GDAL, PROJ, Abseil, OpenSSL, CMake, etc.). We recommend 
using:
 
 ### macOS: Homebrew
-``` bash brew install abseil openssl cmake geos proj ```
+
+```bash
+brew install abseil openssl cmake geos gdal proj
+```
 
 Ensure Homebrew-installed tools are on your PATH (Homebrew usually does this 
automatically).
 
@@ -110,7 +114,7 @@ cd C:\dev\vcpkg
 Next, install the required libraries with vcpkg:
 
 ```powershell
-C:\dev\vcpkg\vcpkg.exe install geos proj abseil openssl
+C:\dev\vcpkg\vcpkg.exe install geos gdal proj abseil openssl
 ```
 
 Configure environment variables (PowerShell example — update paths as needed):
@@ -153,7 +157,7 @@ Linux users may install system dependencies from a system 
package manager. Note
 Ubuntu/Debian (Ubuntu 24.04 LTS is too old; however, later versions have the 
required version of Abseil)
 
 ```shell
-sudo apt-get install -y build-essential cmake libssl-dev libproj-dev 
libgeos-dev python3-dev libabsl-dev
+sudo apt-get install -y build-essential cmake libssl-dev libproj-dev 
libgeos-dev libgdal-dev python3-dev libabsl-dev
 ```
 
 ## Rust
diff --git a/rust/sedona/Cargo.toml b/rust/sedona/Cargo.toml
index 0c6fb4c9..051c7841 100644
--- a/rust/sedona/Cargo.toml
+++ b/rust/sedona/Cargo.toml
@@ -41,6 +41,7 @@ tg = ["dep:sedona-tg"]
 http = ["object_store/http"]
 pointcloud = ["dep:sedona-pointcloud"]
 proj = ["sedona-proj/proj-sys"]
+gdal = ["sedona-gdal/gdal-sys"]
 spatial-join = ["dep:sedona-spatial-join"]
 s2geography = ["dep:sedona-s2geography"]
 
@@ -77,6 +78,7 @@ sedona-geoparquet = { workspace = true }
 sedona-geos = { workspace = true, optional = true }
 sedona-pointcloud = { workspace = true, optional = true }
 sedona-proj = { workspace = true }
+sedona-gdal = { workspace = true }
 sedona-raster-functions = { workspace = true }
 sedona-schema = { workspace = true }
 sedona-spatial-join = { workspace = true, optional = true }

Reply via email to