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

Define methods in extern_class! macro #161

Closed
wants to merge 3 commits into from
Closed

Define methods in extern_class! macro #161

wants to merge 3 commits into from

Conversation

madsmtm
Copy link
Owner

@madsmtm madsmtm commented Jun 11, 2022

Part of #67.

Something that will greatly help consumers create their own Objective-C classes. For example, I'd like to use this in winit to define safe abstractions over NSApplication, NSWindow, NSView, NSResponer and so on.

TODO:

@madsmtm madsmtm added enhancement New feature or request A-framework Affects the framework crates and the translator for them labels Jun 11, 2022
@madsmtm madsmtm marked this pull request as draft June 12, 2022 05:07
@madsmtm
Copy link
Owner Author

madsmtm commented Jun 14, 2022

API idea (doesn't have to actually parse the derives yet, just ensure that they're there - makes it possible to extend the macro in the future):

object! {
    #[derive(Debug, PartialEq, Eq, Hash, AsRef, AsMut, Deref, DerefMut, Borrow, BorrowMut)]
    unsafe pub struct NSArray<T, O: Ownership>: NSObject {
        item: PhantomData<Id<T, O>>,
    }
}

Alternative:

object! {
    unsafe pub struct NSArray<T, O: Ownership>: NSObject {
        item: PhantomData<Id<T, O>>,
    }
    // Deref, DerefMut, AsRef, AsMut, Borrow and BorrowMut are implied
    impl Debug;
    impl PartialEq;
    impl Eq;
    impl Hash;
    // Possibility:
    unsafe impl Default;
    
    // Some way to say "we add inherent `class` method as well"?
}

@madsmtm
Copy link
Owner Author

madsmtm commented Jun 19, 2022

Maybe rename to external_object! or external_class! to not confuse with whatever we'll do about user-declared classes in #30

@madsmtm
Copy link
Owner Author

madsmtm commented Jun 19, 2022

Idea:

object! {
    // Struct declaration as above

    unsafe impl NSSomeObject {
        // Class method.
        // Selector could probably be inferred from the function name a lot of the time, but let's not worry about that just yet!
        #[selector = "new"]
        pub fn new() -> Option<Id<Self, Owned>>;

        // Instance method.
        #[selector = "abc:defGhi:"]
        pub fn abc_def_ghi(&self, arg1: &Object, arg2: i32);

        // Private function, allows user to create a safe wrapper.
        #[selector = "returnsPointer"]
        fn pointer_param_impl(&self) -> *const c_void;

        // Unsafe function, user has to ensure validity of pointer.
        #[selector = "setPointerParam:"]
        pub unsafe fn set_pointer_param(&mut self, param: *const c_void);
    }
}

// Would generate:

impl NSSomeObject {
    #[doc(alias = "new")]
    pub fn new() -> Option<Id<Self, Owned>> {
        // Infer `msg_send_id` from return type
        // Infer `Self::class()` from it being a class method (no `&self/&mut self`)
        unsafe { msg_send_id![Self::class(), new] }
    }

    #[doc(alias = "abc:defGhi:")]
    pub fn abc_def_ghi(&self, arg1: &Object, arg2: bool) {
        // Assume `Bool` is desired!
        unsafe { msg_send![self, abc: arg1, defGhi: Bool::from(arg2)] }
    }

    #[doc(alias = "returnsPointer")]
    fn pointer_param_impl(&self) -> *const c_void {
        unsafe { msg_send![self, returnsPointer] }
    }

    #[doc(alias = "setPointerParam:")]
    pub unsafe fn set_pointer_param(&mut self, param: *const c_void) {
        unsafe { msg_send![self, setPointerParam: param] }
    }
}

#[test]
fn test_encoding() {
    let cls = NSSomeObject::class();
    assert_eq!(verify_message::<(), *const Self>(cls.superclass(), sel!(new)));
    assert_eq!(verify_message::<(&Object, i32), ()>(cls, sel!(abc:defGhi:)));
    assert_eq!(verify_message::<(), *const c_void>(cls, sel!(returnsPointer)));
    assert_eq!(verify_message::<(*const c_void,), ()>(cls, sel!(setPointerParam:)));
}

This is close to the macro ideas from #30, but still distinctly different in that this only helps with accessing the class (we really need both)!

It could even restrict things like returning &Object, forcing you to return Option<&Object> or Option<Id<Object, O>> instead.

@madsmtm
Copy link
Owner Author

madsmtm commented Jun 19, 2022

Unsure of how much of this belongs in objc2 and how much belongs in objc2_foundation - am probably going to make the macro in objc2_foundation, and then we can always move part of it to objc2 if it makes sense)

@madsmtm madsmtm mentioned this pull request Jun 19, 2022
8 tasks
@marysaka
Copy link

Unsure of how much of this belongs in objc2 and how much belongs in objc2_foundation - am probably going to make the macro in objc2_foundation, and then we can always move part of it to objc2 if it makes sense)

I do believe it should just stay in objc2_foundation yeah considering how rare anything doesn't inherit from NSObject

Idea:

object! {
    // Struct declaration as above

