@@ -31,6 +31,7 @@ use std::ffi::CStr;
3131use std:: ffi:: CString ;
3232use std:: mem:: MaybeUninit ;
3333use std:: path:: Path ;
34+ use std:: rc:: Rc ;
3435use thiserror:: Error ;
3536
3637pub 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
223288fn 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
253319fn 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
290357macro_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 {
338401impl 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/// ```
554621pub 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+
560632impl 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