Code works on Intel macs, but not M1s

Hey folks,

I have a cross-platform TTS crate that works mostly fine on Intel macs (it crashes for some users but works for most.) Unfortunately, the AVFoundation backend doesn't work at all on M1s, and I'm wondering if anyone can either offer pointers or PRs? I myself don't have an M1 and am pretty limited in what I can check.

The specific module that's failing is here. Specifically, this function runs, but doesn't speak. Callbacks seem to run, but the cancel callback runs immediately after speech is attempted. Apple's own docs claim this shouldn't run unless speech is stopped, but the only code I have to do this isn't run according to my logging.

I'm not sure if there's a better way to call these APIs other than how I am currently--using the objc crate to send calls to Objective C. I don't suppose Apple has a hidden C-FFI-compatible interface I could call instead? :slight_smile:

So far I've tried commenting out the lines that explicitly set utterance properties for volume/pitch/rate, so all the function does is create the NSString and call the ObjC method after assigning the string to the utterance. No luck.

One thing I've noticed is that ARM cross-compilation seems to bring up casting errors that the compiler usually catches for me. Here, for instance, I had to add an i8 cast to make the code compile on M1s. And this backend works fine on M1s, so clearly I'm doing something right. But given that I have to drop into these unsafe blocks to poke the ObjC APIs, and given the cast errors that the compiler caught for me, I'm wondering if the more complex AVFoundation code is hitting a similar issue on the M1, but since it's in an unsafe block, the compiler isn't able to help. Suggestions welcome--I've been wracking my brain on this one for days and just don't know how to solve it. Sorry I can't distill it to a minimal example. If you have an M1, running:

cargo run --example hello_world

should run the example that fails. And the issue I'm having lies somewhere in the AVFoundation code, which is only a couple hundred lines or so.

Thanks a bunch.

One thing that immediately sticks out to me is that you are ignoring the return value of the method - [AVSpeechUtterance initWithString:]. The expected way to allocated and initialize Objective-C objects is something like the following:

AVSpeechUtterance *utterance =
    [[AVSpeechUtterance alloc]
                        initWithString:@"the big brown fox"];

of which the equivalent Rust code would be:

utterance = msg_send![class!(AVSpeechUtterance), alloc];
utterance = msg_send![utterance, initWithString: str];

The reason for this is a technique known as class clusters that is being used throughout Apple's frameworks. This means that some public classes lie slightly about their type, and the concrete type returned from constructors is not the exact class that you asked for. Instead, it may be a subclass thereof, picked from a set of subclasses optimized for different use cases.

This pattern is implemented by initializers returning an allocated object distinct from the original instance created by +[NSObject alloc]. Essentially, the initializer of a class cluster just throws away the original object and allocates a completely new one, like this:

@implementation MyClassCluster

- (instancetype)initWithString:(NSString *)string {
    [self release]; // throw away original allocation
    ConcreteSublass *obj = [[ConcreteSubclass alloc] init];
    obj.foo = string;
    return obj; // instead of `return self;`
}

@end

Thus, it is possible that on M1 Macs, the AVSpeechUtterance class was changed to a class cluster, and in this case, by ignoring the return value of - initWithString:, you are operating on an empty, non-initialized object, which turns out to be a no-op.


By the way, don't compare BOOLs like value == YES, it's dangerous. Since Objective-C BOOL is always intended to be a C signed char, it can assume values other than YES (1) and NO (0). Therefore, even if it has a C style truthy (non-zero) value, comparison to YES may still yield false. Compare with NO like value != NO instead, that will always yield the correct result.

Thank you. I think I've applied all of your feedback, but my tester still receives the same result.

I assume that once I've allocated/inited the object in question, I can still set variables as I am now? I.e.:

let _: () = msg_send![utterance, setPitchMultiplier: self.pitch];

Or do I need to keep re-assigning to the utterance whenever I change a property?

Any other thoughts? The speakUtterance call returns void--is there any way to get more detailed logging from the runtime? I really don't like those let _: () = calls that the objc crate seems to require.

Yes, setters usually return void so they change the object in-place, you don't need to keep re-assigning after initialization.

There are some suggestions in this thread on Stack Overflow for verbose logging of Objective-C message sends. I'm not sure which of them are still working, though. They are worth a try in any case.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.