    unsafe impl NSSomeObject {
        // Class method.
        // Selector could probably be inferred from the function name a lot of the time, but let's not worry about that just yet!
        #[selector = "new"]
        pub fn new() -> Option<Id<Self, Owned>>;

        // Instance method.
        #[selector = "abc:defGhi:"]
        pub fn abc_def_ghi(&self, arg1: &Object, arg2: i32);

        // Private function, allows user to create a safe wrapper.
        #[selector = "returnsPointer"]
        fn pointer_param_impl(&self) -> *const c_void;

        // Unsafe function, user has to ensure validity of pointer.
        #[selector = "setPointerParam:"]
        pub unsafe fn set_pointer_param(&mut self, param: *const c_void);
    }
}

// Would generate:

impl NSSomeObject {
    #[doc(alias = "new")]
    pub fn new() -> Option<Id<Self, Owned>> {
        // Infer `msg_send_id` from return type
        // Infer `Self::class()` from it being a class method (no `&self/&mut self`)
        unsafe { msg_send_id![Self::class(), new] }
    }

    #[doc(alias = "abc:defGhi:")]
    pub fn abc_def_ghi(&self, arg1: &Object, arg2: bool) {
        // Assume `Bool` is desired!
        unsafe { msg_send![self, abc: arg1, defGhi: Bool::from(arg2)] }
    }

    #[doc(alias = "returnsPointer")]
    fn pointer_param_impl(&self) -> *const c_void {
        unsafe { msg_send![self, returnsPointer] }
    }

    #[doc(alias = "setPointerParam:")]
    pub unsafe fn set_pointer_param(&mut self, param: *const c_void) {
        unsafe { msg_send![self, setPointerParam: param] }
    }
}

#[test]
fn test_encoding() {
    let cls = NSSomeObject::class();
    assert_eq!(verify_message::<(), *const Self>(cls.superclass(), sel!(new)));
    assert_eq!(verify_message::<(&Object, i32), ()>(cls, sel!(abc:defGhi:)));
    assert_eq!(verify_message::<(), *const c_void>(cls, sel!(returnsPointer)));
    assert_eq!(verify_message::<(*const c_void,), ()>(cls, sel!(setPointerParam:)));
}

This is close to the macro ideas from #30, but still distinctly different in that this only helps with accessing the class (we really need both)!

It could even restrict things like returning &Object, forcing you to return Option<&Object> or Option<Id<Object, O>> instead.

I really like this definition overall 👍
I do believe it should assume a method to be unsafe if a safety documentation isn't provided.

Another thing: How would this translate for properties?

I have been working a bit on some UIKit stuffs in the past weeks and this is what i came up in general (while basing it on some stuffs from objc2):

create_objc_object!(UIColor, NSObject);

impl_objc_properties!(
    UIColor,

    static blackColor: UIColor,
    static darkGrayColor: UIColor,
    static lightGrayColor: UIColor,
    static whiteColor: UIColor,
    static grayColor: UIColor,
    static redColor: UIColor,
    static greenColor: UIColor,
    static blueColor: UIColor,
    static cyanColor: UIColor,
    static yellowColor: UIColor,
    static magentaColor: UIColor,
    static orangeColor: UIColor,
    static purpleColor: UIColor,
    static brownColor: UIColor,
    static clearColor: UIColor,
);

create_objc_object!(UIResponder, NSObject);

// TODO: UIResponder definition

create_objc_object!(UIView, UIResponder);

// UIViewRendering
impl_objc_properties!(
    UIView,

    mut clipsToBounds setClipsToBounds: value<bool>,
    mut backgroundColor setBackgroundColor: UIColor,
    mut isOpaque setOpaque: value<bool>,
    mut clearsContextBeforeDrawing setClearsContextBeforeDrawing: value<bool>,
    mut isHidden setHidden: value<bool>,
    mut contentMode setContentMode: value<UIViewContentMode>,
    // contentStretch (deprecated)
    mut alpha setAlpha: value<CGFloat>,

    // Since iOS 8.0
    mut maskView setMaskView: UIView,

    // TODO: null_resettable
    // Since iOS 7.0
    mut tintColor setTintColor: UIColor,

    // Since iOS 7.0
    mut tintAdjustmentMode setTintAdjustmentMode: value<UIViewTintAdjustmentMode>,
);

macro implementation is here if you are interested

@madsmtm madsmtm mentioned this pull request Jul 5, 2022
@madsmtm
Copy link
Owner Author

madsmtm commented Jul 6, 2022

I've managed to create a working prototype of the idea presented in #161 (comment), but I'm not really happy with how brittle it is in detecting Id and bool, and how complex it suddenly became!

I took parts of this PR that I'm content with out (looking at it now, essentially the same as your create_objc_object) into #188, the rest will probably have to wait until I've mulled it over a bit more.

I do believe it should assume a method to be unsafe if a safety documentation isn't provided.

A (declarative) macro would have quite a hard time reliably inspecting whether there is a # Safety comment or not. Instead, I marked the entire impl NSSomeObject as unsafe, as a kind of "the user typed this unsafe, they must now assert that the below functions are correct".

