-
Notifications
You must be signed in to change notification settings - Fork 112
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
ndk: Add AMidi interface #353
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 byopenMutex
defined in Line 102 ofamidi.cpp
AMidiDevice_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-sideMidiDeviceServer
viatransact()
andonTransact()
:- gFidMidiDeviceServerBinder, which represents
IBinder MidiDevice.mDeviceServerBinder
, is set fromandroid_media_midi.cpp
MidiDevice.mDeviceServerBinder
is 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}Port
all havesynchronized
block onmInputPortOutputPorts
andmOutputPortDispatchers[portNumber]
, which meansMidiDevice
is 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
ndk
doesn'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!