-
Notifications
You must be signed in to change notification settings - Fork 49
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
Make instance variables even simpler type-wise #414
Comments
Reminding myself why we can't add a zero-cost Maybe we could get by with just adding an extra, hidden So maybe the "is initialized" flag has to be set on the (This somewhat ties in with the drop flags that Rust has). |
Todo: Figure out whether Swift uses drop flags on Objective-C compatible classes or not. And if not, how Swift-created classes then are safely exposed to Objective-C which may allocate without initalizing, as well as how Swift handles unwinding through initializers. |
A few Swift compiler details on this: swiftlang/swift#33743. Doesn't explain what happens when Objective-C interop is enabled though. |
Perhaps it would be best to revert the "instance variables should work transparently like struct fields" decision, since it seems brittle from a soundness perspective, and it introduces extra code in Instead, we could do something like: declare_class!(
struct MyClass;
pub? struct MyClassIvars {
ivar1: Cell<bool>,
ivar2: AnyRustType,
}
unsafe impl ClassType for MyClass {
type Super = NSObject;
type Mutability = Mutable;
const NAME: &'static str = "MyClass";
}
);
// Generates
struct MyClass(NSObject);
// + Trait impls
impl Encode for MyClassIvars {
const ENCODING: Encoding = {
// Something calculated based on size/alignment of the ivars
};
}
impl MyClass {
const IVAR_NAME: &'static str = concat!("_", MyClass::NAME, "_ivars");
static mut IVAR_OFFSET: isize = 0; // Will be set immediately after class creation
pub? fn ivars(&self) -> &MyClassIvars { ... }
pub? fn ivars_mut(&mut self) -> &mut MyClassIvars { ... }
}
// Usage:
let obj: Id<MyClass>;
// Loads the offset twice, though that _may_ be possible to optimize away
obj.ivars().ivar1.set(true);
obj.ivars().ivar2.any_rust_method();
// Guaranteed to be the most efficient
let ivars = obj.ivars();
ivars.ivar1.set(true);
ivars.ivar2.any_rust_method();
// The whole class is borrowed for the duration, but but disjoint access to the ivars is still possible
let mut ivars = obj.ivars_mut();
ivars.ivar1 = Cell::new(true);
ivars.ivar2.any_mutating_rust_method(); Advantages over having separate
Disadvantages:
Though we may be able to mitigate the first by providing helper methods? Or maybe users should be encouraged to provide those themselves? |
Or perhaps even: struct MyClassIvars {
ivar1: Cell<bool>,
ivar2: AnyRustType,
}
declare_class!(
struct MyClass;
type Ivars = MyClassIvars;
unsafe impl ClassType for MyClass {
type Super = NSObject;
type Mutability = Mutable;
const NAME: &'static str = "MyClass";
}
// ...
); Since we don't actually care about the contents of the ivars (it can even be an enum, or something wrapped in e.g. a |
Okay so it turns out that Swift doesn't even try to handle the case where a class is not fully initialized - the following crashes because it tries to dereference a null pointer: import Foundation
class Bar {
func bar() {
print("Bar.bar")
}
}
@objc public class Foo: NSObject {
var bar = Bar.init()
deinit {
print("deinit")
self.bar.bar() // null pointer deref here
}
} #import "ProjectName-Swift.h" // Or however your bridging headers are set up
int main(int argc, const char * argv[]) {
Foo* foo = [Foo alloc]; // Allocate, but don't initialize the object
return 0;
} Now I'm wondering if we can get away with doing the same if we put more restrictions on initializers, e.g. after #440? Though it would require going through some hoops to get unwind safety (Swift doesn't do unwinding, it just traps). |
Here is a playground I made to see if it was possible to special-case types like |
We could also go the crazy route of:
Or perhaps even do method swizzling before calling All of this would never be called in the usual cases where we don't unwind, so maybe swizzling could actually make sense? |
Another option would be to override |
Also I'm only just now realizing that our current calling of We may be able to combine it with the drop flag for ivars though? |
I think I will be going the drop-flag route, since it's the only clearly safe option - in general, code is allowed to allocate and deallocate without ever initializating, and the fact that Swift doesn't handle this should be considered a bug (or a safety hole, at the very least). I will open an issue with them soon, once I reproduce the issue with the latest Swift version (if only to gain their opinion). EDIT: Done, see swiftlang/swift#68734 |
I think we might be able to use autoref specialization to do things differently depending on whether |
I guess my biggest problem here is indecision: I want to give users the ability to declare their ivars with the runtime, and use e.g. But I also want people to not need to think about it, and just dumping everything in a single ivar is just likely to be more efficient in the general case as Rust doesn't have to load a static as often to figure out the ivar offsets. Maybe there's a middle ground? Maybe we can keep the current design of letting people specify instance variables as-if they were struct members, and then we'd use autoref specialization to add an appropriate Or maybe we add an attribute Also unsure if the performance overhead of loading that static is actually acceptable or not, and if not, if we should add accessor methods for each ivar instead? (Though if we do that, how do we add EDIT: I tried in this gist to see if LLVM can elide accesses to mutable statics that it "should" know are only set once, seems like it cannot. But then again, neither can it in Objective-C. Compiling the following shows two loads of // clang -O2 -fobjc-arc -S -emit-llvm test_objective_c_ivar_access.m -o test_objective_c_ivar_access.ll
#include <AppKit/AppKit.h>
// Try replacing superclass with NSObject
@interface Abc: NSView {
@public
int a;
}
@end
// Uncomment to give the optimizer more information (e.g. that there are no private ivars)
// @implementation Abc
// @end
int foo(Abc* abc) {
int first = abc->a;
[abc hash];
return first + abc->a;
} So initially it seems like the performance characteristics would be exactly the same? Though that's not necessarily enough to say if we consider the performance characteristics acceptable! Interestingly, there is actually an optimization for subclasses of |
I looked a bit more at what Swift does for instance variables, and it seems like they don't emit type encoding information (they just use an empty string), though they do emit ivar names; though that may just be because it made their implementation easier? Swift doesn't really expose the ivars to Objective-C, and even if you specify Anyhow, just yet another argument that perhaps it's okay that we don't expose instance variables in a nice way to Objective-C (rather, users should expose property getters/setters instead). |
Okay, a few interdependent decisions to take:
I think I'm pretty certain at this point what I want for decision 2: I want option ii, to store the instance variables in a single go, so that Making this decision however does kinda drive the rest of our decisions, since we'll need some way to name the intermediate struct And leads us to decision 1, where the explicitness of option i is probably desirable now that there is only one method For decision 4, I guess we can still add some way to add instance variables with explicit names later on, but this does not really have to be integrated into the "normal" way of accessing instance variables, so I think we can honestly just punt on this as future work. This does mean that if your only instance variables are only things like Or perhaps we could re-use |
Regarding deallocation of instance variables, unsure where that should actually happen here? Under ARC and in Swift, it is done by adding a C++ destructor that This allows users to call methods in I guess we could restrict I'm a bit weary about doing so, as I haven't found any official documentation that it'll remain supported? GCC does somewhat document it, and it is emitted in binaries, so I guess it'll be fine? EDIT: We can't use |
We use
IvarDrop
mostly becauseBox
is#[fundamental]
, which means we can't ensure that noEncode
impl exist for it (even though we know that would very likely be wrong).Instead, we might be able to (ab)use HRTBs, see this playground link for the general idea. This is also what
bevy
uses for theirIntoSystem
trait.Ideally we'd be able to do avoid
IvarDrop
,IvarEncode
andIvarBool
altogether, and just do (with or without the ivar names):Still need to figure out how to actually do that in a struct (playground).
The text was updated successfully, but these errors were encountered: