Skip to content

Commit a875098

Browse files
refactor(c/sedona-proj): replace C dynamic loading with pure Rust using libloading (#672)
## Summary Replace the C-based `dlopen`/`dlsym` dynamic PROJ loading (`proj_dyn.c`/`proj_dyn.h`) with a pure Rust implementation using the `libloading` crate. This eliminates ~250 lines of C code while maintaining the same `ProjApi` struct-of-function-pointers architecture used by all existing call sites. ## Approach The `libloading` crate provides safe, cross-platform dynamic library loading (`dlopen`/`LoadLibrary`). Each PROJ symbol is loaded via a `load_fn!` macro that: 1. Loads the symbol as a raw `*const ()` pointer 2. Transmutes it to the expected function pointer type 3. Stores it in the existing `ProjApi` `#[repr(C)]` struct The `Library` handle is stored as `_lib: Option<Library>` in the Rust `ProjApi` wrapper — `Some` when loaded from a shared library, `None` when using `proj-sys`. This ensures the library stays loaded for the lifetime of the function pointers. ## Tests Temporarily enabled python-wheels test and passed on all major platforms: https://github.com/apache/sedona-db/actions/runs/22518558723 ## Upcoming Changes We'll implement c/sedona-gdal using similar approach. Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 18d876b commit a875098

10 files changed

Lines changed: 127 additions & 321 deletions

File tree

Cargo.lock

Lines changed: 11 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ geos = { version = "11.0.1", features = ["geo", "v3_12_0"] }
105105
glam = "0.32.0"
106106
libmimalloc-sys = { version = "0.1", default-features = false }
107107
log = "^0.4"
108+
libloading = "0.9"
108109
lru = "0.16"
109110
mimalloc = { version = "0.1", default-features = false }
110111
num-traits = { version = "0.2", default-features = false, features = ["libm"] }

c/sedona-proj/Cargo.toml

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,6 @@ readme.workspace = true
2828
edition.workspace = true
2929
rust-version.workspace = true
3030

31-
[build-dependencies]
32-
cc = { version = "1" }
33-
3431
[dev-dependencies]
3532
approx = { workspace = true }
3633
geo-types = { workspace = true }
@@ -44,6 +41,7 @@ default = [ "proj-sys" ]
4441
proj-sys = [ "dep:proj-sys" ]
4542

4643
[dependencies]
44+
libloading = { workspace = true }
4745
serde_json = { workspace = true }
4846
arrow-schema = { workspace = true }
4947
arrow-array = { workspace = true }

c/sedona-proj/build.rs

Lines changed: 0 additions & 22 deletions
This file was deleted.

c/sedona-proj/src/dyn_load.rs

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
// Licensed to the Apache Software Foundation (ASF) under one
2+
// or more contributor license agreements. See the NOTICE file
3+
// distributed with this work for additional information
4+
// regarding copyright ownership. The ASF licenses this file
5+
// to you under the Apache License, Version 2.0 (the
6+
// "License"); you may not use this file except in compliance
7+
// with the License. You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing,
12+
// software distributed under the License is distributed on an
13+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
// KIND, either express or implied. See the License for the
15+
// specific language governing permissions and limitations
16+
// under the License.
17+
18+
use std::path::Path;
19+
20+
use libloading::Library;
21+
22+
use crate::error::SedonaProjError;
23+
use crate::proj_dyn_bindgen::ProjApi;
24+
25+
/// Load a single symbol from the library and write it into the given field.
26+
///
27+
/// We load as a raw `*const ()` pointer and transmute to the target function pointer
28+
/// type. This is the standard pattern for dynamic symbol loading where the loaded
29+
/// symbol's signature is known but cannot be expressed as a generic parameter to
30+
/// `Library::get` (because each field has a different signature).
31+
///
32+
/// On failure returns a `SedonaProjError` with the symbol name and the
33+
/// underlying OS error message.
34+
macro_rules! load_fn {
35+
($lib:expr, $api:expr, $name:ident) => {
36+
// The target types here are too verbose to annotate for each call site
37+
#[allow(clippy::missing_transmute_annotations)]
38+
{
39+
$api.$name = Some(unsafe {
40+
let sym = $lib
41+
.get::<*const ()>(concat!(stringify!($name), "\0").as_bytes())
42+
.map_err(|e| {
43+
SedonaProjError::LibraryError(format!(
44+
"Failed to load symbol {}: {}",
45+
stringify!($name),
46+
e
47+
))
48+
})?;
49+
std::mem::transmute(sym.into_raw().into_raw())
50+
});
51+
}
52+
};
53+
}
54+
55+
/// Populate all 21 function-pointer fields of [`ProjApi`] from the given
56+
/// [`Library`] handle.
57+
fn load_all_symbols(lib: &Library, api: &mut ProjApi) -> Result<(), SedonaProjError> {
58+
load_fn!(lib, api, proj_area_create);
59+
load_fn!(lib, api, proj_area_destroy);
60+
load_fn!(lib, api, proj_area_set_bbox);
61+
load_fn!(lib, api, proj_context_create);
62+
load_fn!(lib, api, proj_context_destroy);
63+
load_fn!(lib, api, proj_context_errno_string);
64+
load_fn!(lib, api, proj_context_errno);
65+
load_fn!(lib, api, proj_context_set_database_path);
66+
load_fn!(lib, api, proj_context_set_search_paths);
67+
load_fn!(lib, api, proj_create_crs_to_crs_from_pj);
68+
load_fn!(lib, api, proj_create);
69+
load_fn!(lib, api, proj_cs_get_axis_count);
70+
load_fn!(lib, api, proj_destroy);
71+
load_fn!(lib, api, proj_errno_reset);
72+
load_fn!(lib, api, proj_errno);
73+
load_fn!(lib, api, proj_info);
74+
load_fn!(lib, api, proj_log_level);
75+
load_fn!(lib, api, proj_normalize_for_visualization);
76+
load_fn!(lib, api, proj_trans);
77+
load_fn!(lib, api, proj_trans_array);
78+
load_fn!(lib, api, proj_as_projjson);
79+
80+
Ok(())
81+
}
82+
83+
/// Load a PROJ shared library from `path` and populate a [`ProjApi`] struct.
84+
///
85+
/// Returns the `(Library, ProjApi)` pair. The caller is responsible for
86+
/// keeping the `Library` alive for the lifetime of the function pointers.
87+
pub(crate) fn load_proj_from_path(path: &Path) -> Result<(Library, ProjApi), SedonaProjError> {
88+
let lib = unsafe { Library::new(path.as_os_str()) }.map_err(|e| {
89+
SedonaProjError::LibraryError(format!(
90+
"Failed to load PROJ library from {}: {}",
91+
path.display(),
92+
e
93+
))
94+
})?;
95+
96+
let mut api = ProjApi::default();
97+
load_all_symbols(&lib, &mut api)?;
98+
Ok((lib, api))
99+
}

c/sedona-proj/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
// KIND, either express or implied. See the License for the
1515
// specific language governing permissions and limitations
1616
// under the License.
17+
mod dyn_load;
1718
pub mod error;
1819
mod proj;
1920
mod proj_dyn_bindgen;

c/sedona-proj/src/proj.rs

Lines changed: 13 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,9 @@ use std::{
4242
sync::Arc,
4343
};
4444

45-
use crate::{error::SedonaProjError, proj_dyn_bindgen};
45+
use libloading::Library;
46+
47+
use crate::{dyn_load, error::SedonaProjError, proj_dyn_bindgen};
4648

4749
/// A macro to safely call a function pointer from a ProjApi
4850
///
@@ -449,24 +451,16 @@ impl Proj {
449451
/// loaded using C code; however, this could be migrated to Rust which also
450452
/// provides dynamic library loading capabilities.
451453
///
452-
/// This API is thread safe and is marked as such; however, clients must not
453-
/// call the inner release callback. Doing so will set function pointers to
454-
/// null, which will cause subsequent calls to panic.
455-
#[derive(Default)]
454+
/// This API is thread safe and is marked as such. When loading PROJ from a
455+
/// shared library, the `_lib` field holds the `Library` handle, ensuring that
456+
/// the underlying library and its function pointers remain valid for the
457+
/// lifetime of this `ProjApi` instance.
456458
struct ProjApi {
457459
inner: proj_dyn_bindgen::ProjApi,
458460
name: String,
459-
}
460-
461-
unsafe impl Send for ProjApi {}
462-
unsafe impl Sync for ProjApi {}
463-
464-
impl Drop for ProjApi {
465-
fn drop(&mut self) {
466-
if let Some(releaser) = self.inner.release {
467-
unsafe { releaser(&mut self.inner) }
468-
}
469-
}
461+
/// Keep the dynamically loaded library alive for the lifetime of the function pointers.
462+
/// `None` when using `proj-sys` (statically linked), `Some` when loaded from a shared library.
463+
_lib: Option<Library>,
470464
}
471465

472466
impl Debug for ProjApi {
@@ -477,31 +471,12 @@ impl Debug for ProjApi {
477471

478472
impl ProjApi {
479473
fn try_from_shared_library(shared_library: PathBuf) -> Result<Arc<Self>, SedonaProjError> {
480-
let mut inner = proj_dyn_bindgen::ProjApi::default();
481-
let mut err_message = (0..1024).map(|_| 0).collect::<Vec<u8>>();
482-
let shared_library_c = CString::new(shared_library.to_string_lossy().to_string())
483-
.map_err(|_| SedonaProjError::Invalid("embedded nul in Rust string".to_string()))?;
484-
485-
let err = unsafe {
486-
proj_dyn_bindgen::proj_dyn_api_init(
487-
&mut inner as _,
488-
shared_library_c.as_ptr(),
489-
err_message.as_mut_ptr() as _,
490-
err_message.len().try_into().unwrap(),
491-
)
492-
};
493-
494-
let c_err_message = CStr::from_bytes_until_nul(&err_message)
495-
.map_err(|_| SedonaProjError::Invalid("embedded nul in C string".to_string()))?;
496-
if err != 0 {
497-
return Err(SedonaProjError::LibraryError(
498-
c_err_message.to_string_lossy().to_string(),
499-
));
500-
}
474+
let (lib, inner) = dyn_load::load_proj_from_path(&shared_library)?;
501475

502476
Ok(Arc::new(Self {
503477
inner,
504478
name: shared_library.to_string_lossy().to_string(),
479+
_lib: Some(lib),
505480
}))
506481
}
507482

@@ -624,6 +599,7 @@ impl ProjApi {
624599
Self {
625600
inner,
626601
name: "proj_sys".to_string(),
602+
_lib: None,
627603
}
628604
}
629605
}

0 commit comments

Comments
 (0)