Skip to content

Commit c0ae9ce

Browse files
committed
Refactor Proj context handling behind a Context type
Introduce an internal Context enum (Owned/Shared) plus a reference-counted SharedContext and a per-thread SHARED_CONTEXT thread-local. Proj.ctx becomes a Context; helpers take it by value and Context's Drop destroys Owned contexts (replacing the explicit proj_context_destroy in Proj::drop). Move ctx() out of define_info_methods! into per-type accessors. Note: every constructor still produces Context::Owned. The Shared variant and thread_local_context() are wired up in the next commit: the work is split in order to allow more precise identification of leaks / memory corruption Signed-off-by: Stephan Hügel <shugel@tcd.ie>
1 parent b27d79c commit c0ae9ce

1 file changed

Lines changed: 115 additions & 37 deletions

File tree

src/proj.rs

Lines changed: 115 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ use std::ffi::CStr;
3131
use std::ffi::CString;
3232
use std::mem::MaybeUninit;
3333
use std::path::Path;
34+
use std::rc::Rc;
3435
use thiserror::Error;
3536

3637
pub trait CoordinateType: Float + Copy + PartialOrd + Debug {}
@@ -207,11 +208,75 @@ fn area_set_bbox(parea: *mut proj_sys::PJ_AREA, new_area: Option<Area>) {
207208
}
208209
}
209210

211+
/// A PROJ context (`PJ_CONTEXT`) owned or borrowed by a [`Proj`].
212+
///
213+
/// `Owned` contexts are created (or cloned) for a single `Proj` and destroyed when it is dropped.
214+
/// `Shared` contexts are the per-thread context returned by [`thread_local_context`]; the context
215+
/// is reference counted so that it outlives every `Proj` that uses it, regardless of the order in
216+
/// which the thread-local and any surviving `Proj` instances are dropped at thread exit.
217+
enum Context {
218+
Owned(*mut PJ_CONTEXT),
219+
// Wired into Proj::new/new_known_crs in a follow-up commit.
220+
#[allow(dead_code)]
221+
Shared(Rc<SharedContext>),
222+
}
223+
224+
impl Context {
225+
fn as_ptr(&self) -> *mut PJ_CONTEXT {
226+
match self {
227+
Context::Owned(ctx) => *ctx,
228+
Context::Shared(shared) => shared.ctx,
229+
}
230+
}
231+
}
232+
233+
impl Drop for Context {
234+
fn drop(&mut self) {
235+
// `Owned` contexts are destroyed here; `Shared` contexts are destroyed by
236+
// `SharedContext::drop` once the last reference (thread-local or `Proj`) is gone.
237+
if let Context::Owned(ctx) = *self {
238+
unsafe { proj_context_destroy(ctx) };
239+
}
240+
}
241+
}
242+
243+
/// The per-thread PROJ context, reference counted so it can be shared between every [`Proj`]
244+
/// created on the thread via [`Proj::new`] and [`Proj::new_known_crs`].
245+
struct SharedContext {
246+
ctx: *mut PJ_CONTEXT,
247+
}
248+
249+
impl Drop for SharedContext {
250+
fn drop(&mut self) {
251+
// Must only destroy the context: this runs during thread-local teardown, so it must not
252+
// re-enter the `SHARED_CONTEXT` thread-local. We deliberately do not call proj_cleanup()
253+
// (see the note in `Drop for Proj`).
254+
unsafe { proj_context_destroy(self.ctx) };
255+
}
256+
}
257+
258+
thread_local! {
259+
/// One PROJ context per thread, reused by `Proj::new`/`Proj::new_known_crs`. Creating a fresh
260+
/// context per object opens a new connection to the PROJ database with cold caches, which
261+
/// dominates `Proj` construction time (see https://github.com/georust/proj/issues/256).
262+
static SHARED_CONTEXT: Rc<SharedContext> =
263+
Rc::new(SharedContext { ctx: unsafe { proj_context_create() } });
264+
}
265+
266+
/// Return a reference-counted handle to the calling thread's shared PROJ context.
267+
#[allow(dead_code)] // wired into Proj::new/new_known_crs in a follow-up commit
268+
fn thread_local_context() -> Context {
269+
Context::Shared(SHARED_CONTEXT.with(Rc::clone))
270+
}
271+
210272
/// called by Proj::new and ProjBuilder::transform_new_crs
211-
fn transform_string(ctx: *mut PJ_CONTEXT, definition: &str) -> Result<Proj, ProjCreateError> {
273+
fn transform_string(ctx: Context, definition: &str) -> Result<Proj, ProjCreateError> {
274+
let ctx_ptr = ctx.as_ptr();
212275
let c_definition = CString::new(definition).map_err(ProjCreateError::ArgumentNulError)?;
213-
let ptr = result_from_create(ctx, unsafe { proj_create(ctx, c_definition.as_ptr()) })
214-
.map_err(|e| ProjCreateError::ProjError(e.message(ctx)))?;
276+
let ptr = result_from_create(ctx_ptr, unsafe {
277+
proj_create(ctx_ptr, c_definition.as_ptr())
278+
})
279+
.map_err(|e| ProjCreateError::ProjError(e.message(ctx_ptr)))?;
215280
Ok(Proj {
216281
c_proj: ptr,
217282
ctx,
@@ -221,23 +286,24 @@ fn transform_string(ctx: *mut PJ_CONTEXT, definition: &str) -> Result<Proj, Proj
221286

222287
/// Called by new_known_crs and proj_known_crs
223288
fn transform_epsg(
224-
ctx: *mut PJ_CONTEXT,
289+
ctx: Context,
225290
from: &str,
226291
to: &str,
227292
area: Option<Area>,
228293
) -> Result<Proj, ProjCreateError> {
294+
let ctx_ptr = ctx.as_ptr();
229295
let from_c = CString::new(from).map_err(ProjCreateError::ArgumentNulError)?;
230296
let to_c = CString::new(to).map_err(ProjCreateError::ArgumentNulError)?;
231297
let proj_area = unsafe { proj_area_create() };
232298
area_set_bbox(proj_area, area);
233-
let ptr = result_from_create(ctx, unsafe {
234-
proj_create_crs_to_crs(ctx, from_c.as_ptr(), to_c.as_ptr(), proj_area)
299+
let ptr = result_from_create(ctx_ptr, unsafe {
300+
proj_create_crs_to_crs(ctx_ptr, from_c.as_ptr(), to_c.as_ptr(), proj_area)
235301
})
236-
.map_err(|e| ProjCreateError::ProjError(e.message(ctx)))?;
302+
.map_err(|e| ProjCreateError::ProjError(e.message(ctx_ptr)))?;
237303
// Normalise input and output order to Lon, Lat / Easting Northing by inserting
238304
// An axis swap operation if necessary
239305
let normalised = unsafe {
240-
let normalised = proj_normalize_for_visualization(ctx, ptr);
306+
let normalised = proj_normalize_for_visualization(ctx_ptr, ptr);
241307
// deallocate stale PJ pointer
242308
proj_destroy(ptr);
243309
normalised
@@ -251,12 +317,13 @@ fn transform_epsg(
251317

252318
// called by Proj and ProjBuilder
253319
fn crs_to_crs_from_pj(
254-
ctx: *mut PJ_CONTEXT,
320+
ctx: Context,
255321
source_crs: &Proj,
256322
target_crs: &Proj,
257323
area: Option<Area>,
258324
options: Option<Vec<&str>>,
259325
) -> Result<Proj, ProjCreateError> {
326+
let ctx_ptr = ctx.as_ptr();
260327
let proj_area = unsafe { proj_area_create() };
261328
area_set_bbox(proj_area, area);
262329

@@ -269,16 +336,16 @@ fn crs_to_crs_from_pj(
269336
}
270337
}
271338

272-
let ptr = result_from_create(ctx, unsafe {
339+
let ptr = result_from_create(ctx_ptr, unsafe {
273340
proj_create_crs_to_crs_from_pj(
274-
ctx,
341+
ctx_ptr,
275342
source_crs.c_proj,
276343
target_crs.c_proj,
277344
proj_area,
278345
proj_options.as_ptr(),
279346
)
280347
})
281-
.map_err(|e| ProjCreateError::ProjError(e.message(ctx)))?;
348+
.map_err(|e| ProjCreateError::ProjError(e.message(ctx_ptr)))?;
282349

283350
Ok(Proj {
284351
c_proj: ptr,
@@ -289,10 +356,6 @@ fn crs_to_crs_from_pj(
289356

290357
macro_rules! define_info_methods {
291358
() => {
292-
fn ctx(&self) -> *mut PJ_CONTEXT {
293-
self.ctx
294-
}
295-
296359
/// Return information about the current instance of the PROJ libary.
297360
///
298361
/// See: <https://proj.org/development/reference/datatypes.html#c.PJ_INFO>
@@ -338,6 +401,10 @@ macro_rules! define_info_methods {
338401
impl ProjBuilder {
339402
define_info_methods!();
340403

404+
fn ctx(&self) -> *mut PJ_CONTEXT {
405+
self.ctx
406+
}
407+
341408
/// Enable or disable network access for [resource file download](https://proj.org/resource_files.html#where-are-proj-resource-files-looked-for).
342409
///
343410
/// # Safety
@@ -460,7 +527,7 @@ impl ProjBuilder {
460527
/// This method contains unsafe code.
461528
pub fn proj(mut self, definition: &str) -> Result<Proj, ProjCreateError> {
462529
let ctx = unsafe { std::mem::replace(&mut self.ctx, proj_context_create()) };
463-
transform_string(ctx, definition)
530+
transform_string(Context::Owned(ctx), definition)
464531
}
465532

466533
/// Try to create a transformation object that is a pipeline between two known coordinate reference systems.
@@ -507,7 +574,7 @@ impl ProjBuilder {
507574
area: Option<Area>,
508575
) -> Result<Proj, ProjCreateError> {
509576
let ctx = unsafe { std::mem::replace(&mut self.ctx, proj_context_create()) };
510-
transform_epsg(ctx, from, to, area)
577+
transform_epsg(Context::Owned(ctx), from, to, area)
511578
}
512579
/// Builder version of [`create_crs_to_crs_from_pj()`](fn@Proj::create_crs_to_crs_from_pj())
513580
pub fn proj_create_crs_to_crs_from_pj(
@@ -518,7 +585,7 @@ impl ProjBuilder {
518585
options: Option<Vec<&str>>,
519586
) -> Result<Proj, ProjCreateError> {
520587
let ctx = unsafe { std::mem::replace(&mut self.ctx, proj_context_create()) };
521-
crs_to_crs_from_pj(ctx, source_crs, target_crs, area, options)
588+
crs_to_crs_from_pj(Context::Owned(ctx), source_crs, target_crs, area, options)
522589
}
523590
}
524591

@@ -553,10 +620,15 @@ impl Default for ProjBuilder {
553620
/// ```
554621
pub struct Proj {
555622
c_proj: *mut PJconsts,
556-
ctx: *mut PJ_CONTEXT,
623+
ctx: Context,
557624
area: Option<*mut PJ_AREA>,
558625
}
559626

627+
// `Proj` is intentionally neither `Send` nor `Sync` (enforced by its raw pointers and the `Rc`
628+
// inside `Context`). A PROJ context must not be used concurrently from more than one thread, and
629+
// the shared per-thread context is reference counted with a non-atomic `Rc`. Do not add
630+
// `unsafe impl Send`/`Sync`: create a separate `Proj` per thread instead (as pyproj does).
631+
560632
impl Proj {
561633
/// Create a coordinate metadata object to be used in coordinate operations.
562634
///
@@ -580,7 +652,7 @@ impl Proj {
580652
/// This method contains unsafe code.
581653
pub fn coordinate_metadata_create(&self, epoch: f64) -> Result<Proj, ProjCreateError> {
582654
// Clone the context to avoid double-free in Drop implementations
583-
let cloned_ctx = unsafe { proj_context_clone(self.ctx) };
655+
let cloned_ctx = unsafe { proj_context_clone(self.ctx()) };
584656

585657
let ptr = result_from_create(cloned_ctx, unsafe {
586658
proj_coordinate_metadata_create(cloned_ctx, self.c_proj, epoch)
@@ -589,7 +661,7 @@ impl Proj {
589661

590662
Ok(Proj {
591663
c_proj: ptr,
592-
ctx: cloned_ctx,
664+
ctx: Context::Owned(cloned_ctx),
593665
area: None,
594666
})
595667
}
@@ -605,7 +677,7 @@ impl Proj {
605677
/// # Safety
606678
/// This method contains unsafe code.
607679
pub fn coordinate_metadata_get_epoch(&self) -> f64 {
608-
unsafe { proj_coordinate_metadata_get_epoch(self.ctx, self.c_proj) }
680+
unsafe { proj_coordinate_metadata_get_epoch(self.ctx(), self.c_proj) }
609681
}
610682

611683
/// Try to create a new transformation object
@@ -646,7 +718,7 @@ impl Proj {
646718
// and vice versa, or using PJ_XY for conversion operations
647719
pub fn new(definition: &str) -> Result<Proj, ProjCreateError> {
648720
let ctx = unsafe { proj_context_create() };
649-
transform_string(ctx, definition)
721+
transform_string(Context::Owned(ctx), definition)
650722
}
651723

652724
/// Try to create a new transformation object that is a pipeline between two known coordinate reference systems.
@@ -702,7 +774,7 @@ impl Proj {
702774
area: Option<Area>,
703775
) -> Result<Proj, ProjCreateError> {
704776
let ctx = unsafe { proj_context_create() };
705-
transform_epsg(ctx, from, to, area)
777+
transform_epsg(Context::Owned(ctx), from, to, area)
706778
}
707779

708780
/// Create a transformation object that is a pipeline _between_ two known coordinate reference systems.
@@ -756,8 +828,8 @@ impl Proj {
756828
options: Option<Vec<&str>>,
757829
) -> Result<Proj, ProjCreateError> {
758830
// Clone the context to avoid double-free in Drop implementations
759-
let ctx = unsafe { proj_context_clone(self.ctx) };
760-
crs_to_crs_from_pj(ctx, self, target_crs, area, options)
831+
let ctx = unsafe { proj_context_clone(self.ctx()) };
832+
crs_to_crs_from_pj(Context::Owned(ctx), self, target_crs, area, options)
761833
}
762834

763835
/// Set the bounding box of the area of use
@@ -786,6 +858,10 @@ impl Proj {
786858

787859
define_info_methods!();
788860

861+
fn ctx(&self) -> *mut PJ_CONTEXT {
862+
self.ctx.as_ptr()
863+
}
864+
789865
/// Returns the area of use of a projection
790866
///
791867
/// When multiple usages are available, the first one will be returned.
@@ -801,7 +877,7 @@ impl Proj {
801877
let mut out_area_name = MaybeUninit::uninit();
802878
let res = unsafe {
803879
proj_get_area_of_use(
804-
self.ctx,
880+
self.ctx(),
805881
self.c_proj,
806882
out_west_lon_degree.as_mut_ptr(),
807883
out_south_lat_degree.as_mut_ptr(),
@@ -1144,7 +1220,7 @@ impl Proj {
11441220
unsafe {
11451221
proj_errno_reset(self.c_proj);
11461222
let _success = proj_trans_bounds(
1147-
self.ctx,
1223+
self.ctx(),
11481224
self.c_proj,
11491225
PJ_DIRECTION_PJ_FWD,
11501226
left.to_f64().ok_or(ProjError::FloatConversion)?,
@@ -1266,7 +1342,7 @@ impl Proj {
12661342
proj_options.push(format!("SCHEMA={schema}"))?;
12671343
}
12681344
unsafe {
1269-
let out_ptr = proj_as_projjson(self.ctx, self.c_proj, proj_options.as_ptr());
1345+
let out_ptr = proj_as_projjson(self.ctx(), self.c_proj, proj_options.as_ptr());
12701346
if out_ptr.is_null() {
12711347
Err(ProjError::ExportToJson)
12721348
} else {
@@ -1339,7 +1415,7 @@ impl Proj {
13391415
};
13401416

13411417
unsafe {
1342-
let wkt = proj_as_wkt(self.ctx, self.c_proj, wkt_type, proj_options.as_ptr());
1418+
let wkt = proj_as_wkt(self.ctx(), self.c_proj, wkt_type, proj_options.as_ptr());
13431419
Ok(_string(wkt)?)
13441420
}
13451421
}
@@ -1457,13 +1533,15 @@ impl Drop for Proj {
14571533
if let Some(area) = self.area {
14581534
proj_area_destroy(area)
14591535
}
1536+
// Destroy the PJ before the context: PROJ objects reference their creating context,
1537+
// so it must outlive them. `self.ctx` (a `Context`) is a field, so it is dropped
1538+
// after this explicit body runs, which destroys the context in the right order.
14601539
proj_destroy(self.c_proj);
1461-
proj_context_destroy(self.ctx);
1462-
// We deliberately do not call proj_cleanup() here. It frees PROJ's
1463-
// process-global resources (the grid and +init file caches), so calling
1464-
// it whenever a single object is dropped clears caches that the next
1465-
// object would otherwise reuse, forcing reloads from disk. PROJ intends
1466-
// it to be called once before process termination, not per object.
1540+
// We deliberately do not call proj_cleanup() here. It frees PROJ's process-global
1541+
// resources (the grid and +init file caches), so calling it whenever a single object
1542+
// is dropped clears caches that the next object would otherwise reuse, forcing
1543+
// reloads from disk. PROJ intends it to be called once before process termination,
1544+
// not per object.
14671545
}
14681546
}
14691547
}

0 commit comments

Comments
 (0)