-
Notifications
You must be signed in to change notification settings - Fork 119
ndk: Add AMidi interface #353
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
base: master
Are you sure you want to change the base?
Conversation
Added Cargo feature `midi` and optional linkage to `libamidi.so`
Added Cargo feature `midi`, which enables `ffi/midi`, `api-level-29`, and NdkMediaError in `media`
Fixed typos in comments about thread-safety in midi.rs Co-authored-by: Marijn Suijten <[email protected]>
ndk/src/midi.rs
Outdated
| // There is no mention about thread-safety in the NDK reference, but the official Android C++ MIDI | ||
| // sample stores `AMidiDevice *` and `AMidi{Input,Output}Port *` in global variables and accesses the | ||
| // ports from separate threads. | ||
| // See https://github.com/android/ndk-samples/blob/7f6936ea044ee29c36b5c3ebd62bb3a64e1e6014/native-midi/app/src/main/cpp/AppMidiManager.cpp | ||
| unsafe impl Send for MidiDevice {} | ||
| unsafe impl<'a> Send for MidiInputPort<'a> {} | ||
| unsafe impl<'a> Send for MidiOutputPort<'a> {} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can they be accessed concurrently (i.e., Sync), or are these only safe to be used on different threads as long as there aren't two or more threads poking it at the same time (Send)?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
MidiIntputPort is definitely not Sync: it puts write in the loop, so there's a possibility of interleaved writing.

MidiOutputPort is definitely Sync:MidiReceiver, which is called by AMidiOutputPort_receive implements synchronization logic. MidiOutputPort::receive will immediately return an Err if a simultaneous read call was invoked by another thread.

MidiDevice seems to be Sync: as MidiDeviceServer, which is called by MidiDevice, uses synchronized in every method except for getDeviceInfo() and setDeviceInfo(), but I'm not sure.

There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's an interesting design choice and discrepancy between input and output ports. We can also enforce the write with a &mut self borrow but I think keeping them as Send without Clone/Copy is also fine.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I re-read the source code of MidiDevice and MidiDeviceServer - I think we can assume that MidiDevice is Sync as well.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
AMidiDevice_fromJava,AMidiDevice_release:
Synchronized byopenMutexdefined in Line 102 ofamidi.cppAMidiDevice_get*:
Retrieves values fromAMidiDevice::deviceInfo, which is not changed after instantiation.AMidi{Input, Output}Port_open->AMIDI_openPort->BpMidiDeviceServer::open{Input, Output}Port:
Calls Java-sideMidiDeviceServerviatransact()andonTransact():- gFidMidiDeviceServerBinder, which represents
IBinder MidiDevice.mDeviceServerBinder, is set fromandroid_media_midi.cpp MidiDevice.mDeviceServerBinderis set in the constructor ofMidiDevice:
/* package */ MidiDevice(MidiDeviceInfo deviceInfo, IMidiDeviceServer server, IMidiManager midiManager, IBinder clientToken, IBinder deviceToken) { mDeviceInfo = deviceInfo; mDeviceServer = server; mDeviceServerBinder = mDeviceServer.asBinder(); ...
- ...and the Java-side
MidiDeviceServer.open{Input, Output}Portall havesynchronizedblock onmInputPortOutputPortsandmOutputPortDispatchers[portNumber], which meansMidiDeviceis thread safe.
- gFidMidiDeviceServerBinder, which represents
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's not preemptively add Send/Sync in that case. If it's not documented there's nothing holding the NDK folks from changing the underling implementation and comments.
We recently requested clarification on other APIs as well, perhaps you can do the same here?
TLDR: Let's keep it Send for now until we have official confirmation for Sync.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I found that AMidiDevice must be released in the thread where the JNI is attached, i.e., the app crashes when AMidiDevice_release is called before AttachCurrentThread[AsDaemon] is called from that thread, which is not a documented behavior. I'll remove Send from MidiDevice.
let midi_device = MidiDevice::from_java(...);
thread::spawn(move || {
midi_device. ...
// JNIEnv access error on AMidiDevice_release
});let midi_device = MidiDevice::from_java(...);
thread::spawn(move || {
AttachCurrentThread(vm, &env, ...);
midi_device. ...
// No error
});+++
We could add SafeMidiDevice type like the following, but I think this must be implemented by the user, not provided from ndk::midi.
struct SafeMidiDevice {
device: MidiDevice,
vm: NonNull<JavaVM>,
}
impl SafeMidiDevice {
pub fn new(vm: NonNull<JavaVM>) -> Self { ... }
}
impl Into<MidiDevice> for SafeMidiDevice {
fn into(self) -> MidiDevice {
AttachCurrentThread(self.vm, ...);
self.device
}
}
unsafe impl Send for SafeMidiDevice {}There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It seems that calling AMidiInputPort_close or AMidiOutputPort_close in non-Java thread makes no issue now, but it's not precisely documented as well, and they even have not appeared in the official example. (The official example only calls AMidiDevice_release.) Maybe we should remove impl Send from MidiInputPort and MidiOutputPort as well?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe we should remove it indeed, though having a real-world user/consumer of these API bindings would really help us test and flesh out these things besides just guessing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like you resolved this. No clue about removing impl Send from the ports, the NDK makes no guarantees but it seems logical that you might want to process such data on another thread even if the JNI device can only be accessed on a VM-attached thread?
|
Sorry for the late reply; I'm working on another project. I'll try to make the work done by this weekend. Thanks for your patience! |
|
@paxbun all good, though you might miss out on the next breaking release window. That doesn't hurt as we can always do a non-breaking patch release for these purely-additive APIs, and spend that tiny bit more time fleshing out the functions so that we don't have to make breaking changes in the near future. |
|
@MarijnS95 I think keeping Moreover, I think making Making In addition, since If you think it's okay to keep |
|
@paxbun sounds like you've done enough research to confirm this. But is it usable that way (e.g. sending ports to threads) when they have a lifetime dependency on the |
…f doc-comments of MidiDevice
|
@MarijnS95 You're right, that makes the lifetime parameter meaningless, I overlooked it. |
I had concerns about the lifetime still making it hard to work around this unless the Not sure if we really need a "safe" counterpart or can just make this the default implementation? Anyways, I'll involve @rib since he did a lot on the JNI crate and is probably better at evaluating whether this is the right - and safe - way to go. |
…nt of ndk::midi::safe
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some more () nits, though not sure if they're relevant at this stage.


Just noticed that
ndkdoesn't have an interface for AMidi, so I made one. Please let me know if there're any tests or fixes I have to do before merging!