Skip to content
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

Question: How to add Rust properties to Objective-C objects? #194

Closed
coolbluewater opened this issue Jul 16, 2022 · 11 comments
Closed

Question: How to add Rust properties to Objective-C objects? #194

coolbluewater opened this issue Jul 16, 2022 · 11 comments
Labels
A-objc2 Affects the `objc2`, `objc-sys` and/or `objc2-encode` crates enhancement New feature or request

Comments

@coolbluewater
Copy link

Hi, I'm looking for a way to use normal Rust fields within instances. This would allow Objective-C declared methods to work with rust fields per-instance. So far I haven't seen any examples of this.

FYI, I've used Xamarin iOS for a long time, and this is something that they have figured out really well. Briefly, each Objective-C instance is exposed to C# as a regular C# object. Under the covers, the C# object holds a pointer to the objective-C instance. The reverse is also true, although I haven't located the exact implementation, probably here.

Appreciate any thoughts/suggestions.

@madsmtm madsmtm added enhancement New feature or request A-objc2 Affects the `objc2`, `objc-sys` and/or `objc2-encode` crates labels Jul 16, 2022
@madsmtm
Copy link
Owner

madsmtm commented Jul 16, 2022

I'm working on something that could enable part of this in #190 for simple (T: Encode) types, this would allow you to use self.someIvar in a very natural way.

Other types, e.g. T: Drop types like Box and Id are a bit more involved, since we'll need to do a conversion step and add proper dealloc methods, but could be doable.

@madsmtm
Copy link
Owner

madsmtm commented Jul 16, 2022

Note that I've never used Xamarin before, and am not very well-versed in C#, a few code examples/ideas in pseudo-Rust of what you would like to achieve would help me a lot with understanding the use-case?

Perhaps related upstream issue: SSheldon/rust-objc#56

@coolbluewater
Copy link
Author

coolbluewater commented Jul 16, 2022

Xamarin lets you write iOS and macOS apps entirely in C#. It cleverly exposes Objective-C types as C# types. It also provides ready-made bindings for the various Apple SDKs. Here's a 10-second example:

using UIKit;

public class MyView : UIView {
    UILabel label;

    public override void ViewDidLoad() {
        this.label = new UILabel();
        this.label.Text = "Hello!";
        this.AddSubView(this.label);
    }
}

Some features seen in this example:

  1. A C# class that derives from UIView which is provided by the Xamarin-supplied UIKit binding library.
  2. Names have been sanitized to match C# casing conventions. (IMHO, this is not necessary for rust bindings.)
  3. A method override that needs no selectors and is declared/defined just once.
  4. C# strings convert to NSString transparently.
  5. Fields that are normal C# fields, not ivars. This is the most useful feature, and is what I'm trying to figure out at the moment.

I'll add that Xamarin is basically doing what objc/objc2 are attempting to do, but they went whole-hog and produced a gigantic ecosystem and ended up as a very successful startup that Microsoft eventually bought.

But the core idea is just a set of bindings between Objective-C and C#.

Now for some detail. C# objects live in their own heap, and their lifetime is controlled by the C# garbage collector. Objective-C types are owned by the Objective-C runtime. To connect the two, a C# UIView instance stores a pointer to the underlying Objective-C UIView and also retain's it. Xamarin had lots of bugs with lifetime management that they've worked out over the past decade(!)

@madsmtm
Copy link
Owner

madsmtm commented Jul 16, 2022

Thanks for the example! I'll try to provide a similar example in Rust, assuming that:

  • The linked PR above is finished
  • I find a way to store an Id<T, O> (retained pointer) in instance variables
  • A hypothetical future version of a higher-level crate is created, and it contains bindings to common UIKit functionality.
use some_crate::uikit::{UIView, UILabel, UIResponder};
use objc2_foundation::{declare_class, NSObject};
use objc2::msg_send_id;
use objc2::rc::{Id, Shared};

declare_class! {
    pub unsafe struct MyView: UIView, UIResponder, NSObject {
        // Ideally the label should be created in `init`, then we wouldn't need an `Option` here
        label: Option<Id<UILabel, Shared>>,
    }

    unsafe impl {
        @sel(viewDidLoad)
        fn view_did_load(&self) {
            let mut label = UILabel::new();
            label.set_text("Hello!");
            self.label.set(Some(label.into_shared()));
            // If taking `&mut self`, whether that is sound depends on further stuff
            // *self.label = Some(label.into_shared());
            self.add_sub_view(&self.label);
        }
    }
}

impl MyView {
    pub fn new() -> Id<Self, Shared> {
        unsafe { msg_send_id![Self::class(), new] }
    }
}

The first thing you'll notice is that this is naturally not nearly as clean, since we're just a library, Rust and Objective-C are quite differnet, and Xamarin is a whole ecosystem with compiler plugins and whatnot. However, I think we're actually doing fairly good! Relating this to the features you pointed out:

  1. Providing binding libraries for anything other than the Foundation framework is explicitly out of scope for objc2, but I'll probably work together with cacao to provide user-friendly bindings for some of the most commonly used frameworks.
  2. This could in theory be done automatically in many cases, but I chose not to tackle that yet.
  3. Done.
  4. Won't be done automatically, that would be against the "Rust spirit" (since the conversion wouldn't be zero-cost), but we could provide better utilities for this, see e.g. Static NSString #53.
  5. Done.

cacao takes this even further, allowing you to use Rust traits for overriding common stuff like custom delegates (haven't investigated this much though):

use cacao::view::ViewDelegate;

pub struct MyViewDelegate {
    label: Option<UILabel>,
}

impl ViewDelegate for MyViewDelegate {
    fn did_load(&mut self) {
        let mut label = UILabel::new();
        label.set_text("Hello!");
        *self.label = Some(label.into_shared());
        self.add_sub_view(&self.label);
    }
}

Now for some detail. C# objects live in their own heap, and their lifetime is controlled by the C# garbage collector. Objective-C types are owned by the Objective-C runtime. To connect the two, a C# UIView instance stores a pointer to the underlying Objective-C UIView and also retain's it. Xamarin had lots of bugs with lifetime management that they've worked out over the past decade(!)

I think we'd probably want to allocate as much as possible in the Objective-C runtime, exactly to avoid such issues (the first example does this).

@coolbluewater
Copy link
Author

coolbluewater commented Jul 17, 2022

Thanks.

  1. A method override that needs no selectors and is declared/defined just once.
  1. Done.

Interesting.

  • While editing methods, does autocomplete/intellisense work?
  • During debugging, can we set breakpoints in methods and inspect local variables?
  • During debugging, can we inspect rust properties (ivars) easily?

@madsmtm
Copy link
Owner

madsmtm commented Jul 17, 2022

While editing methods, does autocomplete/intellisense work?

Hmm... Maybe? I'm not really sure, it definitely could but I guess it depends on how rust-analyzer (or whatever you use) is implemented with regards to macros.

During debugging, can we set breakpoints in methods and inspect local variables?

Yup, this should definitely be possible!

During debugging, can we inspect rust properties (ivars) easily?

I'm pretty bad at using debuggers myself, so I don't know, depends on how they handle implementation details of the ivar (it works using Deref, if that helps you). Probably won't work as well as Objective-C or Xamarin though!

@coolbluewater
Copy link
Author

TBH these are the things I'm more interested in getting right.
I've read the goals of objc2, but could you summarize the major differences between objc2 and objc at this point?

@madsmtm
Copy link
Owner

madsmtm commented Jul 19, 2022

While editing methods, does autocomplete/intellisense work?

Should clarify: within the method, autocomplete/intellisense could work somewhat. But importantly, they can't help you write the method signature, you have to do that yourself (hence the unsafe)! I know, it would be wonderful if such things could just work, but we're not a C-compiler, we can't inspect all the C headers that you may (or may not) have lying around.

Hence it is something I won't tackle in objc2 because that makes the scope overwhelmingly large (UIKit and AppKit are huge, and would conflict with objc2's mission), but again, this is in-scope for cacao and some work to find more ergonomic solutions has already been done there.

could you summarize the major differences between objc2 and objc at this point?

I think there's a lot of things, including fixing around 75% of all open objc issues and vastly improving soundness, but I digress; the key innovation is probably the msg_send_id! macro, which helps you with following Cocoa's memory management rules (very much like ARC; automatically calling retain/release when required).

these are the things I'm more interested in getting right.

If you point me towards some resources for using debuggers in Rust (e.g. a GDB or LLDB setup) I could probably try it out and see how we fare, and check if I could improve the situation, but again, objc2 is just a library, so don't expect the wonders that a compiler like Xamarin can pull.

You can also try it out yourself, the declaration example in objc2-foundation should be fairly serviceable for doing such tests.

@coolbluewater
Copy link
Author

Thanks for the responses, let you know if more questions come up.
Re: debugging, I'm using the Xcode debugger with a small app to call into the rust code. At some point I'll try to use VSCode as well, which may require attaching to an already running app. After that the experience should ideally just
be regular IDE based debugging.

@madsmtm
Copy link
Owner

madsmtm commented Jul 27, 2022

Re debugging ivars: I tried it out, it is not very good today, but can be improved with the unstable #![debugger_visualizer] attribute.

I would be willing to put in the work to do so, but that feature doesn't support LLDB (which macOS/XCode uses) yet, so the point is kinda moot until then.

@madsmtm
Copy link
Owner

madsmtm commented Jan 15, 2023

I'll close this issue in favour of #352, which tracks our debugger support.

Feel free to re-open, or create a new issue if you have any more questions.

(also, time has passed and we now have icrate, which should make things even easier to use).

@madsmtm madsmtm closed this as completed Jan 15, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-objc2 Affects the `objc2`, `objc-sys` and/or `objc2-encode` crates enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

2 participants