MacOS: Can't create a delegate that gets called

Hey folks, this is driving me batty and I'm hoping someone can help.

I have a cross-platform crate for text-to-speech that I'm working on adding MacOS/iOS support to. Was hoping that the Cocoa Rust bindings already included the APIs I need, but they don't appear to. So I'm rolling my own. I have basic TTS working, but now I need a delegate to indicate that speech has finished and I should queue the next utterance. Unfortunately, my current attempt isn't getting called at all and I'm not sure why. Not sure if maybe I'm getting the selector syntax wrong, though I've tried a few combinations and nothing seems to work.

I'm modelling my work off of this pattern. Any pointers on what I'm doing wrong? Here is some ObjC that does what I want.

Thanks for any help.

Your delegate class doesn't conform to the NSSpeechSinthesizerDelegate protocol. Maybe the NSSpeechSynthesizer class checks actual protocol conformance and not only the existence of the delegate callback method? Also, shouldn't the delegate method be called speechSynthesizer:didFinishSpeaking: as per the documentation? It also takes the speech synthesizer object as its first argument, not only a BOOL (that your current implementation assumes).

By the way, there are several other strange things in your code:

– you seem to use +new for allocating and initializing the speech synthesizer object but you make separate +alloc and -init calls for creating the delegate object. This in turn results in uglier code (e.g. unnecessary mutability).
– The let _: () = declaration before the setDelegate: method call is unnecessary.
– The NSSpeechSynthesizerBackend struct contains two pointers to allocated objects yet you never -release them. Shouldn't you be implementing Drop for your struct where you release both objects?

So, a couple questions:

  1. What are you proposing the correct syntax be here? I've tried several different things and none worked. I'm not sure what to name the selector, particularly since any additional : in the name causes the function to need new arguments.

  2. Isn't my current function accepting the sender? See the first argument, which I ignore for now.

        extern "C" fn speech_synthesizer_did_finish_speaking(_: &Object, _: Sel, _: BOOL) {

I'll address these, but note that nice code that doesn't work is worse than ugly code that does. :slight_smile: At least the latter can be fixed up.

I thought so, but I don't fully understand the new/alloc/init distinction. Figured I'd get this working then iterate on that. If it works, then I can clean it up until it doesn't, then roll the breaking change back.

If I do:

        unsafe { msg_send![obj, setDelegate: delegate_obj] };

I get:

error[E0282]: type annotations needed
  --> src/backends/ns_speech_synthesizer.rs:31:18
   |
31 |         unsafe { msg_send![obj, setDelegate: delegate_obj] };
   |                   41 caret characters consider giving `result` a type
   |
   = note: this error originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info)
error: aborting due to previous error

Also, the type annotation is required as per the docs:

// Even void methods must have their return type annotated
let _: () = msg_send![obj, release];

Could be that I'm misunderstanding something.

Yes I should, but code that doesn't work and cleans up after itself isn't really useful to me. Plus, there's going to be much more cleanup later when I'm managing a Vec<NSString> so might as well just do it all then once things work.

I've done a bit more digging, and it seems like the issue may be my lack of an NSRunLoop. Formerly my example did:

    io::stdin().read_line(&mut _input)?;

to block and allow for text to speak. But apparently that doesn't let delegates run. Now I'm doing:

    let run_loop: id = unsafe { NSRunLoop::currentRunLoop() };
    unsafe {
        let _: () = msg_send![run_loop, run];
    }
    // io::stdin().read_line(&mut _input)?;

The example still blocks, and Enter doesn't terminate anymore. So it seems like the NSRunLoop may be working. But the delegate still isn't running.

Thanks for any help. Ugh, this is way more painful than working with Microsoft's preview WinRT bindings in a VM was. :confused:

That's exactly what I'm saying. The selector name should be speechSynthesizer:didFinishSpeaking:, and the function should take one more argument, like this:

