madsmtm / objc2

Bindings to Apple's frameworks in Rust
https://docs.rs/objc2/
MIT License
281 stars 35 forks source link

declare_class generates unusable classes #604

Open Yuri6037 opened 3 weeks ago

Yuri6037 commented 3 weeks ago

Hello,

After spending time at building a hybrid Rust + Objective C framework, I just did an experiment with the declare_class macro present in this crate to remove entirely the Objective C part and do everything in Rust. Unfortunately it appears to not generate usable classes.

Consider the following Rust code:

declare_class! {
    pub struct BPXContainer;

    // SAFETY:
    // - The superclass NSObject does not have any subclassing requirements.
    // - Interior mutability is a safe default.
    // - `BPXContainer` does not implement `Drop`.
    unsafe impl ClassType for BPXContainer {
        type Super = NSObject;
        type Mutability = mutability::Mutable;
        const NAME: &'static str = "BPXContainer";
    }

    impl DeclaredClass for BPXContainer {
        type Ivars = ();
    }

    unsafe impl BPXContainer {
        #[method_id(create:)]
        fn create(ty: u8) -> Id<BPXContainer> {
            let obj = Self::alloc().set_ivars(());
            unsafe { msg_send_id![super(obj), init] }
        }
    }
}

The above code is built as a cdylib. With the following Objective-C code:

#import <Foundation/Foundation.h>

@interface BPXContainer
+(BPXContainer*)create:(uint8_t)ty;
@end

int main() {
    @autoreleasepool {
        BPXContainer *container = [BPXContainer create:'P'];
    }
}

The command line I used to build the Objective-C code is the following:

clang test.m -framework Foundation -L target/debug -lbpxbridge

I expected the code to compile, link and run creating a BPXContainer object which currently is of no use, but instead this happened:

Undefined symbols for architecture arm64:
  "_OBJC_CLASS_$_BPXContainer", referenced from:
       in test-a4125c.o
ld: symbol(s) not found for architecture arm64
clang: error: linker command failed with exit code 1 (use -v to see invocation)

Indeed after a quick nm command we see no symbols exported. I've seen that after calling the class() function in a C exported function named register the symbols starts to appear obviously no _OBJC_CLASS_$_BPXContainer symbol is created...

I suppose your crate is not actually declaring classes but instead is expecting to dynamically register classes with an existing runtime, is that correct? If so is there any plan or workaround to support actual class declaration instead of just dynamic registration?

madsmtm commented 3 weeks ago

Yes, your statement here is entirely correct:

your crate is not actually declaring classes but instead is expecting to dynamically register classes with an existing runtime

That said, the runtime that it is registered with is the Objective-C runtime itself, so you can use this class from Objective-C. All of this is somewhat documented in declare_class!, the following line:

The class is guaranteed to have been created and registered with the Objective-C runtime after the ClassType::class function has been called.

So what you need to do is call <BPXContainer as ClassType>::class() sometime before using the class from Objective-C.

This can be accomplished with either a helper function:

// rust
[no_mangle]
extern "C" fn rust_init() {
    let _ = BPXContainer::class();
}

// C
void rust_init(void);

int main() {
    rust_init();
    // Rest of main
}

Or something like the ctor crate:

#[ctor::ctor]
fn init_bpxcontainer() {
    let _ = BPXContainer::class();
}

If so is there any plan [...] to support actual class declaration instead of just dynamic registration?

I have tried to design the macro such that this would be possible in the future. The reason it's not implemented yet is that it is quite hacky in current Rust, we need to emit statics in very special manner that isn't really compatible with rustc's splitting of files into multiple codegen units, and I wanted to make sure that the crate is built on something robust instead.

There exists a project called objrs that has done some of this, I want to add experimental support for the same in objc2, but it's a low priority.

Yuri6037 commented 3 weeks ago

Yes, your statement here is entirely correct:

your crate is not actually declaring classes but instead is expecting to dynamically register classes with an existing runtime

That said, the runtime that it is registered with is the Objective-C runtime itself, so you can use this class from Objective-C. All of this is somewhat documented in declare_class!, the following line:

The class is guaranteed to have been created and registered with the Objective-C runtime after the ClassType::class function has been called.

So what you need to do is call <BPXContainer as ClassType>::class() sometime before using the class from Objective-C.

This can be accomplished with either a helper function:

// rust
[no_mangle]
extern "C" fn rust_init() {
    let _ = BPXContainer::class();
}

// C
void rust_init(void);

int main() {
    rust_init();
    // Rest of main
}

Or something like the ctor crate:

#[ctor::ctor]
fn init_bpxcontainer() {
    let _ = BPXContainer::class();
}

If so is there any plan [...] to support actual class declaration instead of just dynamic registration?

I have tried to design the macro such that this would be possible in the future. The reason it's not implemented yet is that it is quite hacky in current Rust, we need to emit statics in very special manner that isn't really compatible with rustc's splitting of files into multiple codegen units, and I wanted to make sure that the crate is built on something robust instead.

There exists a project called objrs that has done some of this, I want to add experimental support for the same in objc2, but it's a low priority.

I see thanks for the information. Unfortunately registering the classes is not sufficient as Xcode refuses to link against dynamically looked up symbols it requires the selectors and the class itself to be statically generated (with those special symbols). This means your crate is unusable from Xcode directly, you can only use that feature in order to implement protocols and pass derived objects with those protocol implementations to a function needing it in AppKit or some Apple framework...

I'll look into objrs if that works...