diff --git a/Cargo.lock b/Cargo.lock index 158086b4e..6088108b7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3700,6 +3700,16 @@ dependencies = [ "windows-link", ] +[[package]] +name = "libloading" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "754ca22de805bb5744484a5b151a9e1a8e837d5dc232c2d7d8c2e3492edc8b60" +dependencies = [ + "cfg-if", + "windows-link", +] + [[package]] name = "libm" version = "0.2.15" @@ -5464,12 +5474,12 @@ dependencies = [ "approx", "arrow-array", "arrow-schema", - "cc", "criterion", "datafusion-common", "datafusion-expr", "geo-traits", "geo-types", + "libloading 0.9.0", "proj-sys", "rstest", "sedona-common", diff --git a/Cargo.toml b/Cargo.toml index 9ba325743..abe66917b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -105,6 +105,7 @@ geos = { version = "11.0.1", features = ["geo", "v3_12_0"] } glam = "0.32.0" libmimalloc-sys = { version = "0.1", default-features = false } log = "^0.4" +libloading = "0.9" lru = "0.16" mimalloc = { version = "0.1", default-features = false } num-traits = { version = "0.2", default-features = false, features = ["libm"] } diff --git a/c/sedona-proj/Cargo.toml b/c/sedona-proj/Cargo.toml index 0a145fd17..072f15bbc 100644 --- a/c/sedona-proj/Cargo.toml +++ b/c/sedona-proj/Cargo.toml @@ -28,9 +28,6 @@ readme.workspace = true edition.workspace = true rust-version.workspace = true -[build-dependencies] -cc = { version = "1" } - [dev-dependencies] approx = { workspace = true } geo-types = { workspace = true } @@ -44,6 +41,7 @@ default = [ "proj-sys" ] proj-sys = [ "dep:proj-sys" ] [dependencies] +libloading = { workspace = true } serde_json = { workspace = true } arrow-schema = { workspace = true } arrow-array = { workspace = true } diff --git a/c/sedona-proj/build.rs b/c/sedona-proj/build.rs deleted file mode 100644 index f62fd8a61..000000000 --- a/c/sedona-proj/build.rs +++ /dev/null @@ -1,22 +0,0 @@ -// 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. - -fn main() { - println!("cargo:rerun-if-changed=src/proj_dyn.c"); - - cc::Build::new().file("src/proj_dyn.c").compile("proj_dyn"); -} diff --git a/c/sedona-proj/src/dyn_load.rs b/c/sedona-proj/src/dyn_load.rs new file mode 100644 index 000000000..ee7f6495c --- /dev/null +++ b/c/sedona-proj/src/dyn_load.rs @@ -0,0 +1,99 @@ +// 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::error::SedonaProjError; +use crate::proj_dyn_bindgen::ProjApi; + +/// 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 `SedonaProjError` 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| { + SedonaProjError::LibraryError(format!( + "Failed to load symbol {}: {}", + stringify!($name), + e + )) + })?; + std::mem::transmute(sym.into_raw().into_raw()) + }); + } + }; +} + +/// Populate all 21 function-pointer fields of [`ProjApi`] from the given +/// [`Library`] handle. +fn load_all_symbols(lib: &Library, api: &mut ProjApi) -> Result<(), SedonaProjError> { + load_fn!(lib, api, proj_area_create); + load_fn!(lib, api, proj_area_destroy); + load_fn!(lib, api, proj_area_set_bbox); + load_fn!(lib, api, proj_context_create); + load_fn!(lib, api, proj_context_destroy); + load_fn!(lib, api, proj_context_errno_string); + load_fn!(lib, api, proj_context_errno); + load_fn!(lib, api, proj_context_set_database_path); + load_fn!(lib, api, proj_context_set_search_paths); + load_fn!(lib, api, proj_create_crs_to_crs_from_pj); + load_fn!(lib, api, proj_create); + load_fn!(lib, api, proj_cs_get_axis_count); + load_fn!(lib, api, proj_destroy); + load_fn!(lib, api, proj_errno_reset); + load_fn!(lib, api, proj_errno); + load_fn!(lib, api, proj_info); + load_fn!(lib, api, proj_log_level); + load_fn!(lib, api, proj_normalize_for_visualization); + load_fn!(lib, api, proj_trans); + load_fn!(lib, api, proj_trans_array); + load_fn!(lib, api, proj_as_projjson); + + Ok(()) +} + +/// Load a PROJ shared library from `path` and populate a [`ProjApi`] struct. +/// +/// Returns the `(Library, ProjApi)` pair. The caller is responsible for +/// keeping the `Library` alive for the lifetime of the function pointers. +pub(crate) fn load_proj_from_path(path: &Path) -> Result<(Library, ProjApi), SedonaProjError> { + let lib = unsafe { Library::new(path.as_os_str()) }.map_err(|e| { + SedonaProjError::LibraryError(format!( + "Failed to load PROJ library from {}: {}", + path.display(), + e + )) + })?; + + let mut api = ProjApi::default(); + load_all_symbols(&lib, &mut api)?; + Ok((lib, api)) +} diff --git a/c/sedona-proj/src/lib.rs b/c/sedona-proj/src/lib.rs index 6cc48eabf..3cffe6fec 100644 --- a/c/sedona-proj/src/lib.rs +++ b/c/sedona-proj/src/lib.rs @@ -14,6 +14,7 @@ // KIND, either express or implied. See the License for the // specific language governing permissions and limitations // under the License. +mod dyn_load; pub mod error; mod proj; mod proj_dyn_bindgen; diff --git a/c/sedona-proj/src/proj.rs b/c/sedona-proj/src/proj.rs index e52eb2954..b267af419 100644 --- a/c/sedona-proj/src/proj.rs +++ b/c/sedona-proj/src/proj.rs @@ -42,7 +42,9 @@ use std::{ sync::Arc, }; -use crate::{error::SedonaProjError, proj_dyn_bindgen}; +use libloading::Library; + +use crate::{dyn_load, error::SedonaProjError, proj_dyn_bindgen}; /// A macro to safely call a function pointer from a ProjApi /// @@ -449,24 +451,16 @@ impl Proj { /// loaded using C code; however, this could be migrated to Rust which also /// provides dynamic library loading capabilities. /// -/// This API is thread safe and is marked as such; however, clients must not -/// call the inner release callback. Doing so will set function pointers to -/// null, which will cause subsequent calls to panic. -#[derive(Default)] +/// This API is thread safe and is marked as such. When loading PROJ from a +/// shared library, the `_lib` field holds the `Library` handle, ensuring that +/// the underlying library and its function pointers remain valid for the +/// lifetime of this `ProjApi` instance. struct ProjApi { inner: proj_dyn_bindgen::ProjApi, name: String, -} - -unsafe impl Send for ProjApi {} -unsafe impl Sync for ProjApi {} - -impl Drop for ProjApi { - fn drop(&mut self) { - if let Some(releaser) = self.inner.release { - unsafe { releaser(&mut self.inner) } - } - } + /// Keep the dynamically loaded library alive for the lifetime of the function pointers. + /// `None` when using `proj-sys` (statically linked), `Some` when loaded from a shared library. + _lib: Option, } impl Debug for ProjApi { @@ -477,31 +471,12 @@ impl Debug for ProjApi { impl ProjApi { fn try_from_shared_library(shared_library: PathBuf) -> Result, SedonaProjError> { - let mut inner = proj_dyn_bindgen::ProjApi::default(); - let mut err_message = (0..1024).map(|_| 0).collect::>(); - let shared_library_c = CString::new(shared_library.to_string_lossy().to_string()) - .map_err(|_| SedonaProjError::Invalid("embedded nul in Rust string".to_string()))?; - - let err = unsafe { - proj_dyn_bindgen::proj_dyn_api_init( - &mut inner as _, - shared_library_c.as_ptr(), - err_message.as_mut_ptr() as _, - err_message.len().try_into().unwrap(), - ) - }; - - let c_err_message = CStr::from_bytes_until_nul(&err_message) - .map_err(|_| SedonaProjError::Invalid("embedded nul in C string".to_string()))?; - if err != 0 { - return Err(SedonaProjError::LibraryError( - c_err_message.to_string_lossy().to_string(), - )); - } + let (lib, inner) = dyn_load::load_proj_from_path(&shared_library)?; Ok(Arc::new(Self { inner, name: shared_library.to_string_lossy().to_string(), + _lib: Some(lib), })) } @@ -624,6 +599,7 @@ impl ProjApi { Self { inner, name: "proj_sys".to_string(), + _lib: None, } } } diff --git a/c/sedona-proj/src/proj_dyn.c b/c/sedona-proj/src/proj_dyn.c deleted file mode 100644 index 69c54217e..000000000 --- a/c/sedona-proj/src/proj_dyn.c +++ /dev/null @@ -1,150 +0,0 @@ -// 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. - -#include "proj_dyn.h" - -// Original source: -// https://github.com/apache/sedona/blob/670bb4c4a6fea49f0b0159ebdf2a92f00d3ed07a/python/src/geos_c_dyn.c - -#if defined(_WIN32) -#define TARGETING_WINDOWS -#include -#include -#else -#include -#include -#endif - -#include -#include -#include - -#ifdef TARGETING_WINDOWS -static void win32_get_last_error(char* err_msg, int len) { - wchar_t info[256]; - unsigned int error_code = GetLastError(); - int info_length = FormatMessageW( - FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, /* flags */ - NULL, /* message source*/ - error_code, /* the message (error) ID */ - MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), /* default language */ - info, /* the buffer */ - sizeof(info) / sizeof(wchar_t), /* size in wchars */ - NULL); - int num_bytes = - WideCharToMultiByte(CP_UTF8, 0, info, info_length, err_msg, len, NULL, NULL); - num_bytes = (num_bytes < (len - 1)) ? num_bytes : (len - 1); - err_msg[num_bytes] = '\0'; -} -#endif - -static void* try_load_proj_symbol(void* handle, const char* func_name) { -#ifndef TARGETING_WINDOWS - return dlsym(handle, func_name); -#else - return GetProcAddress((HMODULE)handle, func_name); -#endif -} - -static int load_proj_symbol(void* handle, const char* func_name, void** p_func, - char* err_msg, int len) { - void* func = try_load_proj_symbol(handle, func_name); - if (func == NULL) { -#ifndef TARGETING_WINDOWS - snprintf(err_msg, len, "%s", dlerror()); -#else - win32_get_last_error(err_msg, len); -#endif - return -1; - } - *p_func = func; - return 0; -} - -#define LOAD_PROJ_FUNCTION(api, func) \ - if (load_proj_symbol(handle, #func, (void**)(&(api)->func), err_msg, len) != 0) { \ - proj_dyn_release_api(api); \ - return -1; \ - } - -static void proj_dyn_release_api(struct ProjApi* api) { -#ifdef TARGETING_WINDOWS - FreeLibrary((HMODULE)api->private_data); -#else - dlclose(api->private_data); -#endif - memset(api, 0, sizeof(struct ProjApi)); -} - -static int load_proj_from_handle(struct ProjApi* api, void* handle, char* err_msg, - int len) { - LOAD_PROJ_FUNCTION(api, proj_area_create); - LOAD_PROJ_FUNCTION(api, proj_area_destroy); - LOAD_PROJ_FUNCTION(api, proj_area_set_bbox); - LOAD_PROJ_FUNCTION(api, proj_context_create); - LOAD_PROJ_FUNCTION(api, proj_context_destroy); - LOAD_PROJ_FUNCTION(api, proj_context_errno_string); - LOAD_PROJ_FUNCTION(api, proj_context_errno); - LOAD_PROJ_FUNCTION(api, proj_context_set_database_path); - LOAD_PROJ_FUNCTION(api, proj_context_set_search_paths); - LOAD_PROJ_FUNCTION(api, proj_create_crs_to_crs_from_pj); - LOAD_PROJ_FUNCTION(api, proj_create); - LOAD_PROJ_FUNCTION(api, proj_cs_get_axis_count); - LOAD_PROJ_FUNCTION(api, proj_destroy); - LOAD_PROJ_FUNCTION(api, proj_errno_reset); - LOAD_PROJ_FUNCTION(api, proj_errno); - LOAD_PROJ_FUNCTION(api, proj_info); - LOAD_PROJ_FUNCTION(api, proj_log_level); - LOAD_PROJ_FUNCTION(api, proj_normalize_for_visualization); - LOAD_PROJ_FUNCTION(api, proj_trans); - LOAD_PROJ_FUNCTION(api, proj_trans_array); - LOAD_PROJ_FUNCTION(api, proj_as_projjson); - - api->release = &proj_dyn_release_api; - api->private_data = handle; - - return 0; -} - -#undef LOAD_PROJ_FUNCTION - -int proj_dyn_api_init(struct ProjApi* api, const char* shared_object_path, char* err_msg, - int len) { -#ifndef TARGETING_WINDOWS - void* handle = dlopen(shared_object_path, RTLD_LOCAL | RTLD_NOW); - if (handle == NULL) { - snprintf(err_msg, len, "%s", dlerror()); - return -1; - } -#else - int num_chars = MultiByteToWideChar(CP_UTF8, 0, shared_object_path, -1, NULL, 0); - wchar_t* wpath = calloc(num_chars, sizeof(wchar_t)); - if (wpath == NULL) { - snprintf(err_msg, len, "%s", "Cannot allocate memory for wpath"); - return -1; - } - MultiByteToWideChar(CP_UTF8, 0, shared_object_path, -1, wpath, num_chars); - HMODULE module = LoadLibraryW(wpath); - free(wpath); - if (module == NULL) { - win32_get_last_error(err_msg, len); - return -1; - } - void* handle = module; -#endif - return load_proj_from_handle(api, handle, err_msg, len); -} diff --git a/c/sedona-proj/src/proj_dyn.h b/c/sedona-proj/src/proj_dyn.h deleted file mode 100644 index 81e805036..000000000 --- a/c/sedona-proj/src/proj_dyn.h +++ /dev/null @@ -1,97 +0,0 @@ -// 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. - -#include - -#ifndef PROJ_DYN_H_INCLUDED -#define PROJ_DYN_H_INCLUDED - -// Type definitions copied from proj.h -struct pj_ctx; -typedef struct pj_ctx PJ_CONTEXT; - -struct PJ_AREA; -typedef struct PJ_AREA PJ_AREA; - -struct PJconsts; -typedef struct PJconsts PJ; - -typedef struct { - double x, y, z, t; -} PJ_XYZT; - -typedef union { - double v[4]; - PJ_XYZT xyzt; -} PJ_COORD; - -typedef enum { PJ_FWD = 1, PJ_IDENT = 0, PJ_INV = -1 } PJ_DIRECTION; - -typedef enum PJ_LOG_LEVEL { - PJ_LOG_NONE = 0, - PJ_LOG_ERROR = 1, - PJ_LOG_DEBUG = 2, - PJ_LOG_TRACE = 3, - PJ_LOG_TELL = 4, -} PJ_LOG_LEVEL; - -typedef struct { - int major; - int minor; - int patch; - const char* release; - const char* version; - const char* searchpath; -} PJ_INFO; - -struct ProjApi { - PJ_AREA* (*proj_area_create)(void); - void (*proj_area_destroy)(PJ_AREA* area); - int (*proj_area_set_bbox)(PJ_AREA* area, double west_lon_degree, - double south_lat_degree, double east_lon_degree, - double north_lat_degree); - PJ_CONTEXT* (*proj_context_create)(void); - void (*proj_context_destroy)(PJ_CONTEXT* ctx); - int (*proj_context_errno)(PJ_CONTEXT* ctx); - const char* (*proj_context_errno_string)(PJ_CONTEXT* ctx, int err); - int (*proj_context_set_database_path)(PJ_CONTEXT* ctx, const char* dbPath, - const char* const* auxDbPaths, - const char* const* options); - void (*proj_context_set_search_paths)(PJ_CONTEXT* ctx, int count_paths, - const char* const* paths); - PJ* (*proj_create)(PJ_CONTEXT* ctx, const char* definition); - PJ* (*proj_create_crs_to_crs_from_pj)(PJ_CONTEXT* ctx, PJ* source_crs, PJ* target_crs, - PJ_AREA* area, const char* const* options); - int (*proj_cs_get_axis_count)(PJ_CONTEXT* ctx, const PJ* cs); - void (*proj_destroy)(PJ* P); - int (*proj_errno)(const PJ* P); - void (*proj_errno_reset)(PJ* P); - PJ_INFO (*proj_info)(void); - PJ_LOG_LEVEL (*proj_log_level)(PJ_CONTEXT* ctx, PJ_LOG_LEVEL level); - PJ* (*proj_normalize_for_visualization)(PJ_CONTEXT* ctx, const PJ* obj); - PJ_COORD (*proj_trans)(PJ* P, PJ_DIRECTION direction, PJ_COORD coord); - PJ_COORD (*proj_trans_array)(PJ* P, PJ_DIRECTION direction, size_t n, PJ_COORD* coord); - const char* (*proj_as_projjson)(PJ_CONTEXT* ctx, const PJ* obj, - const char* const* options); - void (*release)(struct ProjApi*); - void* private_data; -}; - -int proj_dyn_api_init(struct ProjApi* api, const char* shared_object_path, char* err_msg, - int len); - -#endif diff --git a/c/sedona-proj/src/proj_dyn_bindgen.rs b/c/sedona-proj/src/proj_dyn_bindgen.rs index 55ea2d9ae..3e5c41b95 100644 --- a/c/sedona-proj/src/proj_dyn_bindgen.rs +++ b/c/sedona-proj/src/proj_dyn_bindgen.rs @@ -18,7 +18,7 @@ #![allow(non_snake_case)] #![allow(dead_code)] -use std::os::raw::{c_char, c_int, c_uint, c_void}; +use std::os::raw::{c_char, c_int, c_uint}; #[repr(C)] #[derive(Debug, Copy, Clone)] @@ -153,14 +153,4 @@ pub struct ProjApi { ) -> *const c_char, >, pub release: Option, - pub private_data: *mut c_void, -} - -unsafe extern "C" { - pub fn proj_dyn_api_init( - api: *mut ProjApi, - shared_object_path: *const c_char, - err_msg: *mut c_char, - len: c_int, - ) -> c_int; }