extern "C" fn speech_synthesizer_did_finish_speaking(_: &Object, _: Sel, synthesizer: &Object, _: BOOL)

No it's not. The first argument to Objective-C methods is self, the second is the selector, and then any other arguments follow.

Got it, thanks, didn't get that self was the first parameter. Now I have:


extern "C" fn speech_synthesizer_did_finish_speaking(
_: &Object,
_: Sel,
_: &Object,
_: BOOL,
) {
println!("Got it");
}
unsafe {
decl.add_method(
sel!(speechSynthesizer:didFinishSpeaking:),
speech_synthesizer_did_finish_speaking
as extern "C" fn(&Object, Sel, &Object, BOOL) -> (),
)
};

And I get:

error[E0277]: the trait bound `for<'r, 's> extern "C" fn(&'r mut objc::runtime::Object, objc::runtime::Sel, &'s objc::runtime::Object, i8): objc::declare::MethodImplementation` is not satisfied --> src/backends/ns_speech_synthesizer.rs:31:17 | 31 | / speech_synthesizer_did_finish_speaking 32 | | as extern "C" fn(&mut Object, Sel, &Object, BOOL) -> (), | |___________________________________________________________________________^ the trait `objc::declare::MethodImplementation` is not implemented for `for<'r, 's> extern "C" fn(&'r mut objc::runtime::Object, objc::runtime::Sel, &'s ob jc::runtime::Object, i8)` | = help: the following implementations were found: <for<'r> extern "C" fn(&'r T, objc::runtime::Sel, A, B) -> R as objc::declare::MethodImplementation> <for<'r> extern "C" fn(&'r mut T, objc::runtime::Sel, A, B) -> R as objc::declare::MethodImplementation>

Any pointers on how to fix that?

Thanks a bunch for the help!

Um, that's odd. Maybe it's that Objective-C's variadics are poorly handled? In C it's customary to cast method implementations to IMP which is just a typedef for id (*)(id, SEL, ...). Maybe try casting the function to a fn pointer type that does have the trait bound (eg. the one you used previously) – the actual call will be performed using the correct types anyway (since that is baked into whatever calls your delegate).

Yep. For instance, fn(&Whatever), i.e., for<'any> fn(&'any Whatever) won't unify with a fn(T?) generic.

So the following ought to fix the error message,

+ unsafe
  extern "C" fn speech_synthesizer_did_finish_speaking(
  _: &Object,
  _: Sel,
- _: &Object,
+ _: *const Object,
  _: BOOL,
) {
println!("Got it");
}

but I don't know objc very well, so there may be a better way.

Thanks, this plus my NSRunLoop changes got the delegate working! Much
appreciated.

Hope I can toss out one more quick question here, because predictably
I've hit another snag.

Apple's text-to-speech doesn't do any queuing, and any new speech
interrupts speech already in progress. It looks like the only way to
queue utterances is to manage it yourself, dequeuing the next utterance
when the previous stops speaking.

I'm now having issues managing queues. Initially I thought I'd just use
a Vec<NSString>, but the delegate doesn't have access to self, only
the delegate class.

Now I'm thinking of moving utterances to an NSArray stored as an ivar
on the delegate, But I can't set the NSArray directly because it
doesn't seem to implement Encode. The cocoa crate seems to use id
everywhere, though, and indeed I can create an NSArray, call set_ivar
and assign it to an ivar of type id. I can also call get_ivar in my
delegate, and get back what looks like a memory address.

Is there any way to convert an id back to its NSArray in the
delegate? Or am I on the wrong track with this? Casting doesn't seem to
work, so I assume there's some other way .

Thanks.

As a workaround, can you just send the messages to id? I have no idea what specific type safety the Rust Cocoa bindings try to bolt onto Objective-C APIs, but when compiling with an Objective-C compiler, you can send any existing selector to id-typed expressions, the compiler won't complain.

OMG, I can. This code is making my brain bleed. Ah well, at least the
exposed API is clean. Gonna need a stiff drink after this.