-
-
Notifications
You must be signed in to change notification settings - Fork 2.5k
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
Proposal: Pinned Structs #7769
Comments
From your description I get the One more use-case to take into account, given a pinned value |
I like this addition a lot. Knowing that some structs will stay where I put them is going to be great in a lot of places. However, I must note that this addition contributes to the vaugeness of the |
Should "pinned" be an attribute on the type or an attribute on the field? |
Would we really a new CV-qualifier to enable per-value pinning? It seems that can be easily covered if const AlwaysPinned = pinned struct {
a: usize
};
const MaybePinned = struct {
a: usize
};
var x: AlwaysPinned;
var y: MaybePinned;
var z: pinned MaybePinned; // pinned just like x |
This would require CV qualifiers. Here's the case that is protected on types but not on fields: fn foo(a: Allocator) void;
foo(std.heap.page_allocator.*);
What is the relationship between the value types |
For convenience, the standard library should of course make definitions such as
Couldn't |
If I may suggest, my two cents ... I think that the Example: //introduced pinned modifier on struct
const MyStruct = pinned struct {
//...
}; I think that this special kind of Example: // Wrong, declaring by value must result in compile error
fn doSomething(arg: MyStruct) void {}
// Wrong, returning by value must result in compile error
fn returnSomething() MyStruct {}
// Corret, allocating on the heap, returning by reference
fn init(allocator: *Allocator) *MyStruct {
var ret = allocator.create(MyStruct);
ret.* = .{
// initialization ...
};
return ret;
}
const MyContainer = struct {
// It is ok to hold a pinned struct's value as a field
child: MyStruct,
};
fn main() void {
// parent holds a pinned field
var parent = MyContainer { ... };
// Correct, must be by ref
var child = &parent.child;
// Wrong, trying to copy, compile error
var bad = parent.child;
// Corret, storing on the stack, but referring as a pointer
// this pointer will not live after the function's end.
var myValue = &MyStruct {
// initialization ...
};
} Question: Should a parent struct holding a Example: // This struct must be pinned too?
const MyContainer = struct {
// Holds a pinned struct
child: MyStruct,
};
// It's ok for this struct not be pinned
const AnotherContainer = struct {
// Holds just a reference
child: *MyStruct,
};
And finally, the built-in function |
Sure, that makes sense, but what is the use case for MovableAllocator? Why would anyone ever need to use it? Why does it need to coerce to Allocator? Introducing a modifier on all types (including pointers and others) is a pretty invasive change, arguably more complicated than adding a new CV qualifier. It needs solid use cases to back it up.
This is pretty much the behavior as described above. Pinned is specifically about memory, not the programmer perspective, so IMO that name fits better. There is one example that's different though: // Wrong, returning by value must result in compile error
fn returnSomething() MyStruct {} Zig uses result location semantics (#287, #2765, #2761) for struct return values, so this does not generate a copy and is allowed. Similarly, the semantic model for value parameters allows the compiler to pass them as pointers. We may consider in the future an extension that forces the compiler to pass pinned structs as constrained pointers, which would allow that example to compile as well.
Yes, as noted in the proposal, structs containing pinned fields are implicitly also pinned.
I'm not convinced of this, I think there are valid uses for |
as you said,
Otherwise, it would not need to be public.
I don't know very much about the compiler internals, but there seem to be around 70 base types and three qualifiers, which led me to the assumption that the former set would be easier to extend. A disadvantage of making |
Just reread this proposal. One thing kind of jumps out from the motivation section:
I highlighted the parts that I, finally, noticed. From the programmer perspective, this is about copying values. Shouldn't it be called The capture case that @LemonBoy brought up is a good one. It is a minor nit, but it seems to make it more clear what you are trying to do: stop anyone/anything from making a copy. What am I missing here? Is there some other aspect to |
While I agree that However I do agree that this is not a clear-cut decision, I think @andrewrk will need to weigh in to decide this. |
These seem orthogonal to me. But this is devolving into bikeshedding so I will stop on that topic. The proposal as a whole seems fairly good. I think there are a few things that are not clear to me or seem to make this a bit trickier than apparent:
One of the things I really like about Zig's philosophy is that the intent is to guide the programmer into doing the right thing without forcing guard rails. In this case, it makes me wonder if the |
I agree with @SpexGuy: |
Let me ask something:
Why do we need to enforce var gpa1 = std.heap.GeneralPurposeAllocator(.{}) {};
var allocator1 = &gpa1.allocator;
// OK here
var mem1 = allocator1.alloc(u8, 32);
// Copy the parent struct,
var gpa2 = gpa1;
var allocator2 = &gpa2.allocator;
// But it's OK, allocator2 still valid
var mem2 = allocator2.alloc(u8, 32);
// Wrong, copy the pinned type, segfault here!
var allocator3 = gpa1.allocator;
var mem3 = allocator3.alloc(u8, 32); Why not leave to the programmer set |
Here's a quick reaction after reading through this: Since the polymorphism/@fieldParentPtr use case is the immediate problem, perhaps a |
@jumpnbrownweasel pinned memory area are also important when you store "up-pointers" from children to its parents or similar. |
Yes, I understand. I got the impression reading this that there are some unresolved and complex design issues for the general case, and I was just suggesting that a pinned modifier for fields is all that is necessary for the @fieldParentPtr case, if a partial solution might make sense initially. EDIT, added: |
What about types that are "conditionally" pinned? For example, |
Interesting. I'd extend the question to a union type where some of the cases are pinned and some aren't. Here's the behavior I would expect: const Variant = union(enum) {
allocator: Allocator, // this is pinned
num: usize, // this is not pinned
// ...
};
var variant = Variant { ... };
const v2 = variant; // compile error, variant as a whole is pinned because allocator is pinned
const n = variant.num; // ok
const a = variant.allocator; // compile error, cannot copy a pinned Allocator
switch (variant) {
.allocator => ..., // adding a capture |allocator| would be a compile error because it's a copy, but |*allocator| is ok
.num => |x| ..., // this is ok
} I don't really see any problematic cases here though. I also couldn't come up with a problematic case for |
@marler8997 A potential problematic use case I can come up with is when working with container that does reallocation, like However I actually can't think of a satisfying workaround for this case other than not marking the value type as pinned (Edit: can be partially solved using |
I feel like there is no good way to implement this, since one may want to copy the parent struct. It's probably better for interface types to contain a pointer to the concrete type. const Named = struct {
inner: *const c_void,
getNameFn: fn (*const c_void) []const u8,
pub fn getName(self: @This()) []const u8 {
return self.getNameFn(self.inner);
}
};
const Dog = struct {
name: []const u8,
fn asNamed(self: *const Dog) Named {
return .{ .inner = @ptrCast(*const c_void, self), .getNameFn = Dog_Named_getName };
}
};
fn Dog_Named_getName(self: *const c_void) []const u8 {
return @ptrCast(*const Dog, @alignCast(@alignOf(Dog), self)).name;
}
pub fn main() !void {
const std = @import("std");
var obj = Dog{ .name = "woof" };
const named = obj.asNamed();
std.log.info("{s}", .{named.getName()});
} |
I think so too, especially in light of the discovery (#10037 (comment)) that this method seems to be friendlier to optimization (devirtualization). Not to mention the other advantage of this approach: that the implementation type doesn't need to embed or "know about" the interface. Is it time to stop calling fieldParentPtr the "favored approach to runtime polymorphism"? |
This would be useful when dealing with platform specific contexts (ie.
It's a footgun since you can trivially copy the linux version of this structure, but on macos the |
This proposal supersedes #3803 and #3804 as the plan going forward for pinned structs, as discussed in the design meetings.
Motivation
A common pattern in Zig is to embed a struct in a larger struct, pass pointers to the inner struct around, and then use
@fieldParentPtr
to retrieve the outer struct. This is central to our current favored approach to runtime polymorphism. However, it's easy to accidentally make a value copy of inner structs like this, causing hard to debug memory corruption problems or segfaults.A similar problem exists with types that may contain pointers to data within themselves. As an example, consider an array with an inline stack buffer. Copying this value would yield a broken object, with a pointer pointing into the previous instance.
Finally, at the extreme end of this is async frames. Async frames may contain pointers into themselves, and so cannot safely be copied.
Where possible, the compiler should give an error if you try to copy any of these types. This is also important for the current plan for Result Location Semantics (see #2765), which is very implicit. In the case of these types, it's extremely important to know if RLS is working (and to fail the compile if not).
Proposal
Introduce the
pinned
keyword. This keyword can appear as a modifier to an anonymous type declaration, similar topacked
andextern
.pinned
may coexist withpacked
orextern
, and is compatible withstruct
andunion
types. It is not allowed (or ever needed) onopaque
, because opaque values cannot be copied. It is also not allowed onenum
orerror
, because those types cannot contain self pointers.If a type is
pinned
, it means that the address of the type is part of its value, and the compiler is not allowed to copy an instance to a new location. But braced initialization is still allowed, and the address may be forwarded through result location semantics.Any composite type which contains a field which is pinned is also considered pinned, and the same restrictions apply. This includes error unions and optionals, so #2761 will also be important for working with these types.
Frame types will also be considered pinned.
It's important to note that, despite some superficial similarities,
pinned
is not related to RAII. Pinned will not enforce that your deinitialization code is run, nor will it prevent you from overwriting fields that needs to be cleaned up, or even whole structs with braced initializers. It only prevents you from accidentally relocating an existing object.Examples
Other Notes
Though we have chosen here to put
pinned
on type declarations, for some uses it is actually a property of values. For example, an allocator with no attached state would not need to be pinned. However, introducingpinned
as a new CV-qualifier seems like a much more invasive change, and it's not clear that it's worth it.The text was updated successfully, but these errors were encountered: