kevinmehall / nusb

A new pure-Rust library for cross-platform low-level access to USB devices.
Apache License 2.0
226 stars 28 forks source link

Thoughts about Android support and the disadvantage of `rusb` #86

Open wuwbobo2021 opened 1 month ago

wuwbobo2021 commented 1 month ago

https://github.com/a1ien/rusb/issues/213

Inspired by https://github.com/kevinmehall/nusb/issues/76, I realized that libusb is a wrapper of OS APIs, and rusb is yet another wrapper around libusb.

If only the authors of rusb and nusb had worked together and made newer versions of rusb on the nusb code base, improvements to the new library would have been much faster, making it much more popular than the current nusb.

Currently, I'm more familiar with rusb instead of asynchronous programming. I'll provide a link to my Android minimal CDC-ACM driver crate (which is unimplemented in Android OS), with the rusb wrapper as its private module. Hopefully someone will do it for nusb, or just guide me to make the port (of course, some parts of the existing linux_usbfs code can be used).

kevinmehall commented 1 month ago

The Device::from_fd method added in https://github.com/kevinmehall/nusb/pull/80 (not yet released) makes it possible to use nusb on Android. You'd need some Java code to get the file descriptor from Android and pass it there, which I haven't tried myself, but should be similar to doing it with libusb.

You don't really need to know async to use nusb, just wrap anything that returns a Future in futures_lite::block_on. I should probably just add blocking APIs for users who don't need async. libusb similarly is mostly async internally with blocking APIs wrapping the async ones.

wuwbobo2021 commented 4 weeks ago

When I talked about "porting", I was thinking about porting nusb to Android.

My program currently enumerates device descriptors and interface descriptors successfully, but fails to wrap the file descriptor obtained from the connection with libusb_wrap_sys_device(), even if I claim the interface in Java before doing it. It's ioctl errno 9 returned by ioctl(fd, IOCTL_USBFS_CONNECTINFO, &ci); in op_wrap_sys_device(), linux_usbfs.c in libusb.

I found some comment on the Web, claiming that Android Java API should be better than wrapping the low level fd in sense of compatability. Maybe I should forget about "better performance", avoid libusb or linux_usbfs and do everything with JNI, like what https://github.com/deviceplug/btleplug does for Android.

I've found that a few people are making efforts for libusb, like https://github.com/libusb/libusb/pull/1164, but I haven't found helpful clues within this fork for solving my current problem.

Code snippet from my program:

    fn open_permitted_usb_device(&self, java_dev: &JObject) -> Result<DeviceHandle, Error> {
        use rusb::UsbContext;
        with_jni_env_activity(|env, _| {
            let conn = env
                .call_method(
                    &self.internal,
                    "openDevice",
                    "(Landroid/hardware/usb/UsbDevice;)Landroid/hardware/usb/UsbDeviceConnection;",
                    &[java_dev.into()],
                )
                .and_then(|o| o.l())
                .map_err(|_| Error::Access)?;

            let mut native_fd =
                env.call_method(conn, "getFileDescriptor", "()I", &[])
                    .and_then(|n| n.i())
                    .map_err(|_| Error::BadDescriptor)? as std::ffi::c_int;

            let mut p_libusb_hdl =
                std::mem::MaybeUninit::<*mut libusb1_sys::libusb_device_handle>::uninit();
            unsafe {
                eprintln!("before check_libusb_error. fd: {:#010x}", native_fd);
                check_libusb_error(libusb1_sys::libusb_wrap_sys_device(
                    self.rusb_context.as_raw(),
                    &mut native_fd,
                    p_libusb_hdl.as_mut_ptr(),
                ))?;
                eprintln!("after check_libusb_error.");
                let libusb_hdl =
                    std::ptr::NonNull::new(p_libusb_hdl.assume_init()).ok_or(Error::NoDevice)?;
                Ok(rusb::DeviceHandle::from_libusb(
                    self.rusb_context.clone(),
                    libusb_hdl,
                ))
            }
        })
    }

ADB logcat:

10-26 02:19:59.026 28522 28535 I RustStdoutStderr: [DeviceInfo { internal: GlobalRef { inner: GlobalRefGuard { obj: JObject { internal: 0x10048a, lifetime: PhantomData<&()> }, vm: JavaVM(0x7f96e0d140) } }, vendor_id: 1027, product_id: 24577, class: 0, subclass: 0, protocol: 0, path_name: "/dev/bus/usb/001/004", manufacturer_string: Some("FTDI"), product_string: Some("FT232R USB UART"), serial_number: Some("A601YRKH"), interfaces: [InterfaceInfo { interface_number: 0, alternate_setting: 0, num_endpoints: 2, class: 255, sub_class: 255, protocol: 255 }] }]
10-26 02:19:59.027 28522 28535 I RustStdoutStderr: before check_libusb_error. fd: 0x0000001a
10-26 02:19:59.027 28522 28535 I RustStdoutStderr: [timestamp] [threadID] facility level [function call] <message>
10-26 02:19:59.027 28522 28535 I RustStdoutStderr: --------------------------------------------------------------------------------
10-26 02:19:59.027 28522 28535 I RustStdoutStderr: [ 0.004364] [00006f78] libusb: debug [libusb_wrap_sys_device] wrap_sys_device 0x7f95768e64
10-26 02:19:59.027 28522 28535 I RustStdoutStderr: [ 0.004410] [00006f78] libusb: debug [linux_get_device_address] getting address for device: (null) detached: 1
10-26 02:19:59.027 28522 28535 I RustStdoutStderr: [ 0.004454] [00006f78] libusb: error [op_wrap_sys_device] connectinfo failed, errno=9
10-26 02:19:59.027 28522 28535 I RustStdoutStderr: [ 0.004481] [00006f78] libusb: debug [libusb_wrap_sys_device] wrap_sys_device 0x7f95768e64 returns -1
10-26 02:19:59.027 28522 28535 I RustStdoutStderr: thread '<unnamed>' panicked at usb_cdc_test.rs:11:100:
10-26 02:19:59.027 28522 28535 I RustStdoutStderr: called `Result::unwrap()` on an `Err` value: Io
10-26 02:19:59.027 28522 28535 I RustStdoutStderr: note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
10-26 02:19:59.027 28522 28535 I RustStdoutStderr: [ 0.004900] [00006f78] libusb: debug [libusb_exit]  
10-26 02:19:59.027 28522 28535 I RustStdoutStderr: [ 0.004940] [00006f78] libusb: debug [libusb_unref_device] destroy device 2.1
10-26 02:19:59.027 28522 28535 I RustStdoutStderr: [ 0.004970] [00006f78] libusb: debug [libusb_unref_device] destroy device 1.4
10-26 02:19:59.027 28522 28535 I RustStdoutStderr: [ 0.004996] [00006f78] libusb: debug [libusb_unref_device] destroy device 1.1
10-26 02:19:59.027 28522 28535 I RustStdoutStderr: [ 0.005023] [00006f78] libusb: debug [usbi_remove_event_source] remove fd 24
10-26 02:19:59.028 28522 28535 I RustStdoutStderr: [ 0.005060] [00006f78] libusb: debug [usbi_remove_event_source] remove fd 23
kevinmehall commented 4 weeks ago

Errno 9 is EBADF - Bad file descriptor, so the file descriptor you're getting is not valid, or is getting closed before you use it. I can't really help with the Android part, or libusb/rusb, but maybe @TroyNeubauer has some hints, since he got nusb working on Android.

If JNI can call into Java from Rust rather than the other way around, yeah, it might be nice to implement list_devices for Android that way. Dealing with UI for permissions seems like it could be hard to fit into the same API, but WebUSB requires similar considerations.

wuwbobo2021 commented 2 weeks ago

I have published the initial version: https://crates.io/crates/android-usbser. Please help me with the optional feature unsafe-rusb which doesn’t work on my device.

kevinmehall commented 2 weeks ago

That's using rusb not nusb so is off topic here, and you'd need to be more specific than "doesn’t work on my device" in any case.

wuwbobo2021 commented 2 weeks ago

I have got nusb working with my crate. I will port my CDC-ACM driver to use nusb, but here are a few clues for porting nusb to Android:

I assume that these functions and structs are required for porting nusb to a new platform.

list_devices()
list_buses()
Device
Interface
DevInst (optional)
DeviceId
SysfsPath (optional)
HotplugWatch

Thoughts for Android:

list_devices(): Needs a unique implementation based on UsbManager getDeviceList().

Device: Use linux_usbfs platform's Device, except platform::Device::from_device_info().

DevInst: it may (or may not) be the JNI global reference of android.hardware.usb.UsbDevice, from which device information can be read.

DeviceId: it can be an integer got from UsbDevice.getDeviceId() (not persistent across USB disconnects), or a string got from UsbDevice.getDeviceName(). UsbDevice documentation: In the standard implementation, this is the path of the device file for the device in the usbfs file system. The hashmap returned by UsbManager.getDeviceList() uses it as the key.

SysfsPath: it may be parsed from UsbDevice.getDeviceName().

HotplugWatch: Needs a unique implementation, receiving Android activity intent ACTION_USB_DEVICE_ATTACHED and ACTION_USB_DEVICE_DETACHED.

check_attached_intent() (for possible application startup intent), has_permission() and request_permission() should be public functions available for Android, which may be used by nusb::DeviceInfo for this target_os.

Please describe here if anything listed above is wrong.

Here is the main problem of the possible porting for Android:

Function request_permission() must poll for permission request results differently (provided that it should be synchronous), depending on whether the current thread is the main thread. This may be checked via android.os.Looper, getMainLooper() and isCurrentThread().

In the main thread, thread::sleep() while requesting for permission is not possible, it involves polling for application events. Check the function from crate android-activity, which requires the handle AndroidApp. This can be problematic for intergrating all prodecures into this function.

In a background thread, it must not poll for events directly, and sleeping is possible while it waits for the request result.

In the first case (main thread), the main Resume event may be polled, because the request_permission() shows a request dialog and pauses the main activity. But with my current crate, I found that requestPermission() may be called for double times (2 Resume events) before has_permission() turns to true. So, just consider the same way used for the second case:

In the second case (background thread), a Java helper handling a custom implementation of android.content.BroadcastReceiver may be used to receive the denied result; in this case, has_permission() stays false, but it should stop waiting. In order to embed a bit of Java code into the Rust library without using Gradle, this might help (it is merely a reference).

These complicated issues might be avoided by giving up the idea of putting all required prodecures into request_permission(). However HotplugWatch implementation may not avoid these things. (Maybe it just needs a correctly configured async runtime.)

This Flutter library can be checked: https://pub.dev/packages/libusb_android_helper.

kevinmehall commented 2 weeks ago

You can get started without any changes to nusb. list_devices and watch_devices call the Linux implementation, which requires sysfs and udev, not available on Android. But you don't have to use these. Instead, get the file descriptor using Java/JNI, and pass it to Device::from_fd to get a Device that should already be fully functional.

Yes, it could be good to have an Android implementation of list_devices and watch_devices that calls into Android APIs, but I'm not sure it can be done through the same nusb APIs because of the need to request permissions. WebUSB is similar and maybe could have a common abstraction in nusb. But to start, you can prototype that externally to see what's needed.