|
| 1 | +- Feature Name: `cstr_with_ptr` |
| 2 | +- Start Date: 2016-06-06 |
| 3 | +- RFC PR: (leave this empty) |
| 4 | +- Rust Issue: (leave this empty) |
| 5 | + |
| 6 | +# Summary |
| 7 | +[summary]: #summary |
| 8 | + |
| 9 | +Deprecate `CStr::as_ptr`, which returns a raw pointer and thereby invites unsound code, in favor of |
| 10 | +a new method, `CStr::with_ptr`, which passes the pointer to a user-provided closure. |
| 11 | + |
| 12 | +# Motivation |
| 13 | +[motivation]: #motivation |
| 14 | + |
| 15 | +`CString` wraps a C string and provides easy, safe access to the raw pointer to the string, for |
| 16 | +FFI interop. The safety is correct, as there's nothing unsound about having a raw pointer around |
| 17 | +until you dereference it. However, raw pointers are not tracked by the borrow checker, and once you |
| 18 | +have one it's as easy in Rust as it is in C to keep it around too long (namely, after the backing |
| 19 | +`CString` is dropped) and have a dangling pointer, which leads to unsound code. |
| 20 | + |
| 21 | +There are [hundreds of projects](https://users.rust-lang.org/t/you-should-stop-telling-people-that-safe-rust-is-always-safe/6094/7) |
| 22 | +including a call like `CString::new(...).unwrap().as_ptr()`, which is evidence that this UB |
| 23 | +mistake is widespread. |
| 24 | + |
| 25 | +By changing the API from a method _returning_ a raw pointer to one that runs a user-provided |
| 26 | +closure, we make it easier to write sound code than to write unsound code. Sure, you can still |
| 27 | +have the pointer escape the closure (see [Drawbacks](#drawbacks)), but it's a clear choice. The |
| 28 | +API change tips the scales towards soundness. |
| 29 | + |
| 30 | +# Detailed design |
| 31 | +[design]: #detailed-design |
| 32 | + |
| 33 | +1. Deprecate `CStr::as_ptr`, with the following message: "too easy to misuse because it returns a |
| 34 | +raw pointer; use `with_ptr` instead". The method is stable, so it will not be removed in any 1.x |
| 35 | +release. |
| 36 | + |
| 37 | +2. Add the following method to `CStr`: |
| 38 | + |
| 39 | + ```rust |
| 40 | + /// Calls the provided closure, passing the inner pointer to this C string. |
| 41 | + /// |
| 42 | + /// The pointer will be valid for as long as `self` is and points to a contiguous region of |
| 43 | + /// memory terminated with a 0 byte to represent the end of the string. |
| 44 | + #[allow(deprecated)] // for the as_ptr call |
| 45 | + fn with_ptr<F: FnOnce(*const c_char)>(&self, f: F) { |
| 46 | + f(self.as_ptr()); |
| 47 | + } |
| 48 | + ``` |
| 49 | + |
| 50 | + For example usage see [this playpen](https://play.rust-lang.org/?gist=b6b1495ebee03fea679e95acb6b51ed6). |
| 51 | + |
| 52 | +3. Modify the `CStr` and `CString` examples that use `as_ptr` to use `with_ptr` instead. |
| 53 | + |
| 54 | + `CString::new` has the following example code: |
| 55 | + |
| 56 | + ```rust |
| 57 | + use std::ffi::CString; |
| 58 | + use std::os::raw::c_char; |
| 59 | + |
| 60 | + extern { fn puts(s: *const c_char); } |
| 61 | + |
| 62 | + fn main() { |
| 63 | + let to_print = CString::new("Hello!").unwrap(); |
| 64 | + unsafe { |
| 65 | + puts(to_print.as_ptr()); |
| 66 | + } |
| 67 | + } |
| 68 | + ``` |
| 69 | + |
| 70 | + Under this proposal, it would change to: |
| 71 | + |
| 72 | + ```rust |
| 73 | + use std::ffi::CString; |
| 74 | + use std::os::raw::c_char; |
| 75 | + |
| 76 | + extern { fn puts(s: *const c_char); } |
| 77 | + |
| 78 | + fn main() { |
| 79 | + let to_print = CString::new("Hello!").unwrap(); |
| 80 | + to_print.with_ptr(|p| unsafe { |
| 81 | + puts(p); |
| 82 | + }); |
| 83 | + } |
| 84 | + ``` |
| 85 | + |
| 86 | + There are nearly identical examples on `CString` and `CStr` themselves which would change in the |
| 87 | + same way. |
| 88 | + |
| 89 | +# Drawbacks |
| 90 | +[drawbacks]: #drawbacks |
| 91 | + |
| 92 | +- It deprecates another stable method, which contributes to perception of API churn. |
| 93 | +- It adds surface area to the API of `CStr`. |
| 94 | +- It's still rather easy to circumvent the help and write unsound code by "leaking" the pointer out |
| 95 | +of the closure: |
| 96 | + |
| 97 | + ```rust |
| 98 | + let mut ptr = ptr::null(); |
| 99 | + { |
| 100 | + let s = CString::new("foo").unwrap(); |
| 101 | + s.with_ptr(|p| { ptr = p; }); |
| 102 | + } |
| 103 | + /* ptr is now dangling */ |
| 104 | + ``` |
| 105 | + |
| 106 | +# Alternatives |
| 107 | +[alternatives]: #alternatives |
| 108 | + |
| 109 | +- Do nothing. It remains very easy to write unsound code using `CString` and `CStr::as_ptr`. |
| 110 | +- Move the `temporary_cstring_as_ptr` lint, which warns on the most common way to write said unsound |
| 111 | +code (calling `as_ptr` on a temporary `CString`) from Clippy to rustc. |
| 112 | + |
| 113 | +# Unresolved questions |
| 114 | +[unresolved]: #unresolved-questions |
| 115 | + |
| 116 | +- Should `with_ptr` allow the closure to return a value? It could be |
| 117 | + |
| 118 | + ```rust |
| 119 | + fn with_ptr<F: FnOnce(*const c_char) -> O, O>(&self, f: F) -> O { |
| 120 | + f(self.as_ptr()) |
| 121 | + } |
| 122 | + ``` |
| 123 | + |
| 124 | + which might be convenient but would make it easier to "leak" the pointer (as easy as |
| 125 | + `let ptr = s.with_ptr(|p| p);`). |
| 126 | + |
0 commit comments