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

The API generated for objective-c categories is inconvenient and requires implementation knowledge #2709

Open
afranchuk opened this issue Jan 5, 2024 · 1 comment

Comments

@afranchuk
Copy link

Bindgen code from objective-c categories results in traits like CLASS_CATEGORY to be generated. This is not intuitive for a few reasons:

  • I didn't even realize that bindgen supported categories. I was allowlisting the classes I wanted and saw that methods were missing. I looked in the files and realized that those methods were in category blocks and just assumed that it was unsupported. Only later did I realize that I had to allowlist CLASS_CATEGORY (or more broadly CLASS_.*).
  • Category names (and contents!) are implementation details. They are used in the implementation to organize things but are not exposed in any way in normal objective-c APIs. Creating a separate rust API is an unfortunate mismatch for developers.
  • Because the categories are traits, you need to import them by name. I'd prefer not to use use my_crate_bindings::* to pull in all the category traits, so I need to know their names to import them.

This is a tricky problem to work around, but if there is some way to address some of these points it'd make the developer interaction with the generated code more similar to the interaction when using objective-c.

Input C/C++ Header

@interface Foo
-(void)method;
@end

@interface Foo (BarCategory)
-(void)categoryMethod;
@end

Bindgen Invocation

$ bindgen input.h --objc-extern-crate -- -x objective-c

Actual Results

/* automatically generated by rust-bindgen */

#![allow(
    dead_code,
    non_snake_case,
    non_camel_case_types,
    non_upper_case_globals
)]
#![cfg(target_os = "macos")]

#[macro_use]
extern crate objc;
#[allow(non_camel_case_types)]
pub type id = *mut objc::runtime::Object;
#[repr(transparent)]
#[derive(Clone, Copy)]
pub struct Foo(pub id);
impl std::ops::Deref for Foo {
    type Target = objc::runtime::Object;
    fn deref(&self) -> &Self::Target {
        unsafe { &*self.0 }
    }
}
unsafe impl objc::Message for Foo {}
impl Foo {
    pub fn alloc() -> Self {
        Self(unsafe { msg_send!(objc::class!(Foo), alloc) })
    }
}
impl IFoo for Foo {}
pub trait IFoo: Sized + std::ops::Deref {
    unsafe fn method(self)
    where
        <Self as std::ops::Deref>::Target: objc::Message + Sized,
    {
        msg_send!(self, method)
    }
}
impl Foo_BarCategory for Foo {}
pub trait Foo_BarCategory: Sized + std::ops::Deref {
    unsafe fn categoryMethod(self)
    where
        <Self as std::ops::Deref>::Target: objc::Message + Sized,
    {
        msg_send!(self, categoryMethod)
    }
}

And in code using the generated code:

use my_crate_bindings::{Foo, IFoo, Foo_BarCategory};

let f = Foo::alloc();
unsafe { f.categoryMethod() };

Expected Results

I'm not entirely sure the best approach, but some suggestions:

  • Maybe allowlist_item("Class") can include categories automatically? I figure this is somewhat unlikely given that the allowlist acts on the rust API.
  • Change all categories into impl Foo instead. This is how I expected bindgen to work, though it doesn't mesh well with inheritance (see Objective-c categories aren't included in inheritance traits and impl blocks #1779) and then the trait IFoo might seem weird. I don't think there's a strict need to keep them separate given that categories are invisible in the objective-c API and exist purely in syntax. Alternatively you could merge all included categories into the one trait IFoo (and then inheritance will work, at the cost of what I imagine would be a bit more juggling in the bindgen logic).
  • Put all category traits into a module (either as a whole for everything generated or for each class). For example, I would be much less hesitant to use my_crate_bindings::{Foo, FooCategories::*}. This avoids my needing to know the category names, though I still need to be aware of how bindgen implements categories.
@simlay
Copy link
Contributor

simlay commented Jan 5, 2024

Change all categories into impl Foo instead.

This is not a bad idea. I've been tempted to try this. One thing I'd say is that this will result in a much bigger generated source. Take for example these uikit-sys bindings. It's been a while since I've generated them but IIRC, the generated rust was ~100k to ~300k lines of rust and took a few minutes to compile - it was all in one file and I don't think that can be parallelized. Admittedly, it also generates all the stuff for Foundation. The CIImageProvider category of NSObject for example has a lot of implementers.

Anyway, I commend you for looking into the realm of rust objective-c. Generating ergonomic and reasonable bindings from objective-c headers is pretty hard. The objc2 crate is pushing forward on generating ergonomic bindings for apple frameworks in the icrate crate.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants