Skip to content

Commit 9a044bb

Browse files
committed
Reuse a per-thread context in Proj::new/new_known_crs
Proj::new and Proj::new_known_crs now borrow the per-thread shared context instead of creating (and destroying) a fresh PROJ context each call, mirroring pyproj's one-context-per-thread model. This drops Proj::new to ~1us per instance. ProjBuilder and the clone-based constructors continue to own their own contexts. Adds tests for per-thread reuse, per-thread isolation, ProjBuilder context independence, and that dropping a Proj does not free the shared context. Signed-off-by: Stephan Hügel <shugel@tcd.ie>
1 parent c0ae9ce commit 9a044bb

3 files changed

Lines changed: 94 additions & 7 deletions

File tree

CHANGES.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@
77
files) when a `Proj` or `ProjBuilder` is dropped. These caches persist for
88
the lifetime of the process, substantially speeding up repeated creation of
99
transformation objects (https://github.com/georust/proj/issues/256).
10+
- `Proj::new` and `Proj::new_known_crs` now reuse one PROJ context per thread
11+
instead of creating a new one per call, reducing construction time from
12+
~300us to ~1us per object (https://github.com/georust/proj/issues/256).
1013

1114
- Refactored options logic
1215

src/lib.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,18 @@
150150
//! resources actually loaded and so do not grow without bound: the cost is memory residency for
151151
//! the lifetime of the process, not additional disk usage.
152152
//!
153+
//! ## Threading
154+
//!
155+
//! Each PROJ context (`PJ_CONTEXT`) is tied to a single thread and must not be used concurrently
156+
//! from more than one thread. `Proj` is therefore deliberately neither `Send` nor `Sync`; to
157+
//! transform coordinates on several threads, create a `Proj` on each thread.
158+
//!
159+
//! [`Proj::new`] and [`Proj::new_known_crs`] reuse one context per thread rather than creating a
160+
//! new one for every object. This keeps the connection to the PROJ database and its caches warm,
161+
//! so constructing many transformation objects on a thread is considerably cheaper than it would
162+
//! otherwise be. [`ProjBuilder`] keeps its own context, since it is used to configure
163+
//! context-specific state such as network access and search paths.
164+
//!
153165
//! ## Conform your own types
154166
//!
155167
//! If you have your own geometric types, you can conform them to the `Coord` trait and use `proj`

src/proj.rs

Lines changed: 79 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -216,8 +216,6 @@ fn area_set_bbox(parea: *mut proj_sys::PJ_AREA, new_area: Option<Area>) {
216216
/// which the thread-local and any surviving `Proj` instances are dropped at thread exit.
217217
enum Context {
218218
Owned(*mut PJ_CONTEXT),
219-
// Wired into Proj::new/new_known_crs in a follow-up commit.
220-
#[allow(dead_code)]
221219
Shared(Rc<SharedContext>),
222220
}
223221

@@ -264,7 +262,6 @@ thread_local! {
264262
}
265263

266264
/// 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
268265
fn thread_local_context() -> Context {
269266
Context::Shared(SHARED_CONTEXT.with(Rc::clone))
270267
}
@@ -717,8 +714,7 @@ impl Proj {
717714
// PJ_LP signals projection of geodetic coordinates, with output being PJ_XY
718715
// and vice versa, or using PJ_XY for conversion operations
719716
pub fn new(definition: &str) -> Result<Proj, ProjCreateError> {
720-
let ctx = unsafe { proj_context_create() };
721-
transform_string(Context::Owned(ctx), definition)
717+
transform_string(thread_local_context(), definition)
722718
}
723719

724720
/// Try to create a new transformation object that is a pipeline between two known coordinate reference systems.
@@ -773,8 +769,7 @@ impl Proj {
773769
to: &str,
774770
area: Option<Area>,
775771
) -> Result<Proj, ProjCreateError> {
776-
let ctx = unsafe { proj_context_create() };
777-
transform_epsg(Context::Owned(ctx), from, to, area)
772+
transform_epsg(thread_local_context(), from, to, area)
778773
}
779774

780775
/// Create a transformation object that is a pipeline _between_ two known coordinate reference systems.
@@ -1674,6 +1669,81 @@ mod test {
16741669
let proj = Proj::new(wgs84).unwrap();
16751670
let np = proj.coordinate_metadata_create(epoch).unwrap();
16761671
assert_eq!(np.coordinate_metadata_get_epoch(), 2021.3);
1672+
// The metadata object clones the context, so it owns a distinct one rather than
1673+
// borrowing the shared per-thread context.
1674+
assert_ne!(np.ctx() as usize, proj.ctx() as usize);
1675+
}
1676+
1677+
#[test]
1678+
fn test_new_reuses_thread_context() {
1679+
// Every Proj::new on a thread borrows the same per-thread context.
1680+
let a = Proj::new("EPSG:4326").unwrap();
1681+
let b = Proj::new_known_crs("EPSG:4326", "EPSG:3857", None).unwrap();
1682+
assert_eq!(a.ctx(), b.ctx());
1683+
}
1684+
1685+
#[test]
1686+
fn test_each_thread_gets_its_own_context() {
1687+
// The shared context is per-thread: each thread must get a distinct context. A barrier
1688+
// keeps every context alive simultaneously while addresses are captured, so a freed
1689+
// context's address cannot be reused by another thread and falsely collide.
1690+
use std::sync::{Arc, Barrier};
1691+
1692+
let threads = 4;
1693+
let barrier = Arc::new(Barrier::new(threads + 1));
1694+
let main = Proj::new("EPSG:4326").unwrap();
1695+
1696+
let handles: Vec<_> = (0..threads)
1697+
.map(|_| {
1698+
let barrier = Arc::clone(&barrier);
1699+
// Send the context address (usize), not the !Send Proj itself.
1700+
std::thread::spawn(move || {
1701+
let proj = Proj::new("EPSG:4326").unwrap();
1702+
let addr = proj.ctx() as usize;
1703+
barrier.wait();
1704+
drop(proj);
1705+
addr
1706+
})
1707+
})
1708+
.collect();
1709+
1710+
barrier.wait();
1711+
let mut seen = vec![main.ctx() as usize];
1712+
for handle in handles {
1713+
seen.push(handle.join().unwrap());
1714+
}
1715+
1716+
let total = seen.len();
1717+
seen.sort_unstable();
1718+
seen.dedup();
1719+
assert_eq!(
1720+
seen.len(),
1721+
total,
1722+
"each thread should get a distinct context"
1723+
);
1724+
}
1725+
1726+
#[test]
1727+
fn test_builder_context_is_not_shared() {
1728+
// ProjBuilder owns its context (it mutates context state), so it must not borrow the
1729+
// shared per-thread context.
1730+
let shared = Proj::new("EPSG:4326").unwrap().ctx() as usize;
1731+
let built = ProjBuilder::new().proj("EPSG:4326").unwrap();
1732+
assert_ne!(built.ctx() as usize, shared);
1733+
}
1734+
1735+
#[test]
1736+
fn test_drop_does_not_free_shared_context() {
1737+
// Dropping a Proj must not destroy the shared context other Proj instances still use.
1738+
// Under AddressSanitizer this would surface as a use-after-free if the invariant broke.
1739+
let first = Proj::new("EPSG:4326").unwrap().ctx() as usize;
1740+
for _ in 0..1000 {
1741+
let _ = Proj::new("EPSG:4326").unwrap();
1742+
}
1743+
let transformer = Proj::new_known_crs("EPSG:4326", "EPSG:3857", None).unwrap();
1744+
assert_eq!(transformer.ctx() as usize, first);
1745+
// The shared context is still valid and usable.
1746+
transformer.convert((2.0, 49.0)).unwrap();
16771747
}
16781748

16791749
#[test]
@@ -1688,6 +1758,8 @@ mod test {
16881758

16891759
assert_relative_eq!(result.x(), 1450880.2910605022, epsilon = 1.0e-8);
16901760
assert_relative_eq!(result.y(), 1141263.0111604782, epsilon = 1.0e-8);
1761+
// The transformer clones the source context, so it owns a distinct one.
1762+
assert_ne!(transformer.ctx() as usize, from.ctx() as usize);
16911763
}
16921764

16931765
#[test]

0 commit comments

Comments
 (0)