-
Notifications
You must be signed in to change notification settings - Fork 46
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add Debug implementation for proj::Proj #72
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -10,7 +10,7 @@ use proj_sys::{ | |
proj_trans, proj_trans_array, PJconsts, PJ_AREA, PJ_CONTEXT, PJ_COORD, PJ_DIRECTION_PJ_FWD, | ||
PJ_DIRECTION_PJ_INV, PJ_INFO, PJ_LP, PJ_XY, | ||
}; | ||
use std::fmt::Debug; | ||
use std::fmt::{self, Debug}; | ||
|
||
#[cfg(feature = "network")] | ||
use proj_sys::proj_context_set_enable_network; | ||
|
@@ -124,15 +124,18 @@ impl Area { | |
} | ||
|
||
/// Easily get a String from the external library | ||
pub(crate) fn _string(raw_ptr: *const c_char) -> Result<String, ProjError> { | ||
let c_str = unsafe { CStr::from_ptr(raw_ptr) }; | ||
pub(crate) unsafe fn _string(raw_ptr: *const c_char) -> Result<String, ProjError> { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since this function takes in a pointer and dereferences it, it's possible for someone (one of us since it's There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The latest force push marks a couple functions in |
||
assert!(!raw_ptr.is_null()); | ||
let c_str = CStr::from_ptr(raw_ptr); | ||
Ok(str::from_utf8(c_str.to_bytes())?.to_string()) | ||
} | ||
|
||
/// Look up an error message using the error code | ||
fn error_message(code: c_int) -> Result<String, ProjError> { | ||
let rv = unsafe { proj_errno_string(code) }; | ||
_string(rv) | ||
unsafe { | ||
let rv = proj_errno_string(code); | ||
_string(rv) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Continuing my previous comment about There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That makes sense! Would you preserve this rationale in a comment? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Lo and behold, https://github.com/OSGeo/PROJ/blob/5e077729274f5d28e137e1a41f7d3350146614ef/src/strerrno.cpp#L12-L14 https://github.com/OSGeo/PROJ/blob/5e077729274f5d28e137e1a41f7d3350146614ef/src/strerrno.cpp#L37-L75 Specifically, it returns a null pointer if |
||
} | ||
} | ||
|
||
/// Set the bounding box of the area of use | ||
|
@@ -196,15 +199,17 @@ pub trait Info { | |
/// # Safety | ||
/// This method contains unsafe code. | ||
fn info(&self) -> Result<Projinfo, ProjError> { | ||
let pinfo: PJ_INFO = unsafe { proj_info() }; | ||
Ok(Projinfo { | ||
major: pinfo.major, | ||
minor: pinfo.minor, | ||
patch: pinfo.patch, | ||
release: _string(pinfo.release)?, | ||
version: _string(pinfo.version)?, | ||
searchpath: _string(pinfo.searchpath)?, | ||
}) | ||
unsafe { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What I've gathered from your other changes in this PR is that you advocate marking the function as Is that right? I've never really considered it, but that makes sense to me. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Yeah exactly. And I think this section of TRPL covers that better than I can explain: There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm permanently confused about when / when not to mark functions unsafe. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When determining whether a function should be marked
and
So consider this function (taken directly from the FFI page in the Nomicon): fn kaboom(ptr: *const i32) -> i32 {
*ptr
} I'm able to call this function with whatever pointer I want and it will attempt to read from that memory location (e.g. something like: Coming back to PROJ-land, consider this example: /// Print the description of a CRS given a definition string
fn crs_description(crs_definition: CString) {
let pj_ptr = proj_sys::proj_create(0, crs_definition.as_ptr());
let pj_proj_info = proj_sys::proj_pj_info(pj_ptr);
println!("Description: {}", _string(pj_proj_info.description));
} Is it possible to call this function and have it succeed? Yes! Pass it a valid Is it possible to call this function that will result in undefined behavior? Yes! Pass it a And one more example: /// Print the description of a CRS given a definition string
fn crs_description(crs_definition: CString) {
let pj_ptr = proj_sys::proj_create(0, crs_definition.as_ptr());
if pj_ptr.is_null() {
return;
}
let pj_proj_info = proj_sys::proj_pj_info(pj_ptr);
if pj_proj_info.description.is_null() {
return;
}
println!("Description: {}", _string(pj_proj_info.description));
} Is it possible to call this function that will result in undefined behavior? Assuming I'm not overlooking anything, I don't think so! You can pass it whatever string you want and we shouldn't encounter any undefined behavior. Thus we don't need to mark it as unsafe. Hope that helps a little! |
||
let pinfo: PJ_INFO = proj_info(); | ||
Ok(Projinfo { | ||
major: pinfo.major, | ||
minor: pinfo.minor, | ||
patch: pinfo.patch, | ||
release: _string(pinfo.release)?, | ||
version: _string(pinfo.version)?, | ||
searchpath: _string(pinfo.searchpath)?, | ||
}) | ||
} | ||
} | ||
|
||
/// Check whether network access for [resource file download](https://proj.org/resource_files.html#where-are-proj-resource-files-looked-for) is currently enabled or disabled. | ||
|
@@ -544,7 +549,14 @@ impl Proj { | |
let south = unsafe { out_south_lat_degree.assume_init() }; | ||
let east = unsafe { out_east_lon_degree.assume_init() }; | ||
let north = unsafe { out_north_lat_degree.assume_init() }; | ||
let name = unsafe { out_area_name.assume_init() }; | ||
let name = unsafe { | ||
let name = out_area_name.assume_init(); | ||
if !name.is_null() { | ||
Some(_string(name)?) | ||
} else { | ||
None | ||
} | ||
}; | ||
|
||
let area = if west != -1000.0 && south != -1000.0 && east != -1000.0 && north != -1000.0 | ||
{ | ||
|
@@ -557,12 +569,36 @@ impl Proj { | |
} else { | ||
None | ||
}; | ||
let name = if !name.is_null() { | ||
Some(_string(name)?) | ||
Ok((area, name)) | ||
} | ||
} | ||
|
||
fn pj_info(&self) -> PjInfo { | ||
unsafe { | ||
let pj_info = proj_pj_info(self.c_proj); | ||
let id = if pj_info.id.is_null() { | ||
None | ||
} else { | ||
Some(_string(pj_info.id).expect("PROJ built an invalid string")) | ||
}; | ||
Comment on lines
+579
to
+583
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. With the 1.50.0 release, we could use There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh right. rust versions. 😄 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Too soon! But yes, we should. |
||
let description = if pj_info.description.is_null() { | ||
None | ||
} else { | ||
Some(_string(pj_info.description).expect("PROJ built an invalid string")) | ||
}; | ||
Ok((area, name)) | ||
let definition = if pj_info.definition.is_null() { | ||
None | ||
} else { | ||
Some(_string(pj_info.definition).expect("PROJ built an invalid string")) | ||
}; | ||
let has_inverse = pj_info.has_inverse == 1; | ||
PjInfo { | ||
id, | ||
description, | ||
definition, | ||
has_inverse, | ||
accuracy: pj_info.accuracy, | ||
} | ||
} | ||
} | ||
|
||
|
@@ -571,8 +607,7 @@ impl Proj { | |
/// # Safety | ||
/// This method contains unsafe code. | ||
pub fn def(&self) -> Result<String, ProjError> { | ||
let rv = unsafe { proj_pj_info(self.c_proj) }; | ||
_string(rv.definition) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This previous code assumed that So in this instance, I replaced a potential null pointer dereference with a panic via |
||
Ok(self.pj_info().definition.expect("proj_pj_info did not provide a definition")) | ||
} | ||
|
||
/// Project geodetic coordinates (in radians) into the projection specified by `definition` | ||
|
@@ -826,6 +861,27 @@ impl Proj { | |
} | ||
} | ||
|
||
struct PjInfo { | ||
id: Option<String>, | ||
description: Option<String>, | ||
definition: Option<String>, | ||
has_inverse: bool, | ||
accuracy: f64, | ||
} | ||
|
||
impl fmt::Debug for Proj { | ||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { | ||
let pj_info = self.pj_info(); | ||
f.debug_struct("Proj") | ||
.field("id", &pj_info.id) | ||
.field("description", &pj_info.description) | ||
.field("definition", &pj_info.definition) | ||
.field("has_inverse", &pj_info.has_inverse) | ||
.field("accuracy", &pj_info.accuracy) | ||
.finish() | ||
} | ||
} | ||
|
||
impl Drop for Proj { | ||
fn drop(&mut self) { | ||
unsafe { | ||
|
@@ -933,6 +989,18 @@ mod test { | |
"proj=longlat datum=WGS84 no_defs ellps=WGS84 towgs84=0,0,0" | ||
); | ||
} | ||
|
||
#[test] | ||
fn test_debug() { | ||
let wgs84 = "+proj=longlat +datum=WGS84 +no_defs"; | ||
let proj = Proj::new(wgs84).unwrap(); | ||
let debug_string = format!("{:?}", proj); | ||
assert_eq!( | ||
"Proj { id: Some(\"longlat\"), description: Some(\"PROJ-based coordinate operation\"), definition: Some(\"proj=longlat datum=WGS84 no_defs ellps=WGS84 towgs84=0,0,0\"), has_inverse: true, accuracy: -1.0 }", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Note that if PROJ changes any of the text here in across PROJ versions, this test will fail. Not sure how concerned we should be about that. |
||
debug_string | ||
); | ||
} | ||
|
||
#[test] | ||
#[should_panic] | ||
// This failure is a bug in libproj | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See my comment below regarding the changes in this file: https://github.com/georust/proj/pull/72/files#r574620593