Closed nikomatsakis closed 10 years ago
I feel a little weird having Rust types like Option
"infect" C FFI bindings, but nullable function pointers are currently done with Option<extern "C" fn()>
so it'd at least be consistent with that.
We need to discuss this further, there are interesting issues on both sides of this. P-backcompat-lang.
Some comments:
0 as *T
. iirc
casting integers to *T is mostly allowed for embedded null in static
constants, the need for which goes away here.*T
is nullable and hence Some(null)
is a "legit" value.Upside of the change:
Foo { x: None }
instead
of Foo { x: 0 as *uint }
Downside of the change:
Option<*T>
if
they want to accept nullSee some discussion in #9788
I like this a lot. (It's similar to what I tried proposing a few months ago.) I think the mentioned downside is an upside. It should be explicit whether nulls are possible or not. Why should it be different for *T
than any other type?
If we do this then the Option
optimization would also automatically apply to user-defined types written in terms of *T
, and if we also do the thing where *T
is multi-word for unsized types, then basically people can just use *T
in a natural way and everything else gets taken care of.
My issue with this is that I consider the current enum-pointer optimization to be just that, an optimization, and relying on that behaviour doesn't sit right with me. Currently, Option<~T>
just happens to be represented as a nullable pointer, but if it wasn't all current Rust code should continue working as before.
I am firmly against anything that makes *T
behave noticeably differently in any way, shape or form to some C pointer. I always considered raw pointers to be the place where Rust just left you alone to do your own thing.
This also, for some reason, singles out 0 as a bad value. The whole point of the raw pointers is that Rust cannot determine if they point to any valid location. Why should we treat 0 as special in this case? What about other bogus pointers? What about tagged pointers? They aren't safe either and I don't see how dubiously preventing NULL
gains anything other than overhead and the illusion of safety. Because it's not like this actually prevents *T
from being NULL
, it just prevents you from constructing one. If I forget to wrap my pointer return in an Option, somebody could assume that NULL
isn't a possible value, even though that isn't true. Or I could pass a **T
through and have that be turned to null.
I can see this only producing misleading code and faulty assumptions. Canonicalising Option<*T>
as "null raw pointer" only muddies the water because the implication, that *T
is not null, isn't true.
If Option<*T>
is implemented as a nullable pointer, I'm not too worried, but I am firmly against making Option<*T>
the nullable pointer.
I extremely strongly agree with @Aatch. Making *T
non-nullable seems to violate the entire point of *T
.
I also agree with @Aatch. *T
already signifies "here be dragons". Let's at least make sure the dragons have C semantics.
I approve of this change. It will ensure that writers of C bindings document their assumptions and that their code is consistent with their assumptions. In debug builds, Rust could automatically insert assertions to test these assumptions (e.g. that certain C functions never return null).
Why should we treat 0 as special in this case? What about other bogus pointers? What about tagged pointers?
They are not pointers! They should be represented by corresponding Rust types depending on how they differ from pointers (both 0 and 1 are special; low bits are meant to be lopped off; etc). In the worst case, they can be represented in Rust as (newtyped) uints.
This could be really annoying in kernel code where 0
isn't a special pointer. LLVM currently lacks full support for this, but it will get there.
Because it's not like this actually prevents
*T
from being NULL, it just prevents you from constructing one. If I forget to wrap my pointer return in an Option, somebody could assume that NULL isn't a possible value, even though that isn't true. Or I could pass a**T
through and have that be turned to null.
If this is referring to extern
functions, then it's an objection I was going to raise myself before I realized that it's true of any type. The signatures of foreign functions are not checked in any way and it's entirely the programmer's responsibility to get them correct. If she doesn't, then implicit transmute()
s will happen left and right, and Option<*T>
to *T
is no more special in this sense than any of the other possibilities.
If
Option<*T>
is implemented as a nullable pointer, I'm not too worried, but I am firmly against makingOption<*T>
the nullable pointer.
How then would you distinguish Some(0)
from None
?
I don't get the attachment to C semantics, either. We have no trouble learning from C's mistakes in other parts of the language. Where else in the semantics (not syntax) of the language do we say "we have to do it this way because C does it this way, period", even if another way might be better? Why here?
Finally, do (plural) you feel the same way about function pointers, where the same things (non-nullable, need Option
) are true?
Things I agree with are that writing the null pointer optimization in stone and assuming 0
is an invalid value even when it's not necessarily (e.g. kernels) isn't super, but I feel that these are smaller problems than the benefits.
One further concern I would raise from an ergonomics perspective, overlapping with but not quite the same as things above, is that if it looks like a C pointer, and it sure does, then people are going to expect that it behaves like a C pointer (to some degree this might already be apparent). They're going to think it's the same thing as in C and use it in their extern fn
s indiscriminately, suffer unpleasant effects, and get really surprised when they learn that they have to wrap it in Option
when null is an option. To avoid falling into uncanny valleys, if the semantics change, then maybe the syntax should too.
@glehel I don't like the idea of pushing systems concerns further to the side in a systems programming language. There needs to be some type that you can use to directly deal with the hardware without Rust sticking it's nose in. We have pointers that don't follow C semantics. We do need one that does. This isn't something we can change, we have to deal with C code.
As for other values being passed to extern code being made into illegal values, yes, you're right, but in those cases either some has gone terribly wrong or you are making a terrible mistake.
My issue is that it gives the illusion of safety where it's blatantly false. It's basically saying "these pointers are never null, except when they are", because it's not like the compiler will catch the little things like forgetting to wrap some type in an Option.
Lastly, what, in light of these criticisms, would this change gain you? It doesn't make raw pointers any more safe. It doesn't make writing FFI bindings any easier, it could help catch a class of errors, but only by introducing a new class. All it does, to me, is make things more complicated for no good reason.
@Aatch Agreed. Furthermore, all current FFI bindings can be created pretty easily by looking purely at the types involved in the C declaration. This absolutely is not the case with a non-nullable *T
vs Option<*T>
. The only way to know which type is correct is to read documentation, assuming the documentation even bothers to define behavior surrounding NULL
, or even exists or is correct. And besides the gigantic headache this causes for deciding which types to use, it also completely breaks any ability to use automated FFI generation (e.g. rust-bindgen), because the automation tool can't possibly know whether it should be using *T
or Option<*T>
.
@kballard fwiw, GCC and clang have custom nonnull
and returns_nonnull
attributes for specifying that certain arguments are not null.
@huonw Sure, but almost no code ever uses it. And note that it's an opt-in to being non-nullable, whereas the proposal here makes it an opt-out.
@nikomatsakis you listed as one upside of the change: "More accurate types"; I think that would be more correctly stated as "more precise types"; all doing this can buy you (I think) is the ability to directly express in a Rust-ic fashion a distinction between a nonnullable and a nullable pointer. But as they said in my high school chemistry class, precision is not accuracy.
This comment from @Aatch, "If Option<*T>
is implemented as a nullable pointer, I'm not too worried, but I am firmly against making Option<*T>
the nullable pointer." (emphasis added), sent me on a thought exercise.
There are potentially three or more options here, not two, and I do not know which niko intended from the original description.
sizeof::<Option<*int>>() != sizeof::<*int>
, null ptr is always a *T
, extern bindings use *T
for all ptr parameters and return types.*T
and Option<*T>
the same domain of representation values. sizeof::<Option<*int>>() == sizeof::<*int>
, but the native representation of the null-ptr value (presumably a 0
word) would belong to both types (and would be expressed via ptr::null()
for the former via None
for the latter). 0 as *T
still "works", and we keep std::ptr::null<T>() -> *T
. extern bindings can choose to use either of the two variants (though presumably conventions of usage should naturally arise). The expression let p = 0 as *T; Some(p)
still results in an unboxed null ptr value at runtime, which is silently reinterpreted by the language as None
. (Let us put aside the question of whether the compiler has to respect the potential for this behind-the-scenes coercion between variants, which could invalidate certain hypothetical static flow analyses.)*T
a proper subset of Option<*T>
; in correct executions obeying all the (type) rules, the null ptr is not a *T
in Rust code. Instead, null ptrs are canonically a None
in Rust. sizeof::<Option<*int>>() == sizeof::<*int>
. (There are subvariants of this option, e.g. whether breaking this rule causes a fail!
at a C → Rust FFI boundary, implying runtime checks that we probably do not want, or if breakage just silently passes at C → Rust transitions, which means that we would have to provide some sort of is_null
primitive on *T
to allow code to actually check these things before dereferencing the pointer, and Some(p)
on such a pointer would again be silently reinterpreted as None
; really this latter subvariant just strikes me as a weak version of option 2.)From the debates on this ticket, it seems like a lot of people have been assuming that some variant of (3) is what is being proposed.
(There are also subvariants of (2.), for example where we remove 0 as *T
and std::ptr::null<T>() -> *T
, or keep only one of them, etc. I am pretty sure niko was not proposing (2) as it was written above, as one can see from the discussion on #9788. I do not want to try to work through all the implications of those subvariants now, though doing so may be necessary to make an actual decision; the important high-level point is that in option (2), the two types have the same domain of representation values.)
So, @nikomatsakis: can you clarify which (sub)variant were you proposing?
@pnkfelix I was proposing option 3, though I had considered Option 2 for a while. I find the "hands off my *T
" argument mildly persuasive. We're a bit inconsistent in this regard so far: we rejected lifetimes on raw pointers (like C), but distinguish *
and *mut
and cannot represent *const
(unlike C), and we include null pointers in the domain of *T
(like C). I think I dislike being in the middle. I find the thought of making unsafe pointers as unsafe as they can possibly be, and I also like the thought of making them as safe as they can be.
The main motivator here is the fact that we permit casts from random integers to *T
in safe code just to accommodate null pointers, which already have a perfectly good and safe representation. In other words, we want people to be able to write static constants that include 0 as *T
. This seems like a lose-lose to me: it's awkward to write and it introduces a random safe transmute where others are not considered safe. Of course we could address the 123 as *T
thing by adding a nullptr
keyword a la C++. It'd be trivial to do and might be worth it.
Oh, the other motivator is that I think it's genuinely surprising that Option<~T>
is a single word but Option<*T>
is not. I was certainly surprised.
And one parting thought -- I agree that in general enum repr should remain undefined, but I am ok with specifying the pointer optimization. I suspect it'll be a de facto standard whatever we do.
Hmm, I was thinking more about the question of kernel code. My first thought was since one could still construct a *T
whose value was 0, this shouldn't be a problem in practice for kernel authors, but then I guess we'd be robbing them of the ability to have useful Option<*T>
types, which is perhaps too bad.
On Wed, Nov 27, 2013 at 01:12:01AM -0800, Felix S Klock II wrote:
But as they said in my high school chemistry class, precision is not accuracy.
True. But then, ain't that always the case? But I guess it's particularly the case here, since these values frequent the border between the C badlands and Rust-ic civilization.
My original instinct was to go with option 1: "Keep things as they are". I think the main reason why I'm considering alternatives is that I too find it genuinely surprising that Option<~T>
is a single word but Option<*T>
is not.
@nikomatsakis what about an option 2 subvariant where we also remove 0 as *T
; people who want to write null static constants would have to use the Option<*T>
instead, and we provide some methods in std::ptr
to allow easy transitions between Option<*T>
and *T
?
(This is basically my way of trying to deal with my option 3 probably being a weak version of option 2.)
By the way, in case it is unclear, I do find it distasteful (emphatically "not clever") that a consequence of option 2 as I described it (and I think in expected practice, option 3 as well) an application of |p:*int| { Some(p) }
can effectively return None
. That alone is enough to push me pretty far back into the camp of (1) "Keep things as they are."
@kballard
And besides the gigantic headache this causes for deciding which types to use, it also completely breaks any ability to use automated FFI generation (e.g. rust-bindgen), because the automation tool can't possibly know whether it should be using
*T
orOption<*T>
.
Automation tools would/should stick to Option<*T>
, which is always a good conservative approximation. After all, it's what's semantically equivalent to current practice, while *T
is only syntactically the same - another example of the syntax leading astray.
@Aatch
My issue is that it gives the illusion of safety where it's blatantly false. It's basically saying "these pointers are never null, except when they are", because it's not like the compiler will catch the little things like forgetting to wrap some type in an Option.
Here too, if the story is that the syntax of nullable pointers is changing from *T
to Option<*T>
, then the right thing is to follow that change and use Option<*T>
in extern
s. That should be considered the overwhelming default, and again, it's the type semantically equivalent to C's T*
, so it's clearly the one to use if T*
is the type on the other side. What does this gain you? Now the compiler forces you to check for None
. If you're really sure that it can't ever be that, and don't want to have to check, then you can change the type to be just *T
. You have to make this choice about what to assume already in every function that deals with pointers coming from C, when you choose (or forget) whether to check for .is_null()
before dereferencing them. The difference is that the assumptions would be explicitly documented in the types, and checked by the compiler. Which, again, is the same decision made everywhere else in Rust, for good reason. Implicit nulls lead to bugs, and it makes no more sense to borrow that idea from C here than it does anywhere else.
There are two wrinkles. One is that *T
is easier to write than Option<*T>
, which is awkward if we want the default in extern
s to be the latter. This is unfortunate, and I'm not sure what could be done about it. (And again, if the semantics of *T
are changing to be even less like C's T*
, then I think the syntax should also be changed to not resemble it so much - I'm not sure to what.)
The other wrinkle is that, as mentioned earlier, 0
isn't used everywhere as a sentinel value for "pointers not pointing at anything": in some scenarios, it's a legal address. But on the one hand, my impression is that the world in which it is used this way is far, far larger than the world where it's not. And on the other hand, it's not like C itself escapes making this choice! In C 0
is legal literal of type T*
, while no other number is. Why, pray, does it make sense for C to distinguish the memory address 0
in this way, when it's no different from any other address? If it makes sense for C to distinguish it, so too for Rust.
Another direction you could approach it from. What's going unmentioned is the other major use case for unsafe pointers: as building blocks for Rustic smart pointers, and in data structures with invariants the type system can't express. In these cases you almost never want implicit nullability, and in fact it's a hindrance because it means that Option<Rc<T>>
doesn't automatically qualify for the null pointer optimization. I think it would be worthwhile to have a separate NonNullPtr<T>
type no matter what, if *T
is not it, for these scenarios, to be able to have explicit null checking and to have the Option
optimization propagate naturally. The question then becomes whether it makes sense to have both Option<NonNullPtr<T>>
and *T
as separate types. (And for the N+1th time, if *T
is not nullable, then I don't think it should be called *T
- I'm just sticking to that for now to avoid confusing the discussion further.)
@pnkfelix: I agree that I don't like Option 2 either. :-)
On Wed, Nov 27, 2013 at 03:00:58AM -0800, Felix S Klock II wrote:
...an application of
|p:*int| { Some(p) }
can effectively returnNone
...
Yes, clearly this is suprising too.
@nikomatsakis: On second thought, kernels wanting to use 0 pointers for something real will probably need a project-wide attribute like #[no_std]
to flip off the same optimization used for &T
.
The main motivator here is the fact that we permit casts from random integers to
*T
in safe code just to accommodate null pointers, which already have a perfectly good and safe representation. In other words, we want people to be able to write static constants that include0 as *T
. This seems like a lose-lose to me: it's awkward to write and it introduces a random safe transmute where others are not considered safe. Of course we could address the123 as *T
thing by adding anullptr
keyword a la C++. It'd be trivial to do and might be worth it.
You don't even need a keyword, you could just use something like ptr::null()
that returns the appropriately-typed null pointer (although I suppose you'd need a separate ptr::mut_null()
to get *mut T
). But I disagree that this is even necessary. It's been said time and time again that merely having a *T
is not unsafe; the only unsafe action is to dereference the *T
. To that end I see nothing wrong with 0 as *T
. You can't construct other pointers willy-nilly like this because doing so would break the invariants of the pointer (e.g. that the pointer is always valid). But *T
has no invariants at all, so the ability to construct random values of *T
is not problematic. In fact, I would go so far as to say that the current solution is the simplest solution out of everything proposed here, and in the absence of some overwhelming reason to make things more complicated, simplicity should win. We're already trying to make the language a bit simpler elsewhere (e.g. simplicity is the stated reason why we have no once stack closures), so why turn around and add in unnecessary complexity here?
Keep in mind that what when you say "*T can point anywhere at anything", you're invoking undefined behavior in LLVM.
@cmr As far as I'm aware, it's only undefined if you dereference the *T
. Merely having one is perfectly fine.
I agreed with this at first, but I don't anymore.
I was working with some embedded code where 0 was a valid and used pointer. WIth this change, I wouldn't be able to use Rust for that project. *T
is our escape hatch, and restrictions on it reduce its utility IMO (if we say that *T
is non-nullable, then I need to make a newtype and load/store with inline asm. Although, with smart pointers, that wouldn't be so bad....).
What does this change actually win us, anyway? I don't find the upsides particularly compelling.
@cmr: as @thestinger pointed out, this is just as relevant for other pointer types, i.e. you can't have &T
pointing to 0
either if &T
has the Option
optimization. Was that not relevant / couldn't it be? And there's also still the inconsistency with fn
values if *T
is nullable and those are not.
I'm not worried about those other types. *T
is my way of getting around
the other types' limitations.
On Wed, Dec 4, 2013 at 2:35 PM, Gábor Lehel notifications@github.comwrote:
@cmr https://github.com/cmr: as @thestingerhttps://github.com/thestingerpointed out, this is just as relevant for other pointer types, i.e. you can't have &T pointing to 0 either if &T is nullable. Was that not relevant / couldn't it be? And there's also still the inconsistency with fn values if *T is nullable and those are not.
— Reply to this email directly or view it on GitHubhttps://github.com/mozilla/rust/issues/10571#issuecomment-29837250 .
I'm wondering what the precise meaning of *T
is at all. The type system doesn't enforce various invariants on it (lifetime, ownership, mutability, etc.): okay. But is the programmer expected to obey them instead (which ones?), where violating them invokes undefined behaviour? For example, is it expected to point to valid data of the given type even when it's not being dereferenced? What about mutation through two different *mut T
, or mutation of the pointee of a *T
? What about the garbage collector? How does it know when to follow a *T
pointer, to find out whether Gc<U>
values are transitively reachable through it, and when not to?
What I have bouncing around in my head is that maybe there should be two types. One that shares all of the same properties and type system invariants as the safe pointer types, including non-nullability, except that it's up to the programmer, rather than the compiler, to uphold them, potentially gets followed by the GC, and so forth. This would mainly be used for things like smart pointers and data structures. And one that's truly just a raw memory address "like in C" (or perhaps instead asm?), without nothing at all assumed about it, the programmer can dereference it if she wants to or she can not, and otherwise it's just as inert as an uint
(and is probably just a newtype over one). It wouldn't have any invariants except that you shouldn't use it to break the invariants of other types which do have them. This would mainly be used for interaction with foreign code and other low-level things.
I would not expect the garbage collector to ever follow a *T
. Besides the fact that doing so is unsafe, any *T
in Rust code that actually points to a garbage-collector-managed value would be expected to have a &T
somewhere else on the stack that points to it already.
In C, you're not allowed to do pointer arithmetic outside of the bounds of an object (with a special case allowing one-byte-past-the-end), you're not allowed to do make arbitrary casts between pointers and you're definitely not allowed to dereference a null/dangling pointer. They're not just an address at all, and it's not possible to write something like an XOR-linked-list without hitting undefined behaviour due to the aliasing/derived pointer rules you must respect. LLVM inherits almost all of these semantics from C and unsafe
Rust code needs to respect them.
@kballard what about Rc<Gc<T>>
then? Or you could imagine the Gc
being embedded arbitrarily deeper inside structs, enums, other pointers and so forth.
@thestinger I know, which is why I said "or perhaps instead asm?". I don't personally care how C-like versus uint-like it is or isn't.
@glehel: Hrm, I hadn't considered the fact that Rc<T>
would hold on to T
via a *T
.
C++ programmers aren't going to be willing to make compromises for garbage collection, so it can't dictate the design of the language. If it's intended to be a fully optional feature, it's entirely a library/compiler issue and doesn't belong in language design. The standard library can use as many attributes as needed to support it.
my assumption has been that when we add a proper Gc
The design is still quite fuzzy in my head, but this may involve any/all of:
Traceable
) that a smart-pointer would say up front that it supports (and/or that its type parameters support)These topics remain to be worked out.
(I was about to say "I don't know what bearing the above has on the issue of making *T
not-nullable", but of course I remembered the core issue is whether the Gc would be expected to trace through a field of type *T
. We've had some discussion of that option internally, but my current inclination is to say "No, the Gc will not trace through *T
; gc-reachable gc-heap memory reachable via *T
has to be kept alive (and perhaps also pinned in place) via some other protocol.")
@pnkfelix: I expected it would be something like adding an attribute to fields with raw pointers the garbage collector should trace through. Anyway, adding lots of pain to low-level code is an incentive to maintain another library ecosystem.
@thestinger hmm, I admit that my definition of "some other protocol" had not included that option. But I'm going to take the liberty of reinterpreting my own comment to now include that option, (though I'm still not sure if its what I would go with).
I withdraw this suggestion.
I think the fact that
*T
is nullable is an anachronism (or will be once #10570 is fixed). We should just useOption<*T>
for nullable pointers. Anybody else have an opinion?Nominating.