How would this translate for properties?

I have been working a bit on some UIKit stuffs in the past weeks and this is what i came up in general

Honestly haven't thought about properties yet at all... Your macros look nice though, I would probably go for a more "native"-looking syntax (at least in one sense of the word "native") than you have, and use @property thing, @property(readonly) thing and such, but again, don't really know yet.

basing it on some stuffs from objc2

Cool to see that someone is already starting to use it!

@marysaka
Copy link

marysaka commented Jul 6, 2022

I took parts of this PR that I'm content with out (looking at it now, essentially the same as your create_objc_object) into #188, the rest will probably have to wait until I've mulled it over a bit more.

Awesome, I will try to handle the transition when I get time 👍

A (declarative) macro would have quite a hard time reliably inspecting whether there is a # Safety comment or not. Instead, I marked the entire impl NSSomeObject as unsafe, as a kind of "the user typed this unsafe, they must now assert that the below functions are correct".

That seems fair. The real problem is that currently a lot of crate using objc interactions just wrap around calls to objc_msgSend and call it day without taking into account the safety of those APIs. I guess people just need to be better educated 😅

Honestly haven't thought about properties yet at all... Your macros look nice though, I would probably go for a more "native"-looking syntax (at least in one sense of the word "native") than you have, and use @Property thing, @Property(readonly) thing and such, but again, don't really know yet.

I was kind of thinking doing that at first, but it is quite hard to handle especially if you have multiple attributes passed in the @Property.
I suppose a proc macro could do the job but I'm pretty reticent to have any dependencies on those...

Cool to see that someone is already starting to use it!

Thanks ^^
I have been working the past weeks on tools to build iOS apps entirely in Rust without depending on Xcode and your crates does help quite a bit

@madsmtm
Copy link
Owner Author

madsmtm commented Jul 6, 2022

I was kind of thinking doing that at first, but it is quite hard to handle especially if you have multiple attributes passed in the @Property

Yeah that sounds like it might be a problem.

I suppose a proc macro could do the job but I'm pretty reticent to have any dependencies on those...

Agree (in particular because I know declarative macros fairly well by now, while I'm really terrible at writing proc macros...)

I have been working the past weeks on tools to build iOS apps entirely in Rust without depending on Xcode and your crates does help quite a bit

Cool, do let me know when/if you have something to show! Relatedly, at some point I'll integrate objc2 into cacao, I could see that crate becoming a viable option for safe Objective-C framework interfacing.

@marysaka
Copy link

marysaka commented Jul 6, 2022

Cool, do let me know when/if you have something to show!

Will make sure to let you know 👍

Relatedly, at some point I'll integrate objc2 into cacao, I could see that crate becoming a viable option for safe Objective-C framework interfacing.

Never heard about Cacao but after a quick look, it seems enough for all my UIKit needs so thanks for the info ^^

@madsmtm madsmtm mentioned this pull request Jul 6, 2022
@madsmtm
Copy link
Owner Author

madsmtm commented Jul 6, 2022

Working on this a bit more:

Currently the syntax is

unsafe impl {
    #[unrelated_attribute]
    #sel!(new)
    pub fn new() -> Option<Id<Self, Owned>>;

    #sel!(abc:defGhi:)
    pub fn abc_def_ghi(&self, arg1: &Object, arg2: i32);
}

Because I couldn't find a good way to extract the sel from an arbitary #[...]. Perhaps we could use @ instead of #: @[sel!(abc:defGhi:)]?

More importantly, Option<Id<...>> is detected verbatim (e.g. Option<rc::Id<...>> wouldn't work), but similar to how we have msg_send_id!, perhaps it would make sense to have the user explicitly specify "yes, I am returning an Id and would like you to do magic to make this work"? Something like #id_sel!(new), #id,sel!(new), #msg_send_id,sel!(new), #sel_id!(new) or similar? Likewise for bool.

@madsmtm madsmtm changed the title Expose object! macro Define methods in extern_class! macro Jul 7, 2022
@madsmtm madsmtm mentioned this pull request Jul 24, 2022
3 tasks
@madsmtm
Copy link
Owner Author

madsmtm commented Aug 1, 2022

I managed to find a way so that you can use normal attribute syntax, e.g. #[sel(myMethod:)], and used that in #215, so that is indeed feasible!

Instead of combining "type declaration" and "method declaration" into one macro, I've chosen to split it out into two separate macros:

This makes it much easier to see and document what's happening, and extern_methods! can also be used with classes created by declare_class!.

@madsmtm
Copy link
Owner Author

madsmtm commented Aug 29, 2022

A bit of time has passed, and I've mostly implemented everything in this PR the "right" way (using traits instead of magically looking at the return type). Final part of this will be resolved by #244, so will close this now.

I've moved the property ideas to #256 so it won't be forgotten.

@madsmtm madsmtm closed this Aug 29, 2022
@madsmtm madsmtm deleted the object-macro branch August 29, 2022 15:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-framework Affects the framework crates and the translator for them enhancement New feature or request
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants