-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
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
Compiler: remove the special logic of union of pointer and nil #9872
Conversation
I think passing |
Can we easily preserve this in just external function calls? That is I see the following as the main usecase: lib LibFoo
fun something(name : UInt8*)
end
LibFoo.something(nil) |
That's a special rule when calling C functions: if a nil literal is passed, it works fine. So this is muy changed by this PR. |
Ah, but lib LibFoo
fun something(name : UInt8*)
end
def something(name : String? = nil)
LibFoo.something(name)
end
something will break? |
That still works, though I don't know why. I guess if the type passed is It breaks if you have a union type: lib LibFoo
fun something(name : UInt8*)
end
name = rand < 0.5 ? nil : Pointer(UInt8).new(10)
LibFoo.something(name) The above used to work but now it doesn't. By the way, this also doesn't work: lib LibFoo
fun something(name : UInt8*)
end
name = rand < 0.5 ? nil : "hello"
LibFoo.something(name) # Error: argument 'name' of 'LibFoo#something' must be Pointer(UInt8), not (String | Nil) I feel that we could also make that work, but we'll be adding more and more special rules when using Then, passing an explicit So, in general, I think this PR is good because:
|
Also, maybe you intended to pas a pointer to the C lib but for some reason you ended up with an unintended In that sense, I think passing a |
72c866a
to
c0a3a08
Compare
Well, where I'm coming from is jhass/crystal-gobject of course and Gtk and friends have tons and tons of APIs with tons and tons of optional parameters. And in C an optional parameter means pass me a pointer or a null pointer, because that's just def something(a : String? = nil, b : String? = nil)
LibFoo.something(a ? a.to_unsafe : nil, b ? b.to_unsafe : nil)
end and more complex things and I guess I was hoping to actually reduce that boilerplate a bit :D (The passing of the for example |
It's a good point. But letting I'll wait for input from others. |
I'm fine with this if it makes the Crystal side more robust and doesn't affect existing C passing semantics. What are the arguments against simply converting My idea is that nilable types can be passed to C bindings directly if |
The question is: what if nil is accidentally passed to a function expecting a pointer? There's no way to detect that. And given we have types we should use them to provide type safety. |
I don't think C needs to detect the difference between pointers and |
What I mean is, imagine a C functions expects a non null pointer. And somehow you end up passing nil instead of a pointer. That could happen if, for example, you forgot to assign a variable in an if branch. Then you'll have a bug and the compiler won't detect it. Does that make sense? |
What I assume is this: if rand > 0.5
x = Pointer(UInt8).malloc(16)
end
LibFoo.something(x) But it's not like bugs like those are exclusive to nilable pointers: class Foo
@x = Pointer(UInt8).null
def f
if rand > 0.5
@x = Pointer(UInt8).malloc(16)
end
LibFoo.something(@x)
end
end
Foo.new.f While I agree that type-level guarantees of non-null pointers are good, they don't exactly prevent Crystal (Such a non-null pointer type can probably be done in pure Crystal actually, and once again it doesn't have to interfere with the semantics of |
I think your example of a bit different than what I said. You can always have bugs. But this change prevents more bugs than not having this change. |
I like that it simplifies some compiler's rules and leaves a more strict and predictable behavior on But I'm worried about the burden on the C bindings things will push. Maybe a Another thing we will be loosing (silently) when changing |
It's already falsey. But I regret that "feature", I would only leave I don't understand why there would be a need for |
One difference between For passing nilable types to C (which is also common in e.g. the Qt5 bindings) I ended up writing my own converter. It looks roughly like def to_lib(x : T) forall T
{% if T == Nil %}
Pointer(Void).null
{% elsif T <= Int::Primitive || ... %} # all extern types
x
{% else %}
case x
when Nil
typeof(to_lib(0.unsafe_as(T).not_nil!)).null
when .responds_to?(:to_unsafe) # except Bool
x.to_unsafe
when Int::Primitive, ... # all extern types
x
else
raise ArgumentError.new
end
{% end %}
end It goes one step further than an |
If we don't care about |
My bad. Well, a benefit is that it will be a safer transition. Note: I don't even like nil to be a falsey value 🙊 ... but I think that discussion should come later and allow the compiler to warn about a potential change of semantics before doing it.
To simplify the ternary operators for nil management here and there as @jhass pointed out. struct Pointer
def self.unnil(p)
case p
when Nil
Pointer(T).null
when Pointer
p
else
p.to_unsafe
end
end
end
pp! Pointer(UInt8).unnil(nil) # => Pointer(UInt8).null
pp! Pointer(UInt8).unnil(Pointer(UInt8).null) # => Pointer(UInt8).null
pp! Pointer(UInt8).unnil(Pointer(UInt8).malloc(1)) # => Pointer(UInt8)@0x108420e70
pp! Pointer(UInt8).unnil("hey") # => Pointer(UInt8)@0x107fc8a0c |
Oh, I guess I would call it |
Coming back to this issue:
In summary:
|
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.
Sounds good to me 👍
Should we deprecate this first, though?
addr6 = uninitialized LibC::In6Addr | ||
if LibC.inet_pton(LibC::AF_INET6, hostname, pointerof(addr6).as(Void*)) > 0 | ||
return Result::MatchFound if addr6.unsafe_as(StaticArray(UInt32, 4)) == data.as(StaticArray(UInt32, 4)*).value | ||
begin |
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.
Why this change? Is it related?
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.
san_names was nil, without this change it doesn't compile anymore
How can this be deprecated? It's a language change. |
Instead of removing the logic entirely, the compiler could issue a warning when a |
I see... Could be, I don't know. I don't think this is such a large breaking change |
I would like input from @jhass, if possible, regarding this issue before merging it or not for 0.36. Have you checked how much it affects the bindings you are used to? |
I haven't checked for the moment, but if you're talking about crystal-gobject, it's a generator so whatever change shouldn't be too much effort. |
this did require a few patches on our end but in doing so highlighted a bug where we were passing a nil-able object to a c-function expecting a string. So a worthwhile breaking change I think, thanks @asterite |
Fixes #9870
Making
Pointer(T) | Nil
be represented as a pointer is unsound: when it's a null pointer it's not clear whether that'snil
orPointer(T)
. When interfacing with C this doesn't matter, but when not it can be problematic. And since pointers can be used for things that aren't just C bindings, I think we should change this.It's a small breaking change, but nothing huge. Most people probably don't use nilable pointer types explicitly.