diff --git a/.github/workflows/continuous-integration.yaml b/.github/workflows/continuous-integration.yaml
new file mode 100644
index 00000000..50c693af
--- /dev/null
+++ b/.github/workflows/continuous-integration.yaml
@@ -0,0 +1,70 @@
+name: Continuous Integration
+
+on:
+ push:
+ paths-ignore:
+ - "test-devices/**"
+ - "reference/**"
+ pull_request:
+ paths-ignore:
+ - "test-devices/**"
+ - "reference/**"
+
+
+env:
+ MVN_DEFAULT_ARGS: -B -V -ntp -e -Djansi.passthrough=true -Dstyle.color=always
+
+jobs:
+
+ test_os:
+ name: OS ${{ matrix.os }}
+ strategy:
+ fail-fast: false
+ matrix:
+ os: [ubuntu-latest, macOS-latest, windows-latest]
+ runs-on: ${{ matrix.os }}
+ defaults:
+ run:
+ shell: bash
+ steps:
+ - uses: actions/checkout@v7
+ - name: Setup Java
+ uses: actions/setup-java@v5
+ with:
+ distribution: 'zulu'
+ java-version: '25'
+ - name: Configure unit test GPG key
+ run: |
+ echo -n "$UNIT_TEST_SIGNING_KEY" | base64 --decode | gpg --import
+ env:
+ UNIT_TEST_SIGNING_KEY: ${{ vars.UNIT_TEST_SIGNING_KEY }}
+ - name: Build java-does-usb
+ run: ./mvnw $MVN_DEFAULT_ARGS -DskipTests clean install javadoc:javadoc
+ working-directory: ./java-does-usb
+ - name: Determine java-does-usb version
+ id: libver
+ run: echo "version=$(./mvnw -q -ntp help:evaluate -Dexpression=project.version -DforceStdout)" >> "$GITHUB_OUTPUT"
+ working-directory: ./java-does-usb
+ # Examples pin the latest released version so they double as user-facing documentation;
+ # override that pin here to compile them against the HEAD version just installed above.
+ - name: Example "bulk_transfer"
+ run: ./mvnw $MVN_DEFAULT_ARGS -Djava-does-usb.version=${{ steps.libver.outputs.version }} clean compile
+ working-directory: ./examples/bulk_transfer
+ - name: Example "enumerate"
+ run: ./mvnw $MVN_DEFAULT_ARGS -Djava-does-usb.version=${{ steps.libver.outputs.version }} clean compile
+ working-directory: ./examples/enumerate
+ - name: Example "enumerate" (Kotlin)
+ run: ./gradlew -PjavaDoesUsbVersion=${{ steps.libver.outputs.version }} clean build
+ working-directory: ./examples/enumerate_kotlin
+ - name: Example "monitor"
+ run: ./mvnw $MVN_DEFAULT_ARGS -Djava-does-usb.version=${{ steps.libver.outputs.version }} clean compile
+ working-directory: ./examples/monitor
+ - name: Example "monitor" (Kotlin)
+ run: ./mvnw $MVN_DEFAULT_ARGS -Djava-does-usb.version=${{ steps.libver.outputs.version }} clean package
+ working-directory: ./examples/monitor_kotlin
+ - name: Example "stm_dfu"
+ run: ./mvnw $MVN_DEFAULT_ARGS -Djava-does-usb.version=${{ steps.libver.outputs.version }} clean compile
+ working-directory: ./examples/stm_dfu
+ - name: Example "epaper_display"
+ run: ./mvnw $MVN_DEFAULT_ARGS -Djava-does-usb.version=${{ steps.libver.outputs.version }} clean compile
+ working-directory: ./examples/epaper_display
diff --git a/.github/workflows/test-devices.yaml b/.github/workflows/test-devices.yaml
new file mode 100644
index 00000000..0ad9cc6e
--- /dev/null
+++ b/.github/workflows/test-devices.yaml
@@ -0,0 +1,37 @@
+name: Test Devices CI
+
+on:
+ push:
+ paths:
+ - "test-devices/**"
+ - ".github/**"
+ pull_request:
+ paths:
+ - "test-devices/**"
+ - ".github/**"
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v7
+ - uses: actions/cache@v5
+ with:
+ path: |
+ ~/.cache/pip
+ ~/.platformio/.cache
+ key: ${{ runner.os }}-pio
+ - uses: actions/setup-python@v6
+ with:
+ python-version: '3.9'
+ - name: Install PlatformIO Core
+ run: pip install --upgrade platformio
+
+ - name: Build loopback firmware
+ run: pio run
+ working-directory: ./test-devices/loopback-stm32
+
+ - name: Build composite firmware
+ run: pio run
+ working-directory: ./test-devices/composite-stm32
diff --git a/.gitignore b/.gitignore
index 27dc8f0a..b29b5c47 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,5 @@
+java-does-usb/sample.bin
+
# Maven
target/
*.ser
diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 100644
index 00000000..9c988cc8
--- /dev/null
+++ b/CLAUDE.md
@@ -0,0 +1,77 @@
+# CLAUDE.md
+
+This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
+
+## What this is
+
+*Java Does USB* is a pure-Java USB library for communicating with USB devices using custom/vendor-specific protocols (not standard device classes like mass storage or HID). It accesses the OS's native USB APIs through the **Foreign Function and Memory API** — no JNI, no native third-party libraries. Requires **JDK 25** (the `pom.xml` targets release 25; older JDKs need older library versions, see README).
+
+The publishable library lives in `java-does-usb/`. The repo root also holds `examples/`, `test-devices/` (microcontroller firmware for the hardware test rig), `test-graalvm/` (GraalVM native-image compatibility check), and `reference/`.
+
+## Commands
+
+All library commands run from the `java-does-usb/` directory using the Maven wrapper (`./mvnw`).
+
+```bash
+cd java-does-usb
+
+# Build & install to local Maven repo, skipping hardware tests
+./mvnw clean install -DskipTests
+
+# Run all tests (REQUIRES a physical test device connected — see below)
+./mvnw clean test
+
+# Run a single test class
+./mvnw test -Dtest=BulkTransferTest
+
+# Run a single test method
+./mvnw test -Dtest=BulkTransferTest#transferData
+
+# Build javadoc
+./mvnw javadoc:javadoc
+```
+
+Examples are independent projects, each with its own `mvnw`/`gradlew`. Build one with `cd examples/ && ./mvnw clean compile` (or `./gradlew build` for Kotlin examples).
+
+### Native access flag
+
+Any code calling into the library needs native access enabled. Maven surefire passes `--enable-native-access=ALL-UNNAMED` automatically. When running from an IDE or standalone, add the VM option:
+`--enable-native-access=net.codecrete.usb` (or `ALL-UNNAMED` if modules are ignored).
+
+## Testing requires hardware
+
+The unit tests are integration tests against a real USB device — they will fail with "No test device connected" without one. The device is built from an inexpensive STM32 board flashed with firmware from `test-devices/`:
+
+- **loopback-stm32** (VID `0xcafe`, PID `0xceaf`): supports all tests.
+- **composite-stm32** (VID `0xcafe`, PID `0xcea0`): exercises composite-device handling; some tests are skipped.
+
+`TestDeviceConfig.java` hard-codes the VID/PID and endpoint numbers for both variants; `TestDeviceBase` auto-detects which one is connected. On Linux, a udev rule is needed for device access (see README "Linux" section). On Windows the test device auto-installs the WinUSB driver via WCID descriptors.
+
+The `continuous-integration.yaml` workflow only compiles the library and examples on all three OSes — it does **not** run the hardware tests.
+
+## Architecture
+
+### Cross-platform abstraction
+
+The public API is in package `net.codecrete.usb` (`Usb`, `UsbDevice`, `UsbInterface`, `UsbEndpoint`, exceptions, enums). It is the only exported package (`module-info.java`).
+
+`Usb.java` is the entry point. `Usb.instance()` lazily picks a platform implementation of `UsbDeviceRegistry` based on `os.name`/`os.arch` (Macos/Windows/Linux). Everything else flows through platform-specific subclasses of the abstractions in `net.codecrete.usb.common`:
+
+- `UsbDeviceRegistry` (common) — singleton that runs a background daemon thread enumerating devices and emitting connect/disconnect events. Each OS subclass implements `monitorDevices()` and calls `setInitialDeviceList()` when the first enumeration completes.
+- `UsbDeviceImpl`, `UsbInterfaceImpl`, `UsbEndpointImpl`, `UsbAlternateInterfaceImpl` (common) — shared state and logic; each OS provides a `*UsbDevice` subclass that implements the actual native transfers.
+- `EndpointInputStream`/`EndpointOutputStream` + platform `*EndpointInputStream`/`*EndpointOutputStream` — the high-throughput streaming layer.
+- `Transfer`/`TransferCompletion` + platform `*Transfer` and `*AsyncTask` — asynchronous transfer plumbing (I/O completion ports on Windows, epoll on Linux, run loop on macOS).
+- `ConfigurationParser` (common) — parses raw USB configuration descriptors (portable, byte-level; not OS-specific).
+
+When adding a feature, expect to touch the common abstraction plus **all three** platform implementations (`linux/`, `macos/`, `windows/`).
+
+### Native bindings (`gen` packages)
+
+Native API bindings live under `*/gen/` subpackages and are **generated code**, one package per shared library / macOS framework:
+
+- **Linux & macOS**: generated with [jextract](https://jdk.java.net/jextract/) via scripts in `java-does-usb/jextract/{linux,macos}/`. The generated code **is committed** to the repo (must be regenerated per-OS; it is portable across x64/ARM64 of the same OS).
+- **Windows**: generated at build time by the `windowsapi-maven-plugin` (Windows API Generator) — the function/struct/constant list is configured in `pom.xml`, and the output is **not committed**.
+
+Some bindings are hand-written rather than generated, because jextract cannot capture thread-local error state (`errno` on Linux, `GetLastError()` on Windows) — those need an extra call-state parameter. See `java-does-usb/jextract/README.md` for the full generation process and jextract's quirks before regenerating.
+
+Standard USB descriptor structs (device/config/interface/endpoint/string descriptors, setup packet) are modeled in `net.codecrete.usb.usbstandard` as `MemorySegment` views — these are portable and shared across platforms.
diff --git a/README.md b/README.md
index e6a1f4f9..6ee637da 100644
--- a/README.md
+++ b/README.md
@@ -1,138 +1,172 @@
-# Java Does USB: USB library for Java
+# Java Does USB: USB Library for Java
[](https://javadoc.io/doc/net.codecrete.usb/java-does-usb)
-*Java Does USB* is a library for working with USB devices from Java. It allows to query information about all conntected USB devices and to communicate with USB devices using custom / vendor specific protocols. (It is not intended for communication with standard types of USB devices such as mass storage devices, keyboards etc.)
+*Java Does USB* is a Java library for working with USB devices. It allows you to query the connected USB devices and to communicate with them using custom / vendor-specific protocols. It is not intended for communication with standard types of USB devices such as mass storage devices, keyboards etc.
-The library uses the [Foreign Function & Memory API](https://github.com/openjdk/panama-foreign) to access native APIs of the underlying operating system. It only uses Java code and does not need JNI or any native third-party library.
+The library uses the [Foreign Function and Memory API](https://docs.oracle.com/en/java/javase/22/core/foreign-function-and-memory-api.html) to access native APIs of the underlying operating system. It is written entirely in Java and does not use JNI or any native third-party library. The *Foreign Function and Memory API* has been introduced with Java 22.
-The Foreign Function & Memory API (aka as project Panama) is in preview and will be introduced in a future Java version. Currently, it can be tested with Java 19 Early Access (with preview features enabled).
-## Prerequisite
+## Features
-- Java 19 Early Access, preview features enabled (available at https://www.azul.com/downloads/?package=jdk)
-- Windows (x86 64-bit), macOS (x86 64-bit, ARM 64-bit) or Linux 64 bit (x86 64-bit, ARM 64-bit)
+- Single API for all operating systems (similar to WebUSB API)
+- Enumeration of USB devices
+- Control, bulk and interrupt transfers (optionally with timeout)
+- Notifications about connected/disconnected devices
+- Descriptive information about interfaces, settings and endpoints
+- High-throughput input/output streams
+- Support for alternate interface settings, composite devices and interface association
+- Published on Maven Central and licensed under the permissive MIT license
-It has been tested with Azul Zulu 19.0.77 EA 34.
+## Getting Started
-## Features
+The library is available at Maven Central. To use it, just add it to your Maven or Gradle project.
-### Implemented
+If you are using Maven, add the below dependency to your pom.xml:
-- Single API for all operating systems (similar to WebUSB API)
-- Enumeration of USB devices
-- Control, bulk and interrupt transfer
-- Notifications about connected/disconnected devices
-- Descriptive information about interfaces, settings and endpoints
-- Support fo composite devices
-- Published on Maven Central
+```xml
+
+ net.codecrete.usb
+ java-does-usb
+ 1.3.0
+
+```
-### To do
+If you are using Gradle, add the below dependency to your build.gradle file:
-- Transfers with time-out
-- Device and USB protocol revision
-- Alternate interface settings
-- Support for associated interfaces
-- Isochronous transfer
+```groovy
+compile group: 'net.codecrete.usb', name: 'java-does-usb', version: '1.3.0'
+```
-### Not planned
+```java
+package net.codecrete.usb.sample;
-- Changing configuration: The library selects the first configuration. Changing configurations is rarely used and not supported on Windows.
-- USB 3.0 streams: Not supported on Windows.
-- Providing information about USB buses, controllers and hubs
+import net.codecrete.usb.Usb;
+public class EnumerateDevices {
-## Platform-specific considerations
+ public static void main(String[] args) {
+ for (var device : Usb.getDevices()) {
+ System.out.println(device);
+ }
+ }
+}
+```
-### macOS
-No special considerations apply. Using this library, a Java application can connect to any USB device and claim any interfaces that aren't claimed by an operating system driver or another application.
+## Documentation
+- [Code Examples](https://github.com/manuelbl/JavaDoesUSB/wiki/Java-Does-USB-By-Examples)
+- [Javadoc](https://javadoc.io/doc/net.codecrete.usb/java-does-usb)
-### Linux
-*libudev* is used to discover and monitor USB devices. It is closely tied to *systemd*. So the library only runs on Linux distributions with *systemd* and the related libraries. The majority of Linux distributions suitable for desktop computing (as opposed to distributions optimized for containers) fulfill this requirement.
-Similar to macOS, a Java application can connect to any USB device and claim any interfaces that aren't claimed by an operating system driver or another application.
+## Examples
-Most Linux distributions by default set up user accounts without the permission to access USB devices directly. The *udev* system daemon is responsible for assigning permissions to USB devices. It can be configured to assign specific permissions or ownership.
+- [Bulk Transfer](examples/bulk_transfer/) demonstrates how to find a USB device, open it and communicate using bulk transfer.
+- Enumeration ([Java](examples/enumerate/) / [Kotlin](examples/enumerate_kotlin/)) lists all connected USB devices and displays information about interfaces and endpoints.
+- Monitor ([Java](examples/monitor/) / [Kotlin](examples/monitor_kotlin/)) lists the connected USB devices and then monitors for devices being connected and disconnected.
+- [Device Firmware Upload (DFU) for STM32](examples/stm_dfu) uploads firmware to STM32 microcontrollers supporting the built-in DFU mode.
+- [ePaper Display](examples/epaper_display) communicates with an IT8951 controller for e-Paper displays and shows an image on the display.
+- [Enumerate Native](examples/enumerate_native/) and [Monitor Native](examples/monitor_native/) demostrate how to build a native image with GraalVM.
-Create a file called `/etc/udev/rules.d/80-javadoesusb-udev.rules` with the below content:
-```text
-SUBSYSTEM=="usb", ATTRS{idVendor}=="cafe", MODE="0666"
-```
-This adds the rule to assign permission mode 0666 to all USB devices with vendor ID `0xCAFE`. This unregistered vendor ID is used by test device.
+## Prerequisite
+
+- Java 25 or higher
+- Windows (x86 64-bit, ARM 64-bit), macOS (x86 64-bit, ARM 64-bit) or Linux 64 bit (x86 64-bit, ARM 64-bit)
-### Windows
-The Windows driver model is more rigid than the ones of macOS or Linux. It's not possible to open any USB device by default. Instead, only devices using the *WinUSB* driver can be opened. This even applies to devices with no installed driver.
+## Platform-specific Considerations
-USB devices can implement certain control requests to instruct Windows to automatically install the WinUSB driver (search for WCID or Microsoft OS Compatibility Descriptors). The driver can also be manually installed or replaced using a software called [Zadig](https://zadig.akeo.ie/).
-The test devices implement the required control requests. So the driver is installed automatically.
+### macOS
-This library does not yet run reliably on Windows as the Java VM sometimes overwrites the last error code, which is needed for proper function, not just in error cases. It works incorrectly when run in the debugger and sometimes even without the debugger. A future version of the Foreign Function & Memory API will hopefully provide a way to save the last error code. The developers are aware of the issue.
+No special considerations apply. Using this library, a Java application can connect to any USB device and claim any interface that isn't claimed by an operating system driver or another application. Standard operating system drivers can be unloaded if the application is run with *root* privileges. It runs both on Macs with Apple Silicon and Intel processors.
-The library has not been tested on Windows for ARM64. It might or might not work.
+### Linux
-### 32-bit versions
+*libudev* is used to discover and monitor USB devices. It is closely tied to *systemd*. So the library runs on Linux distributions with *systemd* and the related libraries. The majority of Linux distributions suitable for desktop computing (as opposed to distributions optimized for containers) fulfill this requirement. It runs on both Intel/AMD and ARM processors.
-The Foreign Function & Memory API has not been implemented for 32-bit operating systems / JDKs. So it does not support them (and likely never will).
+Similar to macOS, a Java application can connect to any USB device and claim any interface that isn't claimed by an operating system driver or another application. Standard operating system drivers can be unloaded (without the need for root privileges).
+Most Linux distributions set up user accounts without permissions to access USB devices. The *udev* system daemon is responsible for assigning permissions to USB devices. It can be configured to assign specific permissions or ownership:
+Create a file called `/etc/udev/rules.d/80-javadoesusb-udev.rules` with the below content:
-## Documentation
+```text
+SUBSYSTEM=="usb", ATTRS{idVendor}=="cafe", MODE="0666"
+```
-- [Javadoc](https://javadoc.io/doc/net.codecrete.usb/java-does-usb)
+This adds the rule to assign permission mode 0666 to all USB devices with vendor ID `0xCAFE`. This unregistered vendor ID is used by the test devices.
+Without the *udev* rule, it is still possible to enumerate and query all USB devices.
-## Examples
+### Windows
-- [Bulk Transfer](examples/bulk_transfer/) demonstrates how to find a USB device, open it and communicate using bulk transfer.
-- [Enumeration](examples/enumerate/) lists all connected USB devices and displays information about interfaces and endpoints.
-- [Monitor](examples/monitor/) lists the connected USB devices and then monitors for USB devices being connected and disconnnected.
+The Windows driver model is rather rigid. It's not possible to open a USB device unless it uses the *WinUSB* driver. This even applies to devices with no installed driver. Enumerating and querying USB devices is possible independent of the driver.
+USB devices can implement special control requests to instruct Windows to automatically install the WinUSB driver (search for *WCID* or *Microsoft OS Compatibility Descriptors*). The WinUSB driver can also be manually installed or replaced using a tool called [Zadig](https://zadig.akeo.ie/).
+The test devices implement the required control requests. So the driver is installed automatically.
+
+The implementation runs on both Windows for Intel/AMD and ARM processors.
-## Code generation
-Many bindings for the native APIs have been generated with *jextract*. See the [jextract](java-does-usb/jextract) subdirectory for more information.
+
+## Building from source
+
+To build from source, run the following command:
+
+```
+cd java-does-usb
+mvn clean install -DskipTests
+```
+
+The tests are skipped as they require that a special test device is connected to the computer. See the next section for more information.
## Testing
-In order to run the unit tests, a special test device must be connected to the computer. See the [loopback-stm32](test-devices/loopback-stm32) directory.
+In order to run the unit tests, a special test device must be connected to the computer, which can be easily created from very inexpensive microcontroller boards. Two variants exist:
+
+- [loopback-stm32](test-devices/loopback-stm32)
+- [composite-stm32](test-devices/composite-stm32)
+
+The test device with the *loopback-stm32* code supports all tests. If the test device with the *composite-stm32* code is connected, some tests are skipped, but the correct handling of composite devices is verified instead.
Tests can be run from the command line:
```
+cd java-does-usb
mvn clean test
```
-If they are run from an IDE (such as IntelliJ IDEA), you must likely configure VM options to enable preview features and allow native access:
+If they are run from an IDE (such as IntelliJ IDEA), you will likely need to configure VM options to allow native access:
```
---enable-preview --enable-native-access=net.codecrete.usb
+--enable-native-access=net.codecrete.usb
```
Or (if modules are ignored):
```
---enable-preview --enable-native-access=ALL-UNNAMED
+--enable-native-access=ALL-UNNAMED
```
-If you don't have the test device, you can get a glimpse at the library running the below command. It enumerates all connected USB devices.
-```
-MAVEN_OPTS="--enable-preview --enable-native-access=ALL-UNNAMED" mvn install exec:java -Dexec.classpathScope="test" -DskipTests -Dexec.mainClass="net.codecrete.usb.sample.EnumerateDevices"
-```
+
+## Code generation
+
+Many bindings for the native APIs have been generated with *jextract*. See the [jextract](java-does-usb/jextract) subdirectory for more information. For functions that need to retain the error state (`errno` on Linux, `GetLastError()` on Windows), the bindings have been manually written as *jextract* does not support it.
+
+Since the code can only be generated for the current operating system, it must be generated on separate computers for Linux, Windows and macOS. Thus, the generated code is included in the repository. The generated code is compilable on all operating systems.
diff --git a/RELEASING.md b/RELEASING.md
new file mode 100644
index 00000000..3e3511a0
--- /dev/null
+++ b/RELEASING.md
@@ -0,0 +1,100 @@
+# Releasing Java Does USB
+
+This document describes how a new version of the library is released to Maven Central, and how
+version numbers flow through the repository afterwards.
+
+## Versioning scheme
+
+- The library follows [Semantic Versioning](https://semver.org/) (`MAJOR.MINOR.PATCH`).
+- On the `main` branch, `java-does-usb/pom.xml` always carries the *next*, unreleased version
+ with a `-SNAPSHOT` suffix (e.g. `1.2.2-SNAPSHOT`). This makes it obvious that a build from `main`
+ is a development build, not something published to Central.
+- `README.md` and the example projects under `examples/` intentionally do **not** track this
+ SNAPSHOT. They pin the latest *released* version, because they double as user-facing
+ documentation: a user reading the README or copying an example's `pom.xml`/`build.gradle.kts`
+ should see the version they can actually `mvn install` from Central today.
+- Continuous integration overrides the pinned example version at build time so it still compiles
+ the examples against `main`'s HEAD (see "How CI stays honest" below). No file needs to be edited
+ for this to work.
+
+## Release checklist
+
+1. **Drop the `-SNAPSHOT` suffix.**
+ In `java-does-usb/pom.xml`, set `` to the release version, e.g.:
+ ```bash
+ cd java-does-usb
+ ./mvnw versions:set -DnewVersion=1.3.1 -DgenerateBackupPoms=false
+ ```
+
+2. **Update the version everywhere it's pinned for users.** This means:
+ - `README.md` — the Maven (`...`) and Gradle (`version: '...'`) snippets
+ under "Getting Started", and the version history table if you're adding an entry.
+ - Every example's dependency on `java-does-usb`, pinned via a single property so it's one edit
+ per file (`java-does-usb.version` in the Maven examples, `javaDoesUsbVersion` in the Gradle
+ one):
+ - `examples/enumerate/pom.xml`
+ - `examples/bulk_transfer/pom.xml`
+ - `examples/monitor/pom.xml`
+ - `examples/monitor_kotlin/pom.xml`
+ - `examples/stm_dfu/pom.xml`
+ - `examples/epaper_display/pom.xml`
+ - `examples/enumerate_native/pom.xml`
+ - `examples/monitor_native/pom.xml`
+ - `examples/enumerate_kotlin/build.gradle.kts`
+ - The example's *own* project version (its `` / `version = "..."`, separate from the
+ `java-does-usb.version`/`javaDoesUsbVersion` property), which by convention tracks the library
+ release it was last verified against, for `enumerate`, `bulk_transfer`, `monitor`,
+ `monitor_kotlin`, `stm_dfu`, `epaper_display`, and `enumerate_kotlin`. (`enumerate_native` and
+ `monitor_native` version themselves independently as `1.0-SNAPSHOT` and don't need this.)
+ - The sample console output hardcoded in each example's own `README.md` (e.g. build log lines
+ like `[INFO] Building enumerate 1.2.1` or jar filenames like `stm_dfu-1.2.1.jar`), which
+ embeds the example's own project version from the point above.
+
+ A simple search for the previous version number across `README.md` and `examples/` will locate
+ every occurrence listed above.
+
+3. **Commit, tag, and publish.**
+ ```bash
+ git add -A
+ git commit -m "Release 1.2.2"
+ git tag v1.2.2
+ git push origin main v1.2.2
+ cd java-does-usb
+ ./mvnw clean install # GPG passphrase from password store
+ ./mvnw clean deploy
+ ```
+
+4. **Prepare `main` for the next development iteration.**
+ ```bash
+ cd java-does-usb
+ ./mvnw versions:set -DnewVersion=1.2.3-SNAPSHOT -DgenerateBackupPoms=false
+ git add pom.xml
+ git commit -m "Prepare for next development iteration"
+ git push origin main
+ ```
+ Deliberately do **not** touch `README.md` or the examples in this step — they should keep
+ pointing at the release just made (1.2.2) until the *next* release checklist run.
+
+## How CI stays honest
+
+Because the examples pin the released version, a naive CI setup would silently compile them
+against whatever is available on Maven Central instead of the code actually being tested — i.e.
+after step 4 above, `main` might contain a breaking change that no CI run would ever catch until
+the next release.
+
+To prevent that, `.github/workflows/continuous-integration.yaml`:
+
+1. Builds and `install`s the library from the checked-out `pom.xml` (whatever version — released
+ or `-SNAPSHOT` — is on HEAD) into the local Maven repository.
+2. Reads that exact version back out with `mvn help:evaluate -Dexpression=project.version`.
+3. Passes it to each example build as an override:
+ - Maven examples: `-Djava-does-usb.version=`
+ - The Gradle example (`enumerate_kotlin`): `-PjavaDoesUsbVersion=`
+
+This means CI always compiles every example against the exact commit under test, while the
+committed example files keep showing users the last real release.
+
+If you add a new example, give its `java-does-usb` dependency the same treatment: introduce a
+`java-does-usb.version` property (Maven) or an overridable `javaDoesUsbVersion` val (Gradle)
+defaulting to the current released version, add a CI step following the existing pattern, and add
+it to the list in step 2 of the release checklist above.
diff --git a/examples/bulk_transfer/.mvn/wrapper/maven-wrapper.jar b/examples/bulk_transfer/.mvn/wrapper/maven-wrapper.jar
new file mode 100644
index 00000000..cb28b0e3
Binary files /dev/null and b/examples/bulk_transfer/.mvn/wrapper/maven-wrapper.jar differ
diff --git a/examples/bulk_transfer/.mvn/wrapper/maven-wrapper.properties b/examples/bulk_transfer/.mvn/wrapper/maven-wrapper.properties
new file mode 100644
index 00000000..f3283b08
--- /dev/null
+++ b/examples/bulk_transfer/.mvn/wrapper/maven-wrapper.properties
@@ -0,0 +1,18 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.6.3/apache-maven-3.6.3-bin.zip
+wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar
diff --git a/examples/bulk_transfer/README.md b/examples/bulk_transfer/README.md
index 8168582c..83b4d92e 100644
--- a/examples/bulk_transfer/README.md
+++ b/examples/bulk_transfer/README.md
@@ -4,16 +4,16 @@ This sample shows how to find a device, open it and transfer data from and to bu
## Prerequisites
-- Java 19
+- Java 22
- Apache Maven
- 64-bit operating system (Windows, macOS, Linux)
- A USB device with bulk IN and OUT endpoints (e.g. the test device, see https://github.com/manuelbl/JavaDoesUSB/tree/main/test-devices/loopback-stm32)
## How to run
-### Install Java 19
+### Install Java 22 or higher
-Check that *Java 19* is installed:
+Check that Java 22 or higher is installed:
```shell
$ java -version
@@ -31,17 +31,6 @@ $ mvn -version
If it is not present, install it, typically using package manager like *Homebrew* on macOS, *Chocolately* on Windows and *apt* on Linux.
-### Create the *java-does-usb* library
-
-Since the *java-does-usb* library is not yet available on Maven Central, it must be built locally:
-
-```shell
-$ cd JavaDoesUSB/java-does-usb
-$ mvn install
-```
-
-The result will be put in your local Maven repository.
-
### Run the sample
```shell
@@ -50,24 +39,22 @@ $ mvn compile exec:exec
[INFO] Scanning for projects...
[INFO]
[INFO] --------------< net.codecrete.usb.examples:bulk-transfer >--------------
-[INFO] Building bulk-transfer 0.3-SNAPSHOT
+[INFO] Building bulk-transfer 1.3.0
[INFO] --------------------------------[ jar ]---------------------------------
[INFO]
-[INFO] --- maven-resources-plugin:3.0.2:resources (default-resources) @ bulk-transfer ---
-[INFO] Using 'UTF-8' encoding to copy filtered resources.
-[INFO] skip non existing resourceDirectory /Users/manuel/Documents/Lab/JavaDoesUSB/examples/bulk_transfer/src/main/resources
+[INFO] --- maven-resources-plugin:3.3.1:resources (default-resources) @ bulk-transfer ---
+[INFO] skip non existing resourceDirectory /home/user/Documents/JavaDoesUSB/examples/bulk_transfer/src/main/resources
[INFO]
-[INFO] --- maven-compiler-plugin:3.8.0:compile (default-compile) @ bulk-transfer ---
-[INFO] Changes detected - recompiling the module!
-[INFO] Compiling 1 source file to /Users/manuel/Documents/Lab/JavaDoesUSB/examples/bulk_transfer/target/classes
+[INFO] --- maven-compiler-plugin:3.12.1:compile (default-compile) @ bulk-transfer ---
+[INFO] Nothing to compile - all classes are up to date.
[INFO]
-[INFO] --- exec-maven-plugin:3.1.0:exec (default-cli) @ bulk-transfer ---
+[INFO] --- exec-maven-plugin:3.1.1:exec (default-cli) @ bulk-transfer ---
6 bytes sent.
6 bytes received.
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
-[INFO] Total time: 3.830 s
-[INFO] Finished at: 2022-09-09T14:43:10+02:00
+[INFO] Total time: 1.228 s
+[INFO] Finished at: 2024-10-13T16:23:29+01:00
[INFO] ------------------------------------------------------------------------
```
diff --git a/examples/bulk_transfer/mvnw b/examples/bulk_transfer/mvnw
new file mode 100755
index 00000000..8d937f4c
--- /dev/null
+++ b/examples/bulk_transfer/mvnw
@@ -0,0 +1,308 @@
+#!/bin/sh
+# ----------------------------------------------------------------------------
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+# ----------------------------------------------------------------------------
+
+# ----------------------------------------------------------------------------
+# Apache Maven Wrapper startup batch script, version 3.2.0
+#
+# Required ENV vars:
+# ------------------
+# JAVA_HOME - location of a JDK home dir
+#
+# Optional ENV vars
+# -----------------
+# MAVEN_OPTS - parameters passed to the Java VM when running Maven
+# e.g. to debug Maven itself, use
+# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
+# MAVEN_SKIP_RC - flag to disable loading of mavenrc files
+# ----------------------------------------------------------------------------
+
+if [ -z "$MAVEN_SKIP_RC" ] ; then
+
+ if [ -f /usr/local/etc/mavenrc ] ; then
+ . /usr/local/etc/mavenrc
+ fi
+
+ if [ -f /etc/mavenrc ] ; then
+ . /etc/mavenrc
+ fi
+
+ if [ -f "$HOME/.mavenrc" ] ; then
+ . "$HOME/.mavenrc"
+ fi
+
+fi
+
+# OS specific support. $var _must_ be set to either true or false.
+cygwin=false;
+darwin=false;
+mingw=false
+case "$(uname)" in
+ CYGWIN*) cygwin=true ;;
+ MINGW*) mingw=true;;
+ Darwin*) darwin=true
+ # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home
+ # See https://developer.apple.com/library/mac/qa/qa1170/_index.html
+ if [ -z "$JAVA_HOME" ]; then
+ if [ -x "/usr/libexec/java_home" ]; then
+ JAVA_HOME="$(/usr/libexec/java_home)"; export JAVA_HOME
+ else
+ JAVA_HOME="/Library/Java/Home"; export JAVA_HOME
+ fi
+ fi
+ ;;
+esac
+
+if [ -z "$JAVA_HOME" ] ; then
+ if [ -r /etc/gentoo-release ] ; then
+ JAVA_HOME=$(java-config --jre-home)
+ fi
+fi
+
+# For Cygwin, ensure paths are in UNIX format before anything is touched
+if $cygwin ; then
+ [ -n "$JAVA_HOME" ] &&
+ JAVA_HOME=$(cygpath --unix "$JAVA_HOME")
+ [ -n "$CLASSPATH" ] &&
+ CLASSPATH=$(cygpath --path --unix "$CLASSPATH")
+fi
+
+# For Mingw, ensure paths are in UNIX format before anything is touched
+if $mingw ; then
+ [ -n "$JAVA_HOME" ] && [ -d "$JAVA_HOME" ] &&
+ JAVA_HOME="$(cd "$JAVA_HOME" || (echo "cannot cd into $JAVA_HOME."; exit 1); pwd)"
+fi
+
+if [ -z "$JAVA_HOME" ]; then
+ javaExecutable="$(which javac)"
+ if [ -n "$javaExecutable" ] && ! [ "$(expr "\"$javaExecutable\"" : '\([^ ]*\)')" = "no" ]; then
+ # readlink(1) is not available as standard on Solaris 10.
+ readLink=$(which readlink)
+ if [ ! "$(expr "$readLink" : '\([^ ]*\)')" = "no" ]; then
+ if $darwin ; then
+ javaHome="$(dirname "\"$javaExecutable\"")"
+ javaExecutable="$(cd "\"$javaHome\"" && pwd -P)/javac"
+ else
+ javaExecutable="$(readlink -f "\"$javaExecutable\"")"
+ fi
+ javaHome="$(dirname "\"$javaExecutable\"")"
+ javaHome=$(expr "$javaHome" : '\(.*\)/bin')
+ JAVA_HOME="$javaHome"
+ export JAVA_HOME
+ fi
+ fi
+fi
+
+if [ -z "$JAVACMD" ] ; then
+ if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ else
+ JAVACMD="$(\unset -f command 2>/dev/null; \command -v java)"
+ fi
+fi
+
+if [ ! -x "$JAVACMD" ] ; then
+ echo "Error: JAVA_HOME is not defined correctly." >&2
+ echo " We cannot execute $JAVACMD" >&2
+ exit 1
+fi
+
+if [ -z "$JAVA_HOME" ] ; then
+ echo "Warning: JAVA_HOME environment variable is not set."
+fi
+
+# traverses directory structure from process work directory to filesystem root
+# first directory with .mvn subdirectory is considered project base directory
+find_maven_basedir() {
+ if [ -z "$1" ]
+ then
+ echo "Path not specified to find_maven_basedir"
+ return 1
+ fi
+
+ basedir="$1"
+ wdir="$1"
+ while [ "$wdir" != '/' ] ; do
+ if [ -d "$wdir"/.mvn ] ; then
+ basedir=$wdir
+ break
+ fi
+ # workaround for JBEAP-8937 (on Solaris 10/Sparc)
+ if [ -d "${wdir}" ]; then
+ wdir=$(cd "$wdir/.." || exit 1; pwd)
+ fi
+ # end of workaround
+ done
+ printf '%s' "$(cd "$basedir" || exit 1; pwd)"
+}
+
+# concatenates all lines of a file
+concat_lines() {
+ if [ -f "$1" ]; then
+ # Remove \r in case we run on Windows within Git Bash
+ # and check out the repository with auto CRLF management
+ # enabled. Otherwise, we may read lines that are delimited with
+ # \r\n and produce $'-Xarg\r' rather than -Xarg due to word
+ # splitting rules.
+ tr -s '\r\n' ' ' < "$1"
+ fi
+}
+
+log() {
+ if [ "$MVNW_VERBOSE" = true ]; then
+ printf '%s\n' "$1"
+ fi
+}
+
+BASE_DIR=$(find_maven_basedir "$(dirname "$0")")
+if [ -z "$BASE_DIR" ]; then
+ exit 1;
+fi
+
+MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}; export MAVEN_PROJECTBASEDIR
+log "$MAVEN_PROJECTBASEDIR"
+
+##########################################################################################
+# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
+# This allows using the maven wrapper in projects that prohibit checking in binary data.
+##########################################################################################
+wrapperJarPath="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar"
+if [ -r "$wrapperJarPath" ]; then
+ log "Found $wrapperJarPath"
+else
+ log "Couldn't find $wrapperJarPath, downloading it ..."
+
+ if [ -n "$MVNW_REPOURL" ]; then
+ wrapperUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar"
+ else
+ wrapperUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar"
+ fi
+ while IFS="=" read -r key value; do
+ # Remove '\r' from value to allow usage on windows as IFS does not consider '\r' as a separator ( considers space, tab, new line ('\n'), and custom '=' )
+ safeValue=$(echo "$value" | tr -d '\r')
+ case "$key" in (wrapperUrl) wrapperUrl="$safeValue"; break ;;
+ esac
+ done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties"
+ log "Downloading from: $wrapperUrl"
+
+ if $cygwin; then
+ wrapperJarPath=$(cygpath --path --windows "$wrapperJarPath")
+ fi
+
+ if command -v wget > /dev/null; then
+ log "Found wget ... using wget"
+ [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--quiet"
+ if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
+ wget $QUIET "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath"
+ else
+ wget $QUIET --http-user="$MVNW_USERNAME" --http-password="$MVNW_PASSWORD" "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath"
+ fi
+ elif command -v curl > /dev/null; then
+ log "Found curl ... using curl"
+ [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--silent"
+ if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
+ curl $QUIET -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath"
+ else
+ curl $QUIET --user "$MVNW_USERNAME:$MVNW_PASSWORD" -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath"
+ fi
+ else
+ log "Falling back to using Java to download"
+ javaSource="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.java"
+ javaClass="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.class"
+ # For Cygwin, switch paths to Windows format before running javac
+ if $cygwin; then
+ javaSource=$(cygpath --path --windows "$javaSource")
+ javaClass=$(cygpath --path --windows "$javaClass")
+ fi
+ if [ -e "$javaSource" ]; then
+ if [ ! -e "$javaClass" ]; then
+ log " - Compiling MavenWrapperDownloader.java ..."
+ ("$JAVA_HOME/bin/javac" "$javaSource")
+ fi
+ if [ -e "$javaClass" ]; then
+ log " - Running MavenWrapperDownloader.java ..."
+ ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$wrapperUrl" "$wrapperJarPath") || rm -f "$wrapperJarPath"
+ fi
+ fi
+ fi
+fi
+##########################################################################################
+# End of extension
+##########################################################################################
+
+# If specified, validate the SHA-256 sum of the Maven wrapper jar file
+wrapperSha256Sum=""
+while IFS="=" read -r key value; do
+ case "$key" in (wrapperSha256Sum) wrapperSha256Sum=$value; break ;;
+ esac
+done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties"
+if [ -n "$wrapperSha256Sum" ]; then
+ wrapperSha256Result=false
+ if command -v sha256sum > /dev/null; then
+ if echo "$wrapperSha256Sum $wrapperJarPath" | sha256sum -c > /dev/null 2>&1; then
+ wrapperSha256Result=true
+ fi
+ elif command -v shasum > /dev/null; then
+ if echo "$wrapperSha256Sum $wrapperJarPath" | shasum -a 256 -c > /dev/null 2>&1; then
+ wrapperSha256Result=true
+ fi
+ else
+ echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available."
+ echo "Please install either command, or disable validation by removing 'wrapperSha256Sum' from your maven-wrapper.properties."
+ exit 1
+ fi
+ if [ $wrapperSha256Result = false ]; then
+ echo "Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised." >&2
+ echo "Investigate or delete $wrapperJarPath to attempt a clean download." >&2
+ echo "If you updated your Maven version, you need to update the specified wrapperSha256Sum property." >&2
+ exit 1
+ fi
+fi
+
+MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS"
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin; then
+ [ -n "$JAVA_HOME" ] &&
+ JAVA_HOME=$(cygpath --path --windows "$JAVA_HOME")
+ [ -n "$CLASSPATH" ] &&
+ CLASSPATH=$(cygpath --path --windows "$CLASSPATH")
+ [ -n "$MAVEN_PROJECTBASEDIR" ] &&
+ MAVEN_PROJECTBASEDIR=$(cygpath --path --windows "$MAVEN_PROJECTBASEDIR")
+fi
+
+# Provide a "standardized" way to retrieve the CLI args that will
+# work with both Windows and non-Windows executions.
+MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $*"
+export MAVEN_CMD_LINE_ARGS
+
+WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
+
+# shellcheck disable=SC2086 # safe args
+exec "$JAVACMD" \
+ $MAVEN_OPTS \
+ $MAVEN_DEBUG_OPTS \
+ -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \
+ "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \
+ ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@"
diff --git a/examples/bulk_transfer/mvnw.cmd b/examples/bulk_transfer/mvnw.cmd
new file mode 100644
index 00000000..f80fbad3
--- /dev/null
+++ b/examples/bulk_transfer/mvnw.cmd
@@ -0,0 +1,205 @@
+@REM ----------------------------------------------------------------------------
+@REM Licensed to the Apache Software Foundation (ASF) under one
+@REM or more contributor license agreements. See the NOTICE file
+@REM distributed with this work for additional information
+@REM regarding copyright ownership. The ASF licenses this file
+@REM to you under the Apache License, Version 2.0 (the
+@REM "License"); you may not use this file except in compliance
+@REM with the License. You may obtain a copy of the License at
+@REM
+@REM http://www.apache.org/licenses/LICENSE-2.0
+@REM
+@REM Unless required by applicable law or agreed to in writing,
+@REM software distributed under the License is distributed on an
+@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+@REM KIND, either express or implied. See the License for the
+@REM specific language governing permissions and limitations
+@REM under the License.
+@REM ----------------------------------------------------------------------------
+
+@REM ----------------------------------------------------------------------------
+@REM Apache Maven Wrapper startup batch script, version 3.2.0
+@REM
+@REM Required ENV vars:
+@REM JAVA_HOME - location of a JDK home dir
+@REM
+@REM Optional ENV vars
+@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands
+@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending
+@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven
+@REM e.g. to debug Maven itself, use
+@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
+@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files
+@REM ----------------------------------------------------------------------------
+
+@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on'
+@echo off
+@REM set title of command window
+title %0
+@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on'
+@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO%
+
+@REM set %HOME% to equivalent of $HOME
+if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%")
+
+@REM Execute a user defined script before this one
+if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre
+@REM check for pre script, once with legacy .bat ending and once with .cmd ending
+if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %*
+if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %*
+:skipRcPre
+
+@setlocal
+
+set ERROR_CODE=0
+
+@REM To isolate internal variables from possible post scripts, we use another setlocal
+@setlocal
+
+@REM ==== START VALIDATION ====
+if not "%JAVA_HOME%" == "" goto OkJHome
+
+echo.
+echo Error: JAVA_HOME not found in your environment. >&2
+echo Please set the JAVA_HOME variable in your environment to match the >&2
+echo location of your Java installation. >&2
+echo.
+goto error
+
+:OkJHome
+if exist "%JAVA_HOME%\bin\java.exe" goto init
+
+echo.
+echo Error: JAVA_HOME is set to an invalid directory. >&2
+echo JAVA_HOME = "%JAVA_HOME%" >&2
+echo Please set the JAVA_HOME variable in your environment to match the >&2
+echo location of your Java installation. >&2
+echo.
+goto error
+
+@REM ==== END VALIDATION ====
+
+:init
+
+@REM Find the project base dir, i.e. the directory that contains the folder ".mvn".
+@REM Fallback to current working directory if not found.
+
+set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR%
+IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir
+
+set EXEC_DIR=%CD%
+set WDIR=%EXEC_DIR%
+:findBaseDir
+IF EXIST "%WDIR%"\.mvn goto baseDirFound
+cd ..
+IF "%WDIR%"=="%CD%" goto baseDirNotFound
+set WDIR=%CD%
+goto findBaseDir
+
+:baseDirFound
+set MAVEN_PROJECTBASEDIR=%WDIR%
+cd "%EXEC_DIR%"
+goto endDetectBaseDir
+
+:baseDirNotFound
+set MAVEN_PROJECTBASEDIR=%EXEC_DIR%
+cd "%EXEC_DIR%"
+
+:endDetectBaseDir
+
+IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig
+
+@setlocal EnableExtensions EnableDelayedExpansion
+for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a
+@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS%
+
+:endReadAdditionalConfig
+
+SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe"
+set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar"
+set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
+
+set WRAPPER_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar"
+
+FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO (
+ IF "%%A"=="wrapperUrl" SET WRAPPER_URL=%%B
+)
+
+@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
+@REM This allows using the maven wrapper in projects that prohibit checking in binary data.
+if exist %WRAPPER_JAR% (
+ if "%MVNW_VERBOSE%" == "true" (
+ echo Found %WRAPPER_JAR%
+ )
+) else (
+ if not "%MVNW_REPOURL%" == "" (
+ SET WRAPPER_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar"
+ )
+ if "%MVNW_VERBOSE%" == "true" (
+ echo Couldn't find %WRAPPER_JAR%, downloading it ...
+ echo Downloading from: %WRAPPER_URL%
+ )
+
+ powershell -Command "&{"^
+ "$webclient = new-object System.Net.WebClient;"^
+ "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^
+ "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^
+ "}"^
+ "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%WRAPPER_URL%', '%WRAPPER_JAR%')"^
+ "}"
+ if "%MVNW_VERBOSE%" == "true" (
+ echo Finished downloading %WRAPPER_JAR%
+ )
+)
+@REM End of extension
+
+@REM If specified, validate the SHA-256 sum of the Maven wrapper jar file
+SET WRAPPER_SHA_256_SUM=""
+FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO (
+ IF "%%A"=="wrapperSha256Sum" SET WRAPPER_SHA_256_SUM=%%B
+)
+IF NOT %WRAPPER_SHA_256_SUM%=="" (
+ powershell -Command "&{"^
+ "$hash = (Get-FileHash \"%WRAPPER_JAR%\" -Algorithm SHA256).Hash.ToLower();"^
+ "If('%WRAPPER_SHA_256_SUM%' -ne $hash){"^
+ " Write-Output 'Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised.';"^
+ " Write-Output 'Investigate or delete %WRAPPER_JAR% to attempt a clean download.';"^
+ " Write-Output 'If you updated your Maven version, you need to update the specified wrapperSha256Sum property.';"^
+ " exit 1;"^
+ "}"^
+ "}"
+ if ERRORLEVEL 1 goto error
+)
+
+@REM Provide a "standardized" way to retrieve the CLI args that will
+@REM work with both Windows and non-Windows executions.
+set MAVEN_CMD_LINE_ARGS=%*
+
+%MAVEN_JAVA_EXE% ^
+ %JVM_CONFIG_MAVEN_PROPS% ^
+ %MAVEN_OPTS% ^
+ %MAVEN_DEBUG_OPTS% ^
+ -classpath %WRAPPER_JAR% ^
+ "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^
+ %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %*
+if ERRORLEVEL 1 goto error
+goto end
+
+:error
+set ERROR_CODE=1
+
+:end
+@endlocal & set ERROR_CODE=%ERROR_CODE%
+
+if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost
+@REM check for post script, once with legacy .bat ending and once with .cmd ending
+if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat"
+if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd"
+:skipRcPost
+
+@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on'
+if "%MAVEN_BATCH_PAUSE%"=="on" pause
+
+if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE%
+
+cmd /C exit /B %ERROR_CODE%
diff --git a/examples/bulk_transfer/pom.xml b/examples/bulk_transfer/pom.xml
index 30ae7f23..e6734349 100644
--- a/examples/bulk_transfer/pom.xml
+++ b/examples/bulk_transfer/pom.xml
@@ -6,81 +6,83 @@
net.codecrete.usb.examplesbulk-transfer
- 0.3.0
+ 1.3.0bulk-transferhttps://github.com/manuelbl/JavaDoesUSB/examples/bulk_transferUTF-8
- 19
- 19
+ 22
+ 22
+ 1.3.0net.codecrete.usbjava-does-usb
- 0.3.0
+ ${java-does-usb.version}
-
+ maven-clean-plugin
- 3.1.0
+ 3.3.2maven-resources-plugin
- 3.0.2
+ 3.3.1maven-compiler-plugin
- 3.8.0
+ 3.12.1
- 19
- --enable-preview
- 19
- 19
+ 22
+ 22
+ 22maven-surefire-plugin
- 2.22.1
+ 3.2.5
+
+ --enable-native-access=ALL-UNNAMED
+ maven-jar-plugin
- 3.0.2
+ 3.3.0maven-install-plugin
- 2.5.2
+ 3.1.1maven-deploy-plugin
- 2.8.2
+ 3.1.1maven-site-plugin
- 3.7.1
+ 3.12.1maven-project-info-reports-plugin
- 3.0.0
+ 3.5.0org.codehaus.mojoexec-maven-plugin
- 3.1.0
+ 3.1.1java
- --enable-preview--enable-native-access=ALL-UNNAMED-classpath
diff --git a/examples/bulk_transfer/src/main/java/net/codecrete/usb/examples/BulkTransfer.java b/examples/bulk_transfer/src/main/java/net/codecrete/usb/examples/BulkTransfer.java
index 34a27e95..4e738ef7 100644
--- a/examples/bulk_transfer/src/main/java/net/codecrete/usb/examples/BulkTransfer.java
+++ b/examples/bulk_transfer/src/main/java/net/codecrete/usb/examples/BulkTransfer.java
@@ -14,7 +14,8 @@
*
* This example assumes that one of the interfaces has two bulk endpoints,
* one for sending and one for receiving data. The test device fulfils
- * this requirement (see https://github.com/manuelbl/JavaDoesUSB/tree/main/test-devices/loopback-stm32)
+ * this requirement (see
+ * loopback-stm32)
*
+ * To prevent distortion, the image is cropped to fit the target width and height.
+ *
+ *
+ * The new image will be in 8-bit grayscale.
+ *
+ * @param image image to resize
+ * @param width width, in pixels
+ * @param height height, in pixels
+ * @return resized image
+ */
+ static BufferedImage resizedImage(BufferedImage image, int width, int height) {
+
+ var resizedImage = new BufferedImage(width, height, BufferedImage.TYPE_BYTE_GRAY);
+ var g = (Graphics2D) resizedImage.createGraphics();
+ g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
+ g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
+ g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
+
+ int srcWidth = image.getWidth();
+ int srcHeight = image.getHeight();
+ var targetAspectRatio = (double) width / height;
+ var srcAspectRatio = (double) srcWidth / srcHeight;
+
+ if (targetAspectRatio > srcAspectRatio) {
+ // target image is wider than source image - fit width and cut top and bottom
+ var modifiedHeight = width / srcAspectRatio;
+ var cutY = (int) Math.round((modifiedHeight - height) / 2);
+ g.drawImage(image, 0, -cutY, width, height + cutY, 0, 0, srcWidth, srcHeight, null);
+ } else {
+ // target image is narrower than source image - fit height and cut left and right
+ var modifiedWidth = height * srcAspectRatio;
+ var cutX = (int) Math.round((modifiedWidth - width) / 2);
+ g.drawImage(image, -cutX, 0, width + cutX, height, 0, 0, srcWidth, srcHeight, null);
+ }
+
+ g.dispose();
+ return resizedImage;
+ }
+}
diff --git a/examples/epaper_display/src/main/java/net/codecrete/usb/examples/IT8951Driver.java b/examples/epaper_display/src/main/java/net/codecrete/usb/examples/IT8951Driver.java
new file mode 100644
index 00000000..47ae2b29
--- /dev/null
+++ b/examples/epaper_display/src/main/java/net/codecrete/usb/examples/IT8951Driver.java
@@ -0,0 +1,371 @@
+//
+// Java Does USB
+// Copyright (c) 2023 Manuel Bleichenbacher
+// Licensed under MIT License
+// https://opensource.org/licenses/MIT
+//
+
+package net.codecrete.usb.examples;
+
+import net.codecrete.usb.Usb;
+import net.codecrete.usb.UsbDevice;
+
+import java.awt.image.BufferedImage;
+import java.awt.image.DataBufferByte;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.Arrays;
+import java.util.Random;
+
+/**
+ * Driver for the IT8951 e-paper display controller.
+ */
+public class IT8951Driver {
+
+ private static final int ENDPOINT_IN = 1;
+ private static final int ENDPOINT_OUT = 2;
+
+ private static final byte[] GET_SYS_CMD = {
+ (byte)0xfe, 0, 0x38, 0x39, 0x35, 0x31, (byte)0x80, 0, 0x01, 0, 0x02, 0, 0, 0, 0, 0
+ };
+ private static final byte[] LD_IMG_AREA_CMD = {
+ (byte)0xfe, 0x00, 0x00, 0x00, 0x00, 0x00, (byte)0xa2, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ };
+ private static final byte[] DPY_AREA_CMD = {
+ (byte)0xfe, 0x00, 0x00, 0x00, 0x00, 0x00, (byte)0x94, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ };
+
+ private UsbDevice device;
+ private int sequenceNo = 1;
+
+ private DisplayInfo displayInfo;
+
+ /**
+ * Opens the controller for communication.
+ *
+ * @throws IllegalStateException if no IT8951 device is found
+ */
+ public void open() {
+ var optionalDevice = Usb.findDevice(0x048d, 0x8951);
+ if (optionalDevice.isEmpty())
+ throw new IllegalStateException("No IT8951 device found");
+
+ device = optionalDevice.get();
+ device.detachStandardDrivers();
+ device.open();
+ try {
+ device.claimInterface(0);
+ var sysInfoBytes = readCommand(GET_SYS_CMD, DisplayInfo.LENGTH);
+ displayInfo = DisplayInfo.from(sysInfoBytes);
+
+ } catch (Exception t) {
+ device.close();
+ throw t;
+ }
+ }
+
+ /**
+ * Displays the provided image at the specified position.
+ *
+ * The provided image must be an 8-bit grayscale image,
+ * and it must fully fit within the bounds of the display.
+ *
+ *
+ * The image is rendered in 4-bit grayscale mode. For the
+ * reduction to 4-bit, Floyd-Steinberg dithering is used.
+ *
+ *
+ * @param image image
+ * @param x x-position
+ * @param y y-position
+ */
+ public void displayImage(BufferedImage image, int x, int y) {
+ int width = image.getWidth();
+ int height = image.getHeight();
+ var stride = image.getRaster().getDataBuffer().getSize() / image.getHeight();
+ int address = info().imageBufBase;
+
+ // split into bands to no exceed 60KB transfer size
+ int bandHeight = (60000 - 20) / width;
+
+ var errors = createInitialDitheringErrors(width);
+
+ for (int yOffset = 0; yOffset < height; yOffset += bandHeight) {
+ bandHeight = Math.min(bandHeight, height - yOffset);
+ var pixelData = pixelsFromImage(image, 0, yOffset, width, bandHeight);
+ errors = dither(pixelData, stride, errors);
+ loadImageArea(new Area(address, x, y + yOffset, width, bandHeight), pixelData);
+ }
+
+ displayArea(new DisplayArea(address, 2, x, y, width, height, 1));
+ }
+
+ /**
+ * Close the communication with the controller.
+ */
+ public void close() {
+ device.close();
+ device.attachStandardDrivers();
+ }
+
+ /**
+ * Gets information about the display and controller.
+ *
+ * @return information
+ */
+ public DisplayInfo info() {
+ return displayInfo;
+ }
+
+ /**
+ * Execute a read command.
+ * @param command command
+ * @param expectedLength expected length of received data (in bytes)
+ * @return received data
+ */
+ private byte[] readCommand(byte[] command, int expectedLength) {
+ var cmd = createCommandBlock(command, expectedLength, true);
+ device.transferOut(ENDPOINT_OUT, cmd);
+ byte[] result = device.transferIn(ENDPOINT_IN, 1000);
+ readStatus();
+ return result;
+ }
+
+ /**
+ * Execute a write command.
+ * @param command command
+ * @param data1 part 1 of data to be written
+ * @param data2 part 2 of data to be written (or {@code null} if there is no second part)
+ */
+ private void writeCommand(byte[] command, byte[] data1, byte[] data2) {
+ var cmd = createCommandBlock(command, data1.length + ((data2 != null) ? data2.length : 0), false);
+ device.transferOut(ENDPOINT_OUT, cmd);
+ device.transferOut(ENDPOINT_OUT, data1);
+ if (data2 != null)
+ device.transferOut(ENDPOINT_OUT, data2);
+ readStatus();
+ }
+
+ /**
+ * Execute a write command.
+ * @param command command
+ * @param data data to be written
+ */
+ private void writeCommand(byte[] command, byte[] data) {
+ writeCommand(command, data, null);
+ }
+
+ /**
+ * Wraps the given command in a command block.
+ * @param command command
+ * @param dataLength length of data to be read or written (in bytes)
+ * @param isDirectionIn indicates if data direction is in (from device to host)
+ * @return the command block
+ */
+ private byte[] createCommandBlock(byte[] command, int dataLength, boolean isDirectionIn) {
+ var cmd = ByteBuffer.allocate(15 + command.length).order(ByteOrder.LITTLE_ENDIAN);
+ cmd.putInt(0x43425355); // signature
+ cmd.putInt(sequenceNo);
+ sequenceNo += 1;
+ cmd.putInt(dataLength); // data transfer length
+ cmd.put((byte)(isDirectionIn ? 0x80 : 0x00)); // flags
+ cmd.put((byte)0); // logical unit number
+ cmd.put((byte)command.length);
+ cmd.put(command);
+ return cmd.array();
+ }
+
+ private void loadImageArea(Area area, byte[] pixelData) {
+ writeCommand(LD_IMG_AREA_CMD, area.toByteArray(), pixelData);
+ }
+
+ private void displayArea(DisplayArea area) {
+ writeCommand(DPY_AREA_CMD, area.toByteArray());
+ }
+
+ private static byte[] pixelsFromImage(BufferedImage image, int x, int y, int w, int h) {
+ var buffer = (DataBufferByte) image.getRaster().getDataBuffer();
+ var data = buffer.getData();
+ var stride = buffer.getSize() / image.getHeight();
+
+ byte[] pixels = new byte[w * h];
+ for (int iy = 0; iy < h; iy += 1)
+ System.arraycopy(data, (iy + y) * stride + x, pixels, iy * w, w);
+
+ return pixels;
+ }
+
+ /**
+ * Initializes the errors for dithering.
+ * @param width image width (in pixels)
+ * @return initial errors
+ */
+ private static int[] createInitialDitheringErrors(int width) {
+ // initialize errors with random values in the range [-127, 128].
+ int[] errors = new int[width + 2];
+ var random = new Random();
+
+ for (int i = 0; i < errors.length; i++)
+ errors[i] = random.nextInt(256) - 127;
+
+ return errors;
+ }
+
+ /**
+ * Quantizes the provided grayscale pixel data to 16 levels of gray
+ * using Floyd-Steinberg dithering.
+ *
+ * The pixel data is quantized in place. The resulting grayscale values
+ * are 0, 16, 32, ... 240.
+ *
+ *
+ * The pixel data consists of a single byte per pixel. A new pixel line
+ * starts every stride bytes. The actual image width can be
+ * slightly shorter.
+ *
+ * @param pixels array with pixel data to be modified
+ * @param stride the
+ * @param errors errors carried forward from the previous band
+ * @return errors to be carried forward to the next band
+ */
+ private static int[] dither(byte[] pixels, int stride, int[] errors) {
+ int[] currentErrors = errors;
+ int[] nextErrors = new int[errors.length];
+
+ for (int offset = 0; offset < pixels.length; offset += stride) {
+ Arrays.fill(nextErrors, 0);
+ ditherRow(pixels, offset, currentErrors, nextErrors);
+
+ // swap error arrays
+ int[] errs = currentErrors;
+ currentErrors = nextErrors;
+ nextErrors = errs;
+ }
+
+ return currentErrors;
+ }
+
+ private static void ditherRow(byte[] pixels, int offset, int[] currentErrors, int[] nextErrors) {
+ int w = currentErrors.length - 2;
+ for (int i = 0; i < w; i += 1) {
+ // scale range from [0, 255] to [0, 240]
+ int targetValue = (((pixels[offset + i] & 0xff) * 16 + currentErrors[i + 1]) * 15 + 127) / 255;
+ int quantizedValue;
+ if (targetValue <= 0)
+ quantizedValue = 0;
+ else if (targetValue >= 240)
+ quantizedValue = 240;
+ else
+ quantizedValue = (targetValue + 8) & 0xf0;
+ pixels[offset + i] = (byte) quantizedValue;
+
+ int quantizationError = targetValue - quantizedValue;
+ currentErrors[i + 2] += quantizationError * 7;
+ nextErrors[i] += quantizationError * 3;
+ nextErrors[i + 1] += quantizationError * 5 ;
+ nextErrors[i + 2] += quantizationError;
+ }
+ }
+
+ /**
+ * Read the status block.
+ * @return status block
+ */
+ private Status readStatus() {
+ var result = device.transferIn(ENDPOINT_IN, 1000);
+ if (result.length == 13)
+ return Status.from(result);
+
+ throw new IllegalStateException(String.format("Unexpected length of status block (%d)", result.length));
+ }
+
+ /**
+ * Display and controller information
+ *
+ * @param standardCmdNo standard command number 2T-con Communication Protocol
+ * @param extendedCmdNo extended command number
+ * @param signature signature (0x31 0x35 0x39 0x38 (8951))
+ * @param version command table version
+ * @param width display width (in pixels)
+ * @param height display height (in pixels)
+ * @param updateBufBase update buffer base address
+ * @param imageBufBase image buffer base address (index 0)
+ * @param temperatureNo temperature segment number
+ * @param modeNo display mode number
+ * @param frameCount frame count for each mode (8 elements)
+ * @param numImgBuf number of image buffers
+ */
+ public record DisplayInfo(
+ int standardCmdNo,
+ int extendedCmdNo,
+ int signature,
+ int version,
+ int width,
+ int height,
+ int updateBufBase,
+ int imageBufBase,
+ int temperatureNo,
+ /// Display mode
+ int modeNo,
+ int[] frameCount, // 8 elements
+ int numImgBuf
+ ) {
+ static DisplayInfo from(byte[] bytes) {
+ var buf = ByteBuffer.wrap(bytes).order(ByteOrder.BIG_ENDIAN);
+ int standardCmdNo = buf.getInt();
+ int extendedCmdNo = buf.getInt();
+ int signature = buf.getInt();
+ int version = buf.getInt();
+ int width = buf.getInt();
+ int height = buf.getInt();
+ int updateBufBase = buf.getInt();
+ int imageBufBase = buf.getInt();
+ int temperatureNo = buf.getInt();
+ int modeNo = buf.getInt();
+ int[] frameCount = new int[8];
+ for (int i = 0; i < 8; i += 1)
+ frameCount[i] = buf.getInt();
+ int numImgBuf = buf.getInt();
+ return new DisplayInfo(standardCmdNo, extendedCmdNo, signature, version, width, height,
+ updateBufBase, imageBufBase, temperatureNo, modeNo, frameCount, numImgBuf);
+ }
+
+ static final int LENGTH = 112;
+ }
+
+ record Status(int sequence, int dataRemaining, byte status) {
+ static Status from(byte[] bytes) {
+ var buf = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN);
+ int sequence = buf.getInt();
+ int dataLeft = buf.getInt();
+ byte status = buf.get();
+ return new Status(sequence, dataLeft, status);
+ }
+ }
+
+ record Area(int address, int x, int y, int w, int h) {
+ byte[] toByteArray() {
+ var buf = ByteBuffer.allocate(20).order(ByteOrder.BIG_ENDIAN);
+ buf.putInt(address);
+ buf.putInt(x);
+ buf.putInt(y);
+ buf.putInt(w);
+ buf.putInt(h);
+ return buf.array();
+ }
+ }
+
+ record DisplayArea(int address, int mode, int x, int y, int w, int h, int wait_ready) {
+ byte[] toByteArray() {
+ var buf = ByteBuffer.allocate(28).order(ByteOrder.BIG_ENDIAN);
+ buf.putInt(address);
+ buf.putInt(mode);
+ buf.putInt(x);
+ buf.putInt(y);
+ buf.putInt(w);
+ buf.putInt(h);
+ buf.putInt(wait_ready);
+ return buf.array();
+ }
+ }
+}
diff --git a/examples/epaper_display/tiger.jpg b/examples/epaper_display/tiger.jpg
new file mode 100644
index 00000000..82060f99
Binary files /dev/null and b/examples/epaper_display/tiger.jpg differ
diff --git a/examples/monitor/.mvn/wrapper/maven-wrapper.jar b/examples/monitor/.mvn/wrapper/maven-wrapper.jar
new file mode 100644
index 00000000..cb28b0e3
Binary files /dev/null and b/examples/monitor/.mvn/wrapper/maven-wrapper.jar differ
diff --git a/examples/monitor/.mvn/wrapper/maven-wrapper.properties b/examples/monitor/.mvn/wrapper/maven-wrapper.properties
new file mode 100644
index 00000000..f3283b08
--- /dev/null
+++ b/examples/monitor/.mvn/wrapper/maven-wrapper.properties
@@ -0,0 +1,18 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.6.3/apache-maven-3.6.3-bin.zip
+wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar
diff --git a/examples/monitor/README.md b/examples/monitor/README.md
index 33acf3ba..f32c60c9 100644
--- a/examples/monitor/README.md
+++ b/examples/monitor/README.md
@@ -4,15 +4,15 @@ This sample program monitors USB devices as they are connected and disconnected.
## Prerequisites
-- Java 19
+- Java 22
- Apache Maven
- 64-bit operating system (Windows, macOS, Linux)
## How to run
-### Install Java 19
+### Install Java 22 or higher
-Check that *Java 19* is installed:
+Check that Java 22 or higher is installed:
```shell
$ java -version
@@ -30,37 +30,35 @@ $ mvn -version
If it is not present, install it, typically using package manager like *Homebrew* on macOS, *Chocolately* on Windows and *apt* on Linux.
-### Create the *java-does-usb* library
-
-Since the *java-does-usb* library is not yet available on Maven Central, it must be built locally:
-
-```shell
-$ cd JavaDoesUSB/java-does-usb
-$ mvn install
-```
-
-The result will be put in your local Maven repository.
-
### Run the sample
```shell
$ cd JavaDoesUSB/examples/monitor
$ mvn compile exec:exec
+
[INFO] Scanning for projects...
[INFO]
[INFO] -----------------< net.codecrete.usb.examples:monitor >-----------------
-[INFO] Building monitor 0.3-SNAPSHOT
+[INFO] Building monitor 1.3.0
[INFO] --------------------------------[ jar ]---------------------------------
[INFO]
-[INFO] --- maven-resources-plugin:3.0.2:resources (default-resources) @ monitor ---
-[INFO] Using 'UTF-8' encoding to copy filtered resources.
-[INFO] skip non existing resourceDirectory /Users/me/Documents/JavaDoesUSB/examples/monitor/src/main/resources
+[INFO] --- maven-resources-plugin:3.3.1:resources (default-resources) @ monitor ---
+[INFO] Copying 1 resource from src/main/resources to target/classes
[INFO]
-[INFO] --- maven-compiler-plugin:3.8.0:compile (default-compile) @ monitor ---
-[INFO] Nothing to compile - all classes are up to date
+[INFO] --- maven-compiler-plugin:3.12.1:compile (default-compile) @ monitor ---
+[INFO] Nothing to compile - all classes are up to date.
[INFO]
-[INFO] --- exec-maven-plugin:3.1.0:exec (default-cli) @ monitor ---
-Present: VID: 0xcafe, PID: 0xceaf, manufacturer: JavaDoesUSB, product: Loopback, serial: 8D8F515C5456, ID: 4295291950
-Present: VID: 0x1a40, PID: 0x0801, manufacturer: null, product: USB 2.0 Hub, serial: null, ID: 4295291734
+[INFO] --- exec-maven-plugin:3.1.1:exec (default-cli) @ monitor ---
+Present: VID: 0x1d6b, PID: 0x0002, manufacturer: Linux 6.5.0-18-generic xhci-hcd, product: xHCI Host Controller, serial: 0000:00:14.0, ID: /dev/bus/usb/001/001
+Present: VID: 0xcafe, PID: 0xceaf, manufacturer: JavaDoesUSB, product: Loopback, serial: 35A737883336, ID: /dev/bus/usb/001/005
Monitoring... Press ENTER to quit.
+Disconnected: VID: 0xcafe, PID: 0xceaf, manufacturer: JavaDoesUSB, product: Loopback, serial: 35A737883336, ID: /dev/bus/usb/001/005
+Connected: VID: 0xcafe, PID: 0xceaf, manufacturer: JavaDoesUSB, product: Loopback, serial: 35A737883336, ID: /dev/bus/usb/001/009
+
+[INFO] ------------------------------------------------------------------------
+[INFO] BUILD SUCCESS
+[INFO] ------------------------------------------------------------------------
+[INFO] Total time: 17.647 s
+[INFO] Finished at: 2024-10-13T16:50:59+01:00
+[INFO] -----------------------------------------------------------------------
```
diff --git a/examples/monitor/mvnw b/examples/monitor/mvnw
new file mode 100755
index 00000000..8d937f4c
--- /dev/null
+++ b/examples/monitor/mvnw
@@ -0,0 +1,308 @@
+#!/bin/sh
+# ----------------------------------------------------------------------------
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+# ----------------------------------------------------------------------------
+
+# ----------------------------------------------------------------------------
+# Apache Maven Wrapper startup batch script, version 3.2.0
+#
+# Required ENV vars:
+# ------------------
+# JAVA_HOME - location of a JDK home dir
+#
+# Optional ENV vars
+# -----------------
+# MAVEN_OPTS - parameters passed to the Java VM when running Maven
+# e.g. to debug Maven itself, use
+# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
+# MAVEN_SKIP_RC - flag to disable loading of mavenrc files
+# ----------------------------------------------------------------------------
+
+if [ -z "$MAVEN_SKIP_RC" ] ; then
+
+ if [ -f /usr/local/etc/mavenrc ] ; then
+ . /usr/local/etc/mavenrc
+ fi
+
+ if [ -f /etc/mavenrc ] ; then
+ . /etc/mavenrc
+ fi
+
+ if [ -f "$HOME/.mavenrc" ] ; then
+ . "$HOME/.mavenrc"
+ fi
+
+fi
+
+# OS specific support. $var _must_ be set to either true or false.
+cygwin=false;
+darwin=false;
+mingw=false
+case "$(uname)" in
+ CYGWIN*) cygwin=true ;;
+ MINGW*) mingw=true;;
+ Darwin*) darwin=true
+ # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home
+ # See https://developer.apple.com/library/mac/qa/qa1170/_index.html
+ if [ -z "$JAVA_HOME" ]; then
+ if [ -x "/usr/libexec/java_home" ]; then
+ JAVA_HOME="$(/usr/libexec/java_home)"; export JAVA_HOME
+ else
+ JAVA_HOME="/Library/Java/Home"; export JAVA_HOME
+ fi
+ fi
+ ;;
+esac
+
+if [ -z "$JAVA_HOME" ] ; then
+ if [ -r /etc/gentoo-release ] ; then
+ JAVA_HOME=$(java-config --jre-home)
+ fi
+fi
+
+# For Cygwin, ensure paths are in UNIX format before anything is touched
+if $cygwin ; then
+ [ -n "$JAVA_HOME" ] &&
+ JAVA_HOME=$(cygpath --unix "$JAVA_HOME")
+ [ -n "$CLASSPATH" ] &&
+ CLASSPATH=$(cygpath --path --unix "$CLASSPATH")
+fi
+
+# For Mingw, ensure paths are in UNIX format before anything is touched
+if $mingw ; then
+ [ -n "$JAVA_HOME" ] && [ -d "$JAVA_HOME" ] &&
+ JAVA_HOME="$(cd "$JAVA_HOME" || (echo "cannot cd into $JAVA_HOME."; exit 1); pwd)"
+fi
+
+if [ -z "$JAVA_HOME" ]; then
+ javaExecutable="$(which javac)"
+ if [ -n "$javaExecutable" ] && ! [ "$(expr "\"$javaExecutable\"" : '\([^ ]*\)')" = "no" ]; then
+ # readlink(1) is not available as standard on Solaris 10.
+ readLink=$(which readlink)
+ if [ ! "$(expr "$readLink" : '\([^ ]*\)')" = "no" ]; then
+ if $darwin ; then
+ javaHome="$(dirname "\"$javaExecutable\"")"
+ javaExecutable="$(cd "\"$javaHome\"" && pwd -P)/javac"
+ else
+ javaExecutable="$(readlink -f "\"$javaExecutable\"")"
+ fi
+ javaHome="$(dirname "\"$javaExecutable\"")"
+ javaHome=$(expr "$javaHome" : '\(.*\)/bin')
+ JAVA_HOME="$javaHome"
+ export JAVA_HOME
+ fi
+ fi
+fi
+
+if [ -z "$JAVACMD" ] ; then
+ if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ else
+ JAVACMD="$(\unset -f command 2>/dev/null; \command -v java)"
+ fi
+fi
+
+if [ ! -x "$JAVACMD" ] ; then
+ echo "Error: JAVA_HOME is not defined correctly." >&2
+ echo " We cannot execute $JAVACMD" >&2
+ exit 1
+fi
+
+if [ -z "$JAVA_HOME" ] ; then
+ echo "Warning: JAVA_HOME environment variable is not set."
+fi
+
+# traverses directory structure from process work directory to filesystem root
+# first directory with .mvn subdirectory is considered project base directory
+find_maven_basedir() {
+ if [ -z "$1" ]
+ then
+ echo "Path not specified to find_maven_basedir"
+ return 1
+ fi
+
+ basedir="$1"
+ wdir="$1"
+ while [ "$wdir" != '/' ] ; do
+ if [ -d "$wdir"/.mvn ] ; then
+ basedir=$wdir
+ break
+ fi
+ # workaround for JBEAP-8937 (on Solaris 10/Sparc)
+ if [ -d "${wdir}" ]; then
+ wdir=$(cd "$wdir/.." || exit 1; pwd)
+ fi
+ # end of workaround
+ done
+ printf '%s' "$(cd "$basedir" || exit 1; pwd)"
+}
+
+# concatenates all lines of a file
+concat_lines() {
+ if [ -f "$1" ]; then
+ # Remove \r in case we run on Windows within Git Bash
+ # and check out the repository with auto CRLF management
+ # enabled. Otherwise, we may read lines that are delimited with
+ # \r\n and produce $'-Xarg\r' rather than -Xarg due to word
+ # splitting rules.
+ tr -s '\r\n' ' ' < "$1"
+ fi
+}
+
+log() {
+ if [ "$MVNW_VERBOSE" = true ]; then
+ printf '%s\n' "$1"
+ fi
+}
+
+BASE_DIR=$(find_maven_basedir "$(dirname "$0")")
+if [ -z "$BASE_DIR" ]; then
+ exit 1;
+fi
+
+MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}; export MAVEN_PROJECTBASEDIR
+log "$MAVEN_PROJECTBASEDIR"
+
+##########################################################################################
+# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
+# This allows using the maven wrapper in projects that prohibit checking in binary data.
+##########################################################################################
+wrapperJarPath="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar"
+if [ -r "$wrapperJarPath" ]; then
+ log "Found $wrapperJarPath"
+else
+ log "Couldn't find $wrapperJarPath, downloading it ..."
+
+ if [ -n "$MVNW_REPOURL" ]; then
+ wrapperUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar"
+ else
+ wrapperUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar"
+ fi
+ while IFS="=" read -r key value; do
+ # Remove '\r' from value to allow usage on windows as IFS does not consider '\r' as a separator ( considers space, tab, new line ('\n'), and custom '=' )
+ safeValue=$(echo "$value" | tr -d '\r')
+ case "$key" in (wrapperUrl) wrapperUrl="$safeValue"; break ;;
+ esac
+ done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties"
+ log "Downloading from: $wrapperUrl"
+
+ if $cygwin; then
+ wrapperJarPath=$(cygpath --path --windows "$wrapperJarPath")
+ fi
+
+ if command -v wget > /dev/null; then
+ log "Found wget ... using wget"
+ [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--quiet"
+ if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
+ wget $QUIET "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath"
+ else
+ wget $QUIET --http-user="$MVNW_USERNAME" --http-password="$MVNW_PASSWORD" "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath"
+ fi
+ elif command -v curl > /dev/null; then
+ log "Found curl ... using curl"
+ [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--silent"
+ if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
+ curl $QUIET -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath"
+ else
+ curl $QUIET --user "$MVNW_USERNAME:$MVNW_PASSWORD" -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath"
+ fi
+ else
+ log "Falling back to using Java to download"
+ javaSource="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.java"
+ javaClass="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.class"
+ # For Cygwin, switch paths to Windows format before running javac
+ if $cygwin; then
+ javaSource=$(cygpath --path --windows "$javaSource")
+ javaClass=$(cygpath --path --windows "$javaClass")
+ fi
+ if [ -e "$javaSource" ]; then
+ if [ ! -e "$javaClass" ]; then
+ log " - Compiling MavenWrapperDownloader.java ..."
+ ("$JAVA_HOME/bin/javac" "$javaSource")
+ fi
+ if [ -e "$javaClass" ]; then
+ log " - Running MavenWrapperDownloader.java ..."
+ ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$wrapperUrl" "$wrapperJarPath") || rm -f "$wrapperJarPath"
+ fi
+ fi
+ fi
+fi
+##########################################################################################
+# End of extension
+##########################################################################################
+
+# If specified, validate the SHA-256 sum of the Maven wrapper jar file
+wrapperSha256Sum=""
+while IFS="=" read -r key value; do
+ case "$key" in (wrapperSha256Sum) wrapperSha256Sum=$value; break ;;
+ esac
+done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties"
+if [ -n "$wrapperSha256Sum" ]; then
+ wrapperSha256Result=false
+ if command -v sha256sum > /dev/null; then
+ if echo "$wrapperSha256Sum $wrapperJarPath" | sha256sum -c > /dev/null 2>&1; then
+ wrapperSha256Result=true
+ fi
+ elif command -v shasum > /dev/null; then
+ if echo "$wrapperSha256Sum $wrapperJarPath" | shasum -a 256 -c > /dev/null 2>&1; then
+ wrapperSha256Result=true
+ fi
+ else
+ echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available."
+ echo "Please install either command, or disable validation by removing 'wrapperSha256Sum' from your maven-wrapper.properties."
+ exit 1
+ fi
+ if [ $wrapperSha256Result = false ]; then
+ echo "Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised." >&2
+ echo "Investigate or delete $wrapperJarPath to attempt a clean download." >&2
+ echo "If you updated your Maven version, you need to update the specified wrapperSha256Sum property." >&2
+ exit 1
+ fi
+fi
+
+MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS"
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin; then
+ [ -n "$JAVA_HOME" ] &&
+ JAVA_HOME=$(cygpath --path --windows "$JAVA_HOME")
+ [ -n "$CLASSPATH" ] &&
+ CLASSPATH=$(cygpath --path --windows "$CLASSPATH")
+ [ -n "$MAVEN_PROJECTBASEDIR" ] &&
+ MAVEN_PROJECTBASEDIR=$(cygpath --path --windows "$MAVEN_PROJECTBASEDIR")
+fi
+
+# Provide a "standardized" way to retrieve the CLI args that will
+# work with both Windows and non-Windows executions.
+MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $*"
+export MAVEN_CMD_LINE_ARGS
+
+WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
+
+# shellcheck disable=SC2086 # safe args
+exec "$JAVACMD" \
+ $MAVEN_OPTS \
+ $MAVEN_DEBUG_OPTS \
+ -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \
+ "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \
+ ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@"
diff --git a/examples/monitor/mvnw.cmd b/examples/monitor/mvnw.cmd
new file mode 100644
index 00000000..f80fbad3
--- /dev/null
+++ b/examples/monitor/mvnw.cmd
@@ -0,0 +1,205 @@
+@REM ----------------------------------------------------------------------------
+@REM Licensed to the Apache Software Foundation (ASF) under one
+@REM or more contributor license agreements. See the NOTICE file
+@REM distributed with this work for additional information
+@REM regarding copyright ownership. The ASF licenses this file
+@REM to you under the Apache License, Version 2.0 (the
+@REM "License"); you may not use this file except in compliance
+@REM with the License. You may obtain a copy of the License at
+@REM
+@REM http://www.apache.org/licenses/LICENSE-2.0
+@REM
+@REM Unless required by applicable law or agreed to in writing,
+@REM software distributed under the License is distributed on an
+@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+@REM KIND, either express or implied. See the License for the
+@REM specific language governing permissions and limitations
+@REM under the License.
+@REM ----------------------------------------------------------------------------
+
+@REM ----------------------------------------------------------------------------
+@REM Apache Maven Wrapper startup batch script, version 3.2.0
+@REM
+@REM Required ENV vars:
+@REM JAVA_HOME - location of a JDK home dir
+@REM
+@REM Optional ENV vars
+@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands
+@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending
+@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven
+@REM e.g. to debug Maven itself, use
+@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
+@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files
+@REM ----------------------------------------------------------------------------
+
+@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on'
+@echo off
+@REM set title of command window
+title %0
+@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on'
+@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO%
+
+@REM set %HOME% to equivalent of $HOME
+if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%")
+
+@REM Execute a user defined script before this one
+if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre
+@REM check for pre script, once with legacy .bat ending and once with .cmd ending
+if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %*
+if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %*
+:skipRcPre
+
+@setlocal
+
+set ERROR_CODE=0
+
+@REM To isolate internal variables from possible post scripts, we use another setlocal
+@setlocal
+
+@REM ==== START VALIDATION ====
+if not "%JAVA_HOME%" == "" goto OkJHome
+
+echo.
+echo Error: JAVA_HOME not found in your environment. >&2
+echo Please set the JAVA_HOME variable in your environment to match the >&2
+echo location of your Java installation. >&2
+echo.
+goto error
+
+:OkJHome
+if exist "%JAVA_HOME%\bin\java.exe" goto init
+
+echo.
+echo Error: JAVA_HOME is set to an invalid directory. >&2
+echo JAVA_HOME = "%JAVA_HOME%" >&2
+echo Please set the JAVA_HOME variable in your environment to match the >&2
+echo location of your Java installation. >&2
+echo.
+goto error
+
+@REM ==== END VALIDATION ====
+
+:init
+
+@REM Find the project base dir, i.e. the directory that contains the folder ".mvn".
+@REM Fallback to current working directory if not found.
+
+set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR%
+IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir
+
+set EXEC_DIR=%CD%
+set WDIR=%EXEC_DIR%
+:findBaseDir
+IF EXIST "%WDIR%"\.mvn goto baseDirFound
+cd ..
+IF "%WDIR%"=="%CD%" goto baseDirNotFound
+set WDIR=%CD%
+goto findBaseDir
+
+:baseDirFound
+set MAVEN_PROJECTBASEDIR=%WDIR%
+cd "%EXEC_DIR%"
+goto endDetectBaseDir
+
+:baseDirNotFound
+set MAVEN_PROJECTBASEDIR=%EXEC_DIR%
+cd "%EXEC_DIR%"
+
+:endDetectBaseDir
+
+IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig
+
+@setlocal EnableExtensions EnableDelayedExpansion
+for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a
+@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS%
+
+:endReadAdditionalConfig
+
+SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe"
+set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar"
+set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
+
+set WRAPPER_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar"
+
+FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO (
+ IF "%%A"=="wrapperUrl" SET WRAPPER_URL=%%B
+)
+
+@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
+@REM This allows using the maven wrapper in projects that prohibit checking in binary data.
+if exist %WRAPPER_JAR% (
+ if "%MVNW_VERBOSE%" == "true" (
+ echo Found %WRAPPER_JAR%
+ )
+) else (
+ if not "%MVNW_REPOURL%" == "" (
+ SET WRAPPER_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar"
+ )
+ if "%MVNW_VERBOSE%" == "true" (
+ echo Couldn't find %WRAPPER_JAR%, downloading it ...
+ echo Downloading from: %WRAPPER_URL%
+ )
+
+ powershell -Command "&{"^
+ "$webclient = new-object System.Net.WebClient;"^
+ "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^
+ "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^
+ "}"^
+ "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%WRAPPER_URL%', '%WRAPPER_JAR%')"^
+ "}"
+ if "%MVNW_VERBOSE%" == "true" (
+ echo Finished downloading %WRAPPER_JAR%
+ )
+)
+@REM End of extension
+
+@REM If specified, validate the SHA-256 sum of the Maven wrapper jar file
+SET WRAPPER_SHA_256_SUM=""
+FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO (
+ IF "%%A"=="wrapperSha256Sum" SET WRAPPER_SHA_256_SUM=%%B
+)
+IF NOT %WRAPPER_SHA_256_SUM%=="" (
+ powershell -Command "&{"^
+ "$hash = (Get-FileHash \"%WRAPPER_JAR%\" -Algorithm SHA256).Hash.ToLower();"^
+ "If('%WRAPPER_SHA_256_SUM%' -ne $hash){"^
+ " Write-Output 'Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised.';"^
+ " Write-Output 'Investigate or delete %WRAPPER_JAR% to attempt a clean download.';"^
+ " Write-Output 'If you updated your Maven version, you need to update the specified wrapperSha256Sum property.';"^
+ " exit 1;"^
+ "}"^
+ "}"
+ if ERRORLEVEL 1 goto error
+)
+
+@REM Provide a "standardized" way to retrieve the CLI args that will
+@REM work with both Windows and non-Windows executions.
+set MAVEN_CMD_LINE_ARGS=%*
+
+%MAVEN_JAVA_EXE% ^
+ %JVM_CONFIG_MAVEN_PROPS% ^
+ %MAVEN_OPTS% ^
+ %MAVEN_DEBUG_OPTS% ^
+ -classpath %WRAPPER_JAR% ^
+ "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^
+ %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %*
+if ERRORLEVEL 1 goto error
+goto end
+
+:error
+set ERROR_CODE=1
+
+:end
+@endlocal & set ERROR_CODE=%ERROR_CODE%
+
+if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost
+@REM check for post script, once with legacy .bat ending and once with .cmd ending
+if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat"
+if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd"
+:skipRcPost
+
+@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on'
+if "%MAVEN_BATCH_PAUSE%"=="on" pause
+
+if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE%
+
+cmd /C exit /B %ERROR_CODE%
diff --git a/examples/monitor/pom.xml b/examples/monitor/pom.xml
index 6f0f8e1a..52206a11 100644
--- a/examples/monitor/pom.xml
+++ b/examples/monitor/pom.xml
@@ -6,81 +6,98 @@
net.codecrete.usb.examplesmonitor
- 0.3.0
+ 1.3.0monitorhttps://github.com/manuelbl/JavaDoesUSB/examples/monitorUTF-8
- 19
- 19
+ 22
+ 22
+ 1.3.0net.codecrete.usbjava-does-usb
- 0.3.0
+ ${java-does-usb.version}
+
+
+ org.tinylog
+ tinylog-api
+ 2.6.2
+
+
+ org.tinylog
+ tinylog-impl
+ 2.6.2
+
+
+ org.tinylog
+ jsl-tinylog
+ 2.6.2
-
+ maven-clean-plugin
- 3.1.0
+ 3.3.2maven-resources-plugin
- 3.0.2
+ 3.3.1maven-compiler-plugin
- 3.8.0
+ 3.12.1
- 19
- --enable-preview
- 19
- 19
+ 22
+ 22
+ 22maven-surefire-plugin
- 2.22.1
+ 3.2.5
+
+ --enable-native-access=ALL-UNNAMED
+ maven-jar-plugin
- 3.0.2
+ 3.3.0maven-install-plugin
- 2.5.2
+ 3.1.1maven-deploy-plugin
- 2.8.2
+ 3.1.1maven-site-plugin
- 3.7.1
+ 3.12.1maven-project-info-reports-plugin
- 3.0.0
+ 3.5.0org.codehaus.mojoexec-maven-plugin
- 3.1.0
+ 3.1.1java
- --enable-preview--enable-native-access=ALL-UNNAMED-classpath
diff --git a/examples/monitor/src/main/java/net/codecrete/usb/examples/Monitor.java b/examples/monitor/src/main/java/net/codecrete/usb/examples/Monitor.java
index a897ba94..482d1b9f 100644
--- a/examples/monitor/src/main/java/net/codecrete/usb/examples/Monitor.java
+++ b/examples/monitor/src/main/java/net/codecrete/usb/examples/Monitor.java
@@ -7,8 +7,8 @@
package net.codecrete.usb.examples;
-import net.codecrete.usb.USB;
-import net.codecrete.usb.USBDevice;
+import net.codecrete.usb.Usb;
+import net.codecrete.usb.UsbDevice;
import java.io.IOException;
@@ -19,11 +19,11 @@ public class Monitor {
public static void main(String[] args) throws IOException {
// register callbacks for events
- USB.setOnDeviceConnected((device) -> printDetails(device, "Connected"));
- USB.setOnDeviceDisconnected((device) -> printDetails(device, "Disconnected"));
+ Usb.setOnDeviceConnected(device -> printDetails(device, "Connected"));
+ Usb.setOnDeviceDisconnected(device -> printDetails(device, "Disconnected"));
// display the already present USB devices
- for (var device : USB.getAllDevices())
+ for (var device : Usb.getDevices())
printDetails(device, "Present");
// wait for ENTER to quit program
@@ -31,7 +31,7 @@ public static void main(String[] args) throws IOException {
System.in.read();
}
- private static void printDetails(USBDevice device, String event) {
+ private static void printDetails(UsbDevice device, String event) {
System.out.printf("%-14s", event + ":");
System.out.println(device.toString());
}
diff --git a/examples/monitor/src/main/resources/tinylog.properties b/examples/monitor/src/main/resources/tinylog.properties
new file mode 100644
index 00000000..08b5f1d0
--- /dev/null
+++ b/examples/monitor/src/main/resources/tinylog.properties
@@ -0,0 +1,3 @@
+writer = console
+writer.format = {date: HH:mm:ss.SSS} {level|size=5} [{class|size=40}] {message}
+writer.level = debug
diff --git a/examples/monitor_kotlin/.gitignore b/examples/monitor_kotlin/.gitignore
new file mode 100644
index 00000000..5ff6309b
--- /dev/null
+++ b/examples/monitor_kotlin/.gitignore
@@ -0,0 +1,38 @@
+target/
+!.mvn/wrapper/maven-wrapper.jar
+!**/src/main/**/target/
+!**/src/test/**/target/
+
+### IntelliJ IDEA ###
+.idea/modules.xml
+.idea/jarRepositories.xml
+.idea/compiler.xml
+.idea/libraries/
+*.iws
+*.iml
+*.ipr
+
+### Eclipse ###
+.apt_generated
+.classpath
+.factorypath
+.project
+.settings
+.springBeans
+.sts4-cache
+
+### NetBeans ###
+/nbproject/private/
+/nbbuild/
+/dist/
+/nbdist/
+/.nb-gradle/
+build/
+!**/src/main/**/build/
+!**/src/test/**/build/
+
+### VS Code ###
+.vscode/
+
+### Mac OS ###
+.DS_Store
\ No newline at end of file
diff --git a/examples/monitor_kotlin/.mvn/wrapper/maven-wrapper.properties b/examples/monitor_kotlin/.mvn/wrapper/maven-wrapper.properties
new file mode 100644
index 00000000..c595b009
--- /dev/null
+++ b/examples/monitor_kotlin/.mvn/wrapper/maven-wrapper.properties
@@ -0,0 +1,3 @@
+wrapperVersion=3.3.4
+distributionType=only-script
+distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.14/apache-maven-3.9.14-bin.zip
diff --git a/examples/monitor_kotlin/README.md b/examples/monitor_kotlin/README.md
new file mode 100644
index 00000000..f98849da
--- /dev/null
+++ b/examples/monitor_kotlin/README.md
@@ -0,0 +1,49 @@
+# USB Device Monitoring (Kotlin)
+
+This sample enumerates the connected USB devices and provides information about the interfaces and endpoints.
+
+## Prerequisites
+
+- Java 25
+- Apache Maven
+- 64-bit operating system (Windows, macOS, Linux)
+
+## How to run
+
+### Install Java 25 or higher
+
+Check that Java 25 or higher is installed:
+
+```shell
+$ java -version
+```
+
+If not, download and install it, e.g. from [Azul](https://www.azul.com/downloads/?package=jdk).
+
+### Install Maven
+
+Check that *Maven* is installed:
+
+```shell
+$ mvn -version
+```
+
+If it is not present, install it, typically using package manager like *Homebrew* on macOS, *Chocolately* on Windows and *apt* on Linux.
+
+### Build a self-contained jar file
+
+```shell
+$ cd JavaDoesUSB/examples/monitor_kotlin
+$ mvn clean package
+```
+
+### Run the jar
+
+```shell
+$ java --enable-native-access=ALL-UNNAMED -jar target/monitor-1.3.0-jar-with-dependencies.jar
+Present: VID: 0x1d6b, PID: 0x0002, manufacturer: Linux 6.5.0-18-generic xhci-hcd, product: xHCI Host Controller, serial: 0000:00:14.0, ID: /dev/bus/usb/001/001
+Present: VID: 0xcafe, PID: 0xceaf, manufacturer: JavaDoesUSB, product: Loopback, serial: 35A737883336, ID: /dev/bus/usb/001/009
+Monitoring... Press ENTER to quit.
+Disconnected: VID: 0xcafe, PID: 0xceaf, manufacturer: JavaDoesUSB, product: Loopback, serial: 35A737883336, ID: /dev/bus/usb/001/009
+Connected: VID: 0xcafe, PID: 0xceaf, manufacturer: JavaDoesUSB, product: Loopback, serial: 35A737883336, ID: /dev/bus/usb/001/010
+```
diff --git a/examples/monitor_kotlin/mvnw b/examples/monitor_kotlin/mvnw
new file mode 100755
index 00000000..bd8896bf
--- /dev/null
+++ b/examples/monitor_kotlin/mvnw
@@ -0,0 +1,295 @@
+#!/bin/sh
+# ----------------------------------------------------------------------------
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+# ----------------------------------------------------------------------------
+
+# ----------------------------------------------------------------------------
+# Apache Maven Wrapper startup batch script, version 3.3.4
+#
+# Optional ENV vars
+# -----------------
+# JAVA_HOME - location of a JDK home dir, required when download maven via java source
+# MVNW_REPOURL - repo url base for downloading maven distribution
+# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven
+# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output
+# ----------------------------------------------------------------------------
+
+set -euf
+[ "${MVNW_VERBOSE-}" != debug ] || set -x
+
+# OS specific support.
+native_path() { printf %s\\n "$1"; }
+case "$(uname)" in
+CYGWIN* | MINGW*)
+ [ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")"
+ native_path() { cygpath --path --windows "$1"; }
+ ;;
+esac
+
+# set JAVACMD and JAVACCMD
+set_java_home() {
+ # For Cygwin and MinGW, ensure paths are in Unix format before anything is touched
+ if [ -n "${JAVA_HOME-}" ]; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ]; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ JAVACCMD="$JAVA_HOME/jre/sh/javac"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ JAVACCMD="$JAVA_HOME/bin/javac"
+
+ if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then
+ echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2
+ echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2
+ return 1
+ fi
+ fi
+ else
+ JAVACMD="$(
+ 'set' +e
+ 'unset' -f command 2>/dev/null
+ 'command' -v java
+ )" || :
+ JAVACCMD="$(
+ 'set' +e
+ 'unset' -f command 2>/dev/null
+ 'command' -v javac
+ )" || :
+
+ if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then
+ echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2
+ return 1
+ fi
+ fi
+}
+
+# hash string like Java String::hashCode
+hash_string() {
+ str="${1:-}" h=0
+ while [ -n "$str" ]; do
+ char="${str%"${str#?}"}"
+ h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296))
+ str="${str#?}"
+ done
+ printf %x\\n $h
+}
+
+verbose() { :; }
+[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; }
+
+die() {
+ printf %s\\n "$1" >&2
+ exit 1
+}
+
+trim() {
+ # MWRAPPER-139:
+ # Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds.
+ # Needed for removing poorly interpreted newline sequences when running in more
+ # exotic environments such as mingw bash on Windows.
+ printf "%s" "${1}" | tr -d '[:space:]'
+}
+
+scriptDir="$(dirname "$0")"
+scriptName="$(basename "$0")"
+
+# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties
+while IFS="=" read -r key value; do
+ case "${key-}" in
+ distributionUrl) distributionUrl=$(trim "${value-}") ;;
+ distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;;
+ esac
+done <"$scriptDir/.mvn/wrapper/maven-wrapper.properties"
+[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties"
+
+case "${distributionUrl##*/}" in
+maven-mvnd-*bin.*)
+ MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/
+ case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in
+ *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;;
+ :Darwin*x86_64) distributionPlatform=darwin-amd64 ;;
+ :Darwin*arm64) distributionPlatform=darwin-aarch64 ;;
+ :Linux*x86_64*) distributionPlatform=linux-amd64 ;;
+ *)
+ echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2
+ distributionPlatform=linux-amd64
+ ;;
+ esac
+ distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip"
+ ;;
+maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;;
+*) MVN_CMD="mvn${scriptName#mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;;
+esac
+
+# apply MVNW_REPOURL and calculate MAVEN_HOME
+# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/
+[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}"
+distributionUrlName="${distributionUrl##*/}"
+distributionUrlNameMain="${distributionUrlName%.*}"
+distributionUrlNameMain="${distributionUrlNameMain%-bin}"
+MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}"
+MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")"
+
+exec_maven() {
+ unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || :
+ exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD"
+}
+
+if [ -d "$MAVEN_HOME" ]; then
+ verbose "found existing MAVEN_HOME at $MAVEN_HOME"
+ exec_maven "$@"
+fi
+
+case "${distributionUrl-}" in
+*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;;
+*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;;
+esac
+
+# prepare tmp dir
+if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then
+ clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; }
+ trap clean HUP INT TERM EXIT
+else
+ die "cannot create temp dir"
+fi
+
+mkdir -p -- "${MAVEN_HOME%/*}"
+
+# Download and Install Apache Maven
+verbose "Couldn't find MAVEN_HOME, downloading and installing it ..."
+verbose "Downloading from: $distributionUrl"
+verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName"
+
+# select .zip or .tar.gz
+if ! command -v unzip >/dev/null; then
+ distributionUrl="${distributionUrl%.zip}.tar.gz"
+ distributionUrlName="${distributionUrl##*/}"
+fi
+
+# verbose opt
+__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR=''
+[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v
+
+# normalize http auth
+case "${MVNW_PASSWORD:+has-password}" in
+'') MVNW_USERNAME='' MVNW_PASSWORD='' ;;
+has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;;
+esac
+
+if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then
+ verbose "Found wget ... using wget"
+ wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl"
+elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then
+ verbose "Found curl ... using curl"
+ curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl"
+elif set_java_home; then
+ verbose "Falling back to use Java to download"
+ javaSource="$TMP_DOWNLOAD_DIR/Downloader.java"
+ targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName"
+ cat >"$javaSource" <<-END
+ public class Downloader extends java.net.Authenticator
+ {
+ protected java.net.PasswordAuthentication getPasswordAuthentication()
+ {
+ return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() );
+ }
+ public static void main( String[] args ) throws Exception
+ {
+ setDefault( new Downloader() );
+ java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() );
+ }
+ }
+ END
+ # For Cygwin/MinGW, switch paths to Windows format before running javac and java
+ verbose " - Compiling Downloader.java ..."
+ "$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java"
+ verbose " - Running Downloader.java ..."
+ "$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")"
+fi
+
+# If specified, validate the SHA-256 sum of the Maven distribution zip file
+if [ -n "${distributionSha256Sum-}" ]; then
+ distributionSha256Result=false
+ if [ "$MVN_CMD" = mvnd.sh ]; then
+ echo "Checksum validation is not supported for maven-mvnd." >&2
+ echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2
+ exit 1
+ elif command -v sha256sum >/dev/null; then
+ if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c - >/dev/null 2>&1; then
+ distributionSha256Result=true
+ fi
+ elif command -v shasum >/dev/null; then
+ if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then
+ distributionSha256Result=true
+ fi
+ else
+ echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2
+ echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2
+ exit 1
+ fi
+ if [ $distributionSha256Result = false ]; then
+ echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2
+ echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2
+ exit 1
+ fi
+fi
+
+# unzip and move
+if command -v unzip >/dev/null; then
+ unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip"
+else
+ tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar"
+fi
+
+# Find the actual extracted directory name (handles snapshots where filename != directory name)
+actualDistributionDir=""
+
+# First try the expected directory name (for regular distributions)
+if [ -d "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" ]; then
+ if [ -f "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/bin/$MVN_CMD" ]; then
+ actualDistributionDir="$distributionUrlNameMain"
+ fi
+fi
+
+# If not found, search for any directory with the Maven executable (for snapshots)
+if [ -z "$actualDistributionDir" ]; then
+ # enable globbing to iterate over items
+ set +f
+ for dir in "$TMP_DOWNLOAD_DIR"/*; do
+ if [ -d "$dir" ]; then
+ if [ -f "$dir/bin/$MVN_CMD" ]; then
+ actualDistributionDir="$(basename "$dir")"
+ break
+ fi
+ fi
+ done
+ set -f
+fi
+
+if [ -z "$actualDistributionDir" ]; then
+ verbose "Contents of $TMP_DOWNLOAD_DIR:"
+ verbose "$(ls -la "$TMP_DOWNLOAD_DIR")"
+ die "Could not find Maven distribution directory in extracted archive"
+fi
+
+verbose "Found extracted Maven distribution directory: $actualDistributionDir"
+printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$actualDistributionDir/mvnw.url"
+mv -- "$TMP_DOWNLOAD_DIR/$actualDistributionDir" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME"
+
+clean || :
+exec_maven "$@"
diff --git a/examples/monitor_kotlin/mvnw.cmd b/examples/monitor_kotlin/mvnw.cmd
new file mode 100644
index 00000000..5761d948
--- /dev/null
+++ b/examples/monitor_kotlin/mvnw.cmd
@@ -0,0 +1,189 @@
+<# : batch portion
+@REM ----------------------------------------------------------------------------
+@REM Licensed to the Apache Software Foundation (ASF) under one
+@REM or more contributor license agreements. See the NOTICE file
+@REM distributed with this work for additional information
+@REM regarding copyright ownership. The ASF licenses this file
+@REM to you under the Apache License, Version 2.0 (the
+@REM "License"); you may not use this file except in compliance
+@REM with the License. You may obtain a copy of the License at
+@REM
+@REM http://www.apache.org/licenses/LICENSE-2.0
+@REM
+@REM Unless required by applicable law or agreed to in writing,
+@REM software distributed under the License is distributed on an
+@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+@REM KIND, either express or implied. See the License for the
+@REM specific language governing permissions and limitations
+@REM under the License.
+@REM ----------------------------------------------------------------------------
+
+@REM ----------------------------------------------------------------------------
+@REM Apache Maven Wrapper startup batch script, version 3.3.4
+@REM
+@REM Optional ENV vars
+@REM MVNW_REPOURL - repo url base for downloading maven distribution
+@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven
+@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output
+@REM ----------------------------------------------------------------------------
+
+@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0)
+@SET __MVNW_CMD__=
+@SET __MVNW_ERROR__=
+@SET __MVNW_PSMODULEP_SAVE=%PSModulePath%
+@SET PSModulePath=
+@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @(
+ IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B)
+)
+@SET PSModulePath=%__MVNW_PSMODULEP_SAVE%
+@SET __MVNW_PSMODULEP_SAVE=
+@SET __MVNW_ARG0_NAME__=
+@SET MVNW_USERNAME=
+@SET MVNW_PASSWORD=
+@IF NOT "%__MVNW_CMD__%"=="" ("%__MVNW_CMD__%" %*)
+@echo Cannot start maven from wrapper >&2 && exit /b 1
+@GOTO :EOF
+: end batch / begin powershell #>
+
+$ErrorActionPreference = "Stop"
+if ($env:MVNW_VERBOSE -eq "true") {
+ $VerbosePreference = "Continue"
+}
+
+# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties
+$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl
+if (!$distributionUrl) {
+ Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties"
+}
+
+switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) {
+ "maven-mvnd-*" {
+ $USE_MVND = $true
+ $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip"
+ $MVN_CMD = "mvnd.cmd"
+ break
+ }
+ default {
+ $USE_MVND = $false
+ $MVN_CMD = $script -replace '^mvnw','mvn'
+ break
+ }
+}
+
+# apply MVNW_REPOURL and calculate MAVEN_HOME
+# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/
+if ($env:MVNW_REPOURL) {
+ $MVNW_REPO_PATTERN = if ($USE_MVND -eq $False) { "/org/apache/maven/" } else { "/maven/mvnd/" }
+ $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace "^.*$MVNW_REPO_PATTERN",'')"
+}
+$distributionUrlName = $distributionUrl -replace '^.*/',''
+$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$',''
+
+$MAVEN_M2_PATH = "$HOME/.m2"
+if ($env:MAVEN_USER_HOME) {
+ $MAVEN_M2_PATH = "$env:MAVEN_USER_HOME"
+}
+
+if (-not (Test-Path -Path $MAVEN_M2_PATH)) {
+ New-Item -Path $MAVEN_M2_PATH -ItemType Directory | Out-Null
+}
+
+$MAVEN_WRAPPER_DISTS = $null
+if ((Get-Item $MAVEN_M2_PATH).Target[0] -eq $null) {
+ $MAVEN_WRAPPER_DISTS = "$MAVEN_M2_PATH/wrapper/dists"
+} else {
+ $MAVEN_WRAPPER_DISTS = (Get-Item $MAVEN_M2_PATH).Target[0] + "/wrapper/dists"
+}
+
+$MAVEN_HOME_PARENT = "$MAVEN_WRAPPER_DISTS/$distributionUrlNameMain"
+$MAVEN_HOME_NAME = ([System.Security.Cryptography.SHA256]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join ''
+$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME"
+
+if (Test-Path -Path "$MAVEN_HOME" -PathType Container) {
+ Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME"
+ Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"
+ exit $?
+}
+
+if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) {
+ Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl"
+}
+
+# prepare tmp dir
+$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile
+$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir"
+$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null
+trap {
+ if ($TMP_DOWNLOAD_DIR.Exists) {
+ try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
+ catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
+ }
+}
+
+New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null
+
+# Download and Install Apache Maven
+Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..."
+Write-Verbose "Downloading from: $distributionUrl"
+Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName"
+
+$webclient = New-Object System.Net.WebClient
+if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) {
+ $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD)
+}
+[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
+$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null
+
+# If specified, validate the SHA-256 sum of the Maven distribution zip file
+$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum
+if ($distributionSha256Sum) {
+ if ($USE_MVND) {
+ Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties."
+ }
+ Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash
+ if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) {
+ Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property."
+ }
+}
+
+# unzip and move
+Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null
+
+# Find the actual extracted directory name (handles snapshots where filename != directory name)
+$actualDistributionDir = ""
+
+# First try the expected directory name (for regular distributions)
+$expectedPath = Join-Path "$TMP_DOWNLOAD_DIR" "$distributionUrlNameMain"
+$expectedMvnPath = Join-Path "$expectedPath" "bin/$MVN_CMD"
+if ((Test-Path -Path $expectedPath -PathType Container) -and (Test-Path -Path $expectedMvnPath -PathType Leaf)) {
+ $actualDistributionDir = $distributionUrlNameMain
+}
+
+# If not found, search for any directory with the Maven executable (for snapshots)
+if (!$actualDistributionDir) {
+ Get-ChildItem -Path "$TMP_DOWNLOAD_DIR" -Directory | ForEach-Object {
+ $testPath = Join-Path $_.FullName "bin/$MVN_CMD"
+ if (Test-Path -Path $testPath -PathType Leaf) {
+ $actualDistributionDir = $_.Name
+ }
+ }
+}
+
+if (!$actualDistributionDir) {
+ Write-Error "Could not find Maven distribution directory in extracted archive"
+}
+
+Write-Verbose "Found extracted Maven distribution directory: $actualDistributionDir"
+Rename-Item -Path "$TMP_DOWNLOAD_DIR/$actualDistributionDir" -NewName $MAVEN_HOME_NAME | Out-Null
+try {
+ Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null
+} catch {
+ if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) {
+ Write-Error "fail to move MAVEN_HOME"
+ }
+} finally {
+ try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
+ catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
+}
+
+Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"
diff --git a/examples/monitor_kotlin/pom.xml b/examples/monitor_kotlin/pom.xml
new file mode 100644
index 00000000..2612f221
--- /dev/null
+++ b/examples/monitor_kotlin/pom.xml
@@ -0,0 +1,92 @@
+
+ 4.0.0
+
+ net.codecrete.usb.examples
+ monitor
+ 1.3.0
+ jar
+
+ monitor
+ https://github.com/manuelbl/JavaDoesUSB/examples/monitor_kotlin
+
+
+ 2.3.21
+ true
+ UTF-8
+ net.codecrete.usb.examples.MonitorKt
+ 1.3.0
+
+
+
+ ${project.basedir}/src/main/kotlin
+ ${project.basedir}/src/test/kotlin
+
+
+
+ org.jetbrains.kotlin
+ kotlin-maven-plugin
+ ${kotlin.version}
+ true
+
+
+ org.apache.maven.plugins
+ maven-assembly-plugin
+ 3.6.0
+
+
+ make-assembly
+ package
+
+ single
+
+
+
+
+ ${main.class}
+
+
+
+ jar-with-dependencies
+
+
+
+
+
+
+
+
+
+
+ org.jetbrains.kotlin
+ kotlin-stdlib
+ ${kotlin.version}
+
+
+ net.codecrete.usb
+ java-does-usb
+ ${java-does-usb.version}
+
+
+ org.tinylog
+ tinylog-api
+ 2.7.0
+
+
+ org.tinylog
+ tinylog-impl
+ 2.7.0
+
+
+ org.tinylog
+ jsl-tinylog
+ 2.7.0
+
+
+ junit
+ junit
+ 4.13.2
+ test
+
+
+
diff --git a/examples/monitor_kotlin/src/main/kotlin/net/codecrete/usb/examples/Monitor.kt b/examples/monitor_kotlin/src/main/kotlin/net/codecrete/usb/examples/Monitor.kt
new file mode 100644
index 00000000..b16d568a
--- /dev/null
+++ b/examples/monitor_kotlin/src/main/kotlin/net/codecrete/usb/examples/Monitor.kt
@@ -0,0 +1,41 @@
+//
+// Java Does USB
+// Copyright (c) 2023 Manuel Bleichenbacher
+// Licensed under MIT License
+// https://opensource.org/licenses/MIT
+//
+package net.codecrete.usb.examples
+
+import net.codecrete.usb.Usb
+import net.codecrete.usb.UsbDevice
+
+/**
+ * Sample application monitoring USB devices as they are connected and disconnected.
+ */
+
+fun main() {
+ Monitor().monitor()
+}
+
+class Monitor {
+ fun monitor() {
+ // register callbacks for events
+ Usb.setOnDeviceConnected { device -> printDetails(device, "Connected") }
+ Usb.setOnDeviceDisconnected { device ->
+ printDetails( device, "Disconnected")
+ }
+
+ // display the already present USB devices
+ for (device in Usb.getDevices())
+ printDetails(device, "Present")
+
+ // wait for ENTER to quit program
+ println("Monitoring... Press ENTER to quit.")
+ System.`in`.read()
+ }
+
+ private fun printDetails(device: UsbDevice, event: String) {
+ print(String.format("%-14s", "$event:"))
+ println(device.toString())
+ }
+}
\ No newline at end of file
diff --git a/examples/monitor_native/README.md b/examples/monitor_native/README.md
new file mode 100644
index 00000000..3018321d
--- /dev/null
+++ b/examples/monitor_native/README.md
@@ -0,0 +1,49 @@
+# Device Monitoring (Native)
+
+This example project demonstrates how to build a native application that
+monitors USB devices using [GraalVM](https://www.graalvm.org/) and
+this _Java Does USB_ library.
+
+## Build and Run
+
+### Prerequisites
+
+- [GraalVM](https://www.graalvm.org/) 25 or higher
+- [Maven](https://maven.apache.org/) 3.9 or higher
+
+
+### Preparation
+
+GraalVM needs help to learn about the Java FFM downcall and upcall descriptors,
+and it needs some help to include all required methods. The relevant items
+differ from operating system to operating system. When building the native image,
+the operating system must be selected by changing the path in the file
+`native-image.properties` in the directory
+`src/main/resources/META-INF/native-image/net.codecrete.usb.examples/monitor_native`.
+
+```properties
+Args = --enable-native-access=ALL-UNNAMED -H:ConfigurationFileDirectories=config/macos
+```
+
+Note the last word of the line. In this case, it is `macos`. Change this to
+`linux` or `windows` if needed.
+
+In your own Maven project, you might also need to move the file or rather rename
+directory. It must be named according to the pattern
+`src/main/resources/META-INF/native-image//`.
+
+Also check that the environment variable `JAVA_HOME` points to the GraalVM directory.
+
+
+### Building
+
+```shell
+mvn -Pnative package
+```
+
+
+### Running
+
+```shell
+./target/monitor-native
+```
diff --git a/examples/monitor_native/config/linux/reachability-metadata.json b/examples/monitor_native/config/linux/reachability-metadata.json
new file mode 100644
index 00000000..e1746c04
--- /dev/null
+++ b/examples/monitor_native/config/linux/reachability-metadata.json
@@ -0,0 +1,111 @@
+{
+ "foreign": {
+ "downcalls": [
+ {
+ "returnType": "jint",
+ "parameterTypes": [
+ "void*",
+ "void*"
+ ]
+ },
+ {
+ "returnType": "jint",
+ "parameterTypes": [
+ "void*",
+ "void*",
+ "void*"
+ ]
+ },
+ {
+ "returnType": "jint",
+ "parameterTypes": [
+ "jint",
+ "jlong",
+ "void*"
+ ],
+ "options": {
+ "captureCallState": true,
+ "firstVariadicArg": 2
+ }
+ },
+ {
+ "returnType": "jint",
+ "parameterTypes": [
+ "jint"
+ ]
+ },
+ {
+ "returnType": "void*",
+ "parameterTypes": [
+ "void*"
+ ]
+ },
+ {
+ "returnType": "jint",
+ "parameterTypes": [
+ "jint",
+ "void*",
+ "jint",
+ "jint"
+ ],
+ "options": {
+ "captureCallState": true
+ }
+ },
+ {
+ "returnType": "void*",
+ "parameterTypes": []
+ },
+ {
+ "returnType": "jint",
+ "parameterTypes": [
+ "jint"
+ ],
+ "options": {
+ "captureCallState": true
+ }
+ },
+ {
+ "returnType": "jint",
+ "parameterTypes": [
+ "void*",
+ "jint"
+ ],
+ "options": {
+ "captureCallState": true
+ }
+ },
+ {
+ "returnType": "void*",
+ "parameterTypes": [
+ "void*",
+ "void*"
+ ]
+ },
+ {
+ "returnType": "void*",
+ "parameterTypes": [
+ "jint"
+ ]
+ },
+ {
+ "returnType": "jint",
+ "parameterTypes": [
+ "void*"
+ ]
+ },
+ {
+ "returnType": "jint",
+ "parameterTypes": [
+ "jint",
+ "jint",
+ "jint",
+ "void*"
+ ],
+ "options": {
+ "captureCallState": true
+ }
+ }
+ ]
+ }
+}
\ No newline at end of file
diff --git a/examples/monitor_native/config/macos/reachability-metadata.json b/examples/monitor_native/config/macos/reachability-metadata.json
new file mode 100644
index 00000000..113fb587
--- /dev/null
+++ b/examples/monitor_native/config/macos/reachability-metadata.json
@@ -0,0 +1,318 @@
+{
+ "foreign": {
+ "upcalls": [
+ {
+ "returnType": "void",
+ "parameterTypes": [
+ "void*",
+ "jint",
+ "void*"
+ ]
+ },
+ {
+ "returnType": "void*",
+ "parameterTypes": [
+ "void*",
+ "jint",
+ "void*",
+ "void*"
+ ]
+ },
+ {
+ "returnType": "void",
+ "parameterTypes": [
+ "void*",
+ "jint"
+ ]
+ }
+ ],
+ "downcalls": [
+ {
+ "returnType": "void",
+ "parameterTypes": [
+ "void*",
+ "void*",
+ "void*"
+ ]
+ },
+ {
+ "returnType": "void",
+ "parameterTypes": [
+ "void*",
+ "struct(jlong,jlong)",
+ "void*"
+ ]
+ },
+ {
+ "returnType": "void*",
+ "parameterTypes": [
+ "void*",
+ "void*",
+ "jlong"
+ ]
+ },
+ {
+ "returnType": "jint",
+ "parameterTypes": [
+ "void*",
+ "void*",
+ "void*"
+ ]
+ },
+ {
+ "returnType": "jint",
+ "parameterTypes": [
+ "void*",
+ "jint",
+ "void*",
+ "jdouble",
+ "jdouble",
+ "void*",
+ "void*"
+ ]
+ },
+ {
+ "returnType": "jint",
+ "parameterTypes": [
+ "void*",
+ "void*",
+ "void*",
+ "void*",
+ "void*",
+ "void*"
+ ]
+ },
+ {
+ "returnType": "jint",
+ "parameterTypes": [
+ "jint"
+ ]
+ },
+ {
+ "returnType": "jlong",
+ "parameterTypes": [
+ "void*"
+ ]
+ },
+ {
+ "returnType": "void*",
+ "parameterTypes": []
+ },
+ {
+ "returnType": "jint",
+ "parameterTypes": [
+ "void*",
+ "void*",
+ "void*",
+ "void*"
+ ]
+ },
+ {
+ "returnType": "jint",
+ "parameterTypes": [
+ "jint",
+ "void*",
+ "void*",
+ "void*",
+ "void*"
+ ]
+ },
+ {
+ "returnType": "void*",
+ "parameterTypes": [
+ "jint",
+ "void*",
+ "void*",
+ "jint"
+ ]
+ },
+ {
+ "returnType": "void*",
+ "parameterTypes": [
+ "void*",
+ "void*"
+ ]
+ },
+ {
+ "returnType": "jint",
+ "parameterTypes": [
+ "void*",
+ "jbyte"
+ ]
+ },
+ {
+ "returnType": "void*",
+ "parameterTypes": [
+ "void*",
+ "jint",
+ "void*",
+ "void*"
+ ]
+ },
+ {
+ "returnType": "jbyte",
+ "parameterTypes": [
+ "void*",
+ "jlong",
+ "void*"
+ ]
+ },
+ {
+ "returnType": "void*",
+ "parameterTypes": [
+ "void*",
+ "struct(jbyte,jbyte,jbyte,jbyte,jbyte,jbyte,jbyte,jbyte,jbyte,jbyte,jbyte,jbyte,jbyte,jbyte,jbyte,jbyte)"
+ ]
+ },
+ {
+ "returnType": "void",
+ "parameterTypes": []
+ },
+ {
+ "returnType": "jint",
+ "parameterTypes": [
+ "void*",
+ "jbyte",
+ "void*",
+ "jint",
+ "jint",
+ "jint",
+ "void*",
+ "void*"
+ ]
+ },
+ {
+ "returnType": "jint",
+ "parameterTypes": [
+ "void*",
+ "jbyte",
+ "void*",
+ "void*",
+ "void*",
+ "void*",
+ "void*"
+ ]
+ },
+ {
+ "returnType": "jint",
+ "parameterTypes": [
+ "void*",
+ "void*"
+ ]
+ },
+ {
+ "returnType": "void*",
+ "parameterTypes": [
+ "void*"
+ ]
+ },
+ {
+ "returnType": "jlong",
+ "parameterTypes": []
+ },
+ {
+ "returnType": "void",
+ "parameterTypes": [
+ "void*"
+ ]
+ },
+ {
+ "returnType": "jint",
+ "parameterTypes": [
+ "void*",
+ "jbyte",
+ "void*"
+ ]
+ },
+ {
+ "returnType": "jint",
+ "parameterTypes": [
+ "jint",
+ "void*"
+ ]
+ },
+ {
+ "returnType": "void*",
+ "parameterTypes": [
+ "jint"
+ ]
+ },
+ {
+ "returnType": "jint",
+ "parameterTypes": [
+ "void*"
+ ]
+ },
+ {
+ "returnType": "void*",
+ "parameterTypes": [
+ "void*",
+ "void*",
+ "void*",
+ "void*",
+ "void*"
+ ]
+ },
+ {
+ "returnType": "jint",
+ "parameterTypes": [
+ "void*",
+ "jbyte",
+ "void*",
+ "jint",
+ "void*",
+ "void*"
+ ]
+ },
+ {
+ "returnType": "jint",
+ "parameterTypes": [
+ "void*",
+ "struct(jbyte,jbyte,jbyte,jbyte,jbyte,jbyte,jbyte,jbyte,jbyte,jbyte,jbyte,jbyte,jbyte,jbyte,jbyte,jbyte)",
+ "void*"
+ ]
+ },
+ {
+ "returnType": "struct(jbyte,jbyte,jbyte,jbyte,jbyte,jbyte,jbyte,jbyte,jbyte,jbyte,jbyte,jbyte,jbyte,jbyte,jbyte,jbyte)",
+ "parameterTypes": [
+ "void*"
+ ]
+ },
+ {
+ "returnType": "void",
+ "parameterTypes": [
+ "void*",
+ "jint"
+ ]
+ }
+ ]
+ },
+ "reflection": [
+ {
+ "type": "net.codecrete.usb.macos.gen.corefoundation.CFMessagePortCreateLocal$callout$Function",
+ "methods": [
+ {
+ "name": "apply",
+ "parameterTypes": [
+ "java.lang.foreign.MemorySegment",
+ "int",
+ "java.lang.foreign.MemorySegment",
+ "java.lang.foreign.MemorySegment"
+ ]
+ }
+ ]
+ },
+ {
+ "type": "net.codecrete.usb.macos.gen.iokit.IOServiceAddMatchingNotification$callback$Function",
+ "methods": [
+ {
+ "name": "apply",
+ "parameterTypes": [
+ "java.lang.foreign.MemorySegment",
+ "int"
+ ]
+ }
+ ]
+ }
+ ]
+}
diff --git a/examples/monitor_native/config/windows/reachability-metadata.json b/examples/monitor_native/config/windows/reachability-metadata.json
new file mode 100644
index 00000000..f3abe566
--- /dev/null
+++ b/examples/monitor_native/config/windows/reachability-metadata.json
@@ -0,0 +1,377 @@
+{
+ "foreign": {
+ "downcalls": [
+ {
+ "returnType": "jint",
+ "parameterTypes": [
+ "jint",
+ "void*",
+ "jint",
+ "jint",
+ "void*",
+ "jint",
+ "void*"
+ ],
+ "options": {
+ "captureCallState": true
+ }
+ },
+ {
+ "returnType": "jint",
+ "parameterTypes": [
+ "jlong"
+ ],
+ "options": {
+ "captureCallState": true
+ }
+ },
+ {
+ "returnType": "jint",
+ "parameterTypes": [
+ "jlong",
+ "jint",
+ "void*"
+ ],
+ "options": {
+ "captureCallState": true
+ }
+ },
+ {
+ "returnType": "jint",
+ "parameterTypes": [
+ "jlong",
+ "void*"
+ ],
+ "options": {
+ "captureCallState": true
+ }
+ },
+ {
+ "returnType": "jint",
+ "parameterTypes": [
+ "jlong",
+ "void*",
+ "jint",
+ "void*"
+ ],
+ "options": {
+ "captureCallState": true
+ }
+ },
+ {
+ "returnType": "jint",
+ "parameterTypes": [
+ "jlong",
+ "void*",
+ "void*",
+ "jint",
+ "void*"
+ ],
+ "options": {
+ "captureCallState": true
+ }
+ },
+ {
+ "returnType": "jint",
+ "parameterTypes": [
+ "jlong",
+ "void*",
+ "void*",
+ "jint",
+ "void*",
+ "void*"
+ ],
+ "options": {
+ "captureCallState": true
+ }
+ },
+ {
+ "returnType": "jint",
+ "parameterTypes": [
+ "jlong",
+ "void*",
+ "void*",
+ "void*",
+ "void*",
+ "jint",
+ "void*",
+ "jint"
+ ],
+ "options": {
+ "captureCallState": true
+ }
+ },
+ {
+ "returnType": "jint",
+ "parameterTypes": [
+ "void*"
+ ]
+ },
+ {
+ "returnType": "jint",
+ "parameterTypes": [
+ "void*",
+ "jbyte"
+ ],
+ "options": {
+ "captureCallState": true
+ }
+ },
+ {
+ "returnType": "jint",
+ "parameterTypes": [
+ "void*",
+ "jbyte",
+ "jint",
+ "jint",
+ "void*"
+ ],
+ "options": {
+ "captureCallState": true
+ }
+ },
+ {
+ "returnType": "jint",
+ "parameterTypes": [
+ "void*",
+ "jbyte",
+ "void*"
+ ],
+ "options": {
+ "captureCallState": true
+ }
+ },
+ {
+ "returnType": "jint",
+ "parameterTypes": [
+ "void*",
+ "jbyte",
+ "void*",
+ "jint",
+ "void*",
+ "void*"
+ ],
+ "options": {
+ "captureCallState": true
+ }
+ },
+ {
+ "returnType": "jint",
+ "parameterTypes": [
+ "void*",
+ "jint",
+ "void*",
+ "jint",
+ "void*",
+ "jint",
+ "void*",
+ "void*"
+ ],
+ "options": {
+ "captureCallState": true
+ }
+ },
+ {
+ "returnType": "jint",
+ "parameterTypes": [
+ "void*",
+ "void*"
+ ]
+ },
+ {
+ "returnType": "jint",
+ "parameterTypes": [
+ "void*",
+ "void*"
+ ],
+ "options": {
+ "captureCallState": true
+ }
+ },
+ {
+ "returnType": "jint",
+ "parameterTypes": [
+ "void*",
+ "void*",
+ "jint",
+ "jint"
+ ],
+ "options": {
+ "captureCallState": true
+ }
+ },
+ {
+ "returnType": "jint",
+ "parameterTypes": [
+ "void*",
+ "void*",
+ "void*",
+ "void*",
+ "jint"
+ ],
+ "options": {
+ "captureCallState": true
+ }
+ },
+ {
+ "returnType": "jint",
+ "parameterTypes": [
+ "void*",
+ "void*",
+ "void*",
+ "void*",
+ "void*",
+ "void*"
+ ]
+ },
+ {
+ "returnType": "jlong",
+ "parameterTypes": [
+ "void*",
+ "jint",
+ "jlong",
+ "jlong"
+ ]
+ },
+ {
+ "returnType": "jlong",
+ "parameterTypes": [
+ "void*",
+ "void*"
+ ],
+ "options": {
+ "captureCallState": true
+ }
+ },
+ {
+ "returnType": "jlong",
+ "parameterTypes": [
+ "void*",
+ "void*",
+ "void*",
+ "jint"
+ ],
+ "options": {
+ "captureCallState": true
+ }
+ },
+ {
+ "returnType": "jshort",
+ "parameterTypes": [
+ "void*"
+ ],
+ "options": {
+ "captureCallState": true
+ }
+ },
+ {
+ "returnType": "void*",
+ "parameterTypes": [
+ "jint",
+ "void*",
+ "void*",
+ "jint",
+ "jint",
+ "jint",
+ "jint",
+ "jint",
+ "void*",
+ "void*",
+ "void*",
+ "void*"
+ ],
+ "options": {
+ "captureCallState": true
+ }
+ },
+ {
+ "returnType": "void*",
+ "parameterTypes": [
+ "jlong",
+ "void*",
+ "jint",
+ "jint",
+ "jint",
+ "jint"
+ ],
+ "options": {
+ "captureCallState": true
+ }
+ },
+ {
+ "returnType": "void*",
+ "parameterTypes": [
+ "void*"
+ ],
+ "options": {
+ "captureCallState": true
+ }
+ },
+ {
+ "returnType": "void*",
+ "parameterTypes": [
+ "void*",
+ "jint",
+ "jint",
+ "void*",
+ "jint",
+ "jint",
+ "void*"
+ ],
+ "options": {
+ "captureCallState": true
+ }
+ },
+ {
+ "returnType": "void*",
+ "parameterTypes": [
+ "void*",
+ "void*",
+ "jint"
+ ],
+ "options": {
+ "captureCallState": true
+ }
+ },
+ {
+ "returnType": "void*",
+ "parameterTypes": [
+ "void*",
+ "void*",
+ "jlong",
+ "jint"
+ ],
+ "options": {
+ "captureCallState": true
+ }
+ }
+ ],
+ "upcalls": [
+ {
+ "returnType": "jlong",
+ "parameterTypes": [
+ "void*",
+ "jint",
+ "jlong",
+ "jlong"
+ ]
+ }
+ ]
+ },
+ "reflection": [
+ {
+ "type": "windows.win32.ui.windowsandmessaging.WNDPROC$Function",
+ "methods": [
+ {
+ "name": "invoke",
+ "parameterTypes": [
+ "java.lang.foreign.MemorySegment",
+ "int",
+ "long",
+ "long"
+ ]
+ }
+ ]
+ }
+ ]
+}
diff --git a/examples/monitor_native/pom.xml b/examples/monitor_native/pom.xml
new file mode 100644
index 00000000..68e930de
--- /dev/null
+++ b/examples/monitor_native/pom.xml
@@ -0,0 +1,85 @@
+
+ 4.0.0
+
+ net.codecrete.usb.examples
+ monitor_native
+ jar
+ 1.0-SNAPSHOT
+ monitor_native
+ https://www.github.com/manuelbl/java-does-usb
+
+
+ 25
+ 25
+ UTF-8
+ 0.11.0
+ 1.3.0
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.14.0
+
+ true
+
+
+
+ org.apache.maven.plugins
+ maven-jar-plugin
+ 3.4.2
+
+
+
+ net.codecrete.usb.examples.App
+ true
+
+
+
+
+
+
+
+
+
+ net.codecrete.usb
+ java-does-usb
+ ${java-does-usb.version}
+
+
+ junit
+ junit
+ 3.8.1
+ test
+
+
+
+
+
+ native
+
+
+
+ org.graalvm.buildtools
+ native-maven-plugin
+ ${native.maven.plugin.version}
+ true
+
+
+ build-native
+
+ compile-no-fork
+
+ package
+
+
+
+
+
+
+
+
+
diff --git a/examples/monitor_native/src/main/java/net/codecrete/usb/examples/App.java b/examples/monitor_native/src/main/java/net/codecrete/usb/examples/App.java
new file mode 100644
index 00000000..3bb1ccb9
--- /dev/null
+++ b/examples/monitor_native/src/main/java/net/codecrete/usb/examples/App.java
@@ -0,0 +1,38 @@
+//
+// Java Does USB
+// Copyright (c) 2022 Manuel Bleichenbacher
+// Licensed under MIT License
+// https://opensource.org/licenses/MIT
+//
+
+package net.codecrete.usb.examples;
+
+import net.codecrete.usb.Usb;
+import net.codecrete.usb.UsbDevice;
+
+import java.io.IOException;
+
+/**
+ * Sample application monitoring USB devices as they are connected and disconnected.
+ */
+public class App {
+
+ static void main() throws IOException {
+ // register callbacks for events
+ Usb.setOnDeviceConnected(device -> printDetails(device, "Connected"));
+ Usb.setOnDeviceDisconnected(device -> printDetails(device, "Disconnected"));
+
+ // display the already present USB devices
+ for (var device : Usb.getDevices())
+ printDetails(device, "Present");
+
+ // wait for ENTER to quit program
+ System.out.println("Monitoring... Press ENTER to quit.");
+ System.in.read();
+ }
+
+ private static void printDetails(UsbDevice device, String event) {
+ System.out.printf("%-14s", event + ":");
+ System.out.println(device.toString());
+ }
+}
\ No newline at end of file
diff --git a/examples/monitor_native/src/main/resources/META-INF/native-image/net.codecrete.usb.examples/monitor_native/native-image.properties b/examples/monitor_native/src/main/resources/META-INF/native-image/net.codecrete.usb.examples/monitor_native/native-image.properties
new file mode 100644
index 00000000..cf6e3f9a
--- /dev/null
+++ b/examples/monitor_native/src/main/resources/META-INF/native-image/net.codecrete.usb.examples/monitor_native/native-image.properties
@@ -0,0 +1 @@
+Args = --enable-native-access=ALL-UNNAMED -H:ConfigurationFileDirectories=config/windows
diff --git a/examples/stm_dfu/.mvn/wrapper/maven-wrapper.jar b/examples/stm_dfu/.mvn/wrapper/maven-wrapper.jar
new file mode 100644
index 00000000..cb28b0e3
Binary files /dev/null and b/examples/stm_dfu/.mvn/wrapper/maven-wrapper.jar differ
diff --git a/examples/stm_dfu/.mvn/wrapper/maven-wrapper.properties b/examples/stm_dfu/.mvn/wrapper/maven-wrapper.properties
new file mode 100644
index 00000000..f3283b08
--- /dev/null
+++ b/examples/stm_dfu/.mvn/wrapper/maven-wrapper.properties
@@ -0,0 +1,18 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.6.3/apache-maven-3.6.3-bin.zip
+wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar
diff --git a/examples/stm_dfu/README.md b/examples/stm_dfu/README.md
new file mode 100644
index 00000000..8c5285d0
--- /dev/null
+++ b/examples/stm_dfu/README.md
@@ -0,0 +1,73 @@
+# Device Firmware Upload (DFU) for STM32
+
+This sample programs implements firmware upload for STM32 microcontrollers with the built-in DFU mode.
+
+Even though the DFU
+
+## Prerequisites
+
+- Java 22
+- Apache Maven
+- 64-bit operating system (Windows, macOS, Linux)
+
+## How to run
+
+### Install Java 22 or higher
+
+Check that Java 22 or higher is installed:
+
+```shell
+$ java -version
+```
+
+If not, download and install it, e.g. from [Azul](https://www.azul.com/downloads/?package=jdk).
+
+### Install Maven
+
+Check that *Maven* is installed:
+
+```shell
+$ mvn -version
+```
+
+If it is not present, install it, typically using package manager like *Homebrew* on macOS, *Chocolately* on Windows and *apt* on Linux.
+
+### Build the application
+
+```shell
+$ mvn clean package
+```
+
+### Put the STM32 development board in DFU mode
+
+- Connect an STM32 development board (like a BlackPill board) to your computer while pressing the *Boot* button.
+- Ensure that it is DFU mode by checking macOS *System Information* or Windows *Device Manager*. The device should appear as "STM32 BOOTLOADER".
+
+On Windows, the *WinUSB* driver must be installed. See https://github.com/manuelbl/JavaDoesUSB/wiki/DFU-on-Windows for additional information.
+
+On many Linux distributions, the default permissions of USB devices do not allow access. To change it, create a file `/etc/udev/rules.d/50-stm-dfu.rules` with the below content:
+
+```text
+SUBSYSTEM=="usb", ATTRS{idVendor}=="0483", ATTRS{idProduct}=="df11", MODE="0666"
+```
+
+
+### Run the application
+
+Run the command below (adapting the file path depending on your specific board):
+
+```shell
+$ mvn package
+$ java --enable-native-access=ALL-UNNAMED -jar target/stm_dfu-1.3.0.jar ../../test-devices/loopback-stm32/bin/blackpill-f401cc.bin
+DFU device found with serial 35A737883336.
+Target memory segment: Internal Flash
+Erasing page at 0x8000000 (size 0x4000)
+Writing data at 0x8000000 (size 0x800)
+Writing data at 0x8000800 (size 0x800)
+Writing data at 0x8001000 (size 0x800)
+Writing data at 0x8001800 (size 0x800)
+Writing data at 0x8002000 (size 0x800)
+Writing data at 0x8002800 (size 0x1f4)
+Firmware successfully downloaded and verified
+DFU mode exited and firmware started
+```
diff --git a/examples/stm_dfu/mvnw b/examples/stm_dfu/mvnw
new file mode 100755
index 00000000..8d937f4c
--- /dev/null
+++ b/examples/stm_dfu/mvnw
@@ -0,0 +1,308 @@
+#!/bin/sh
+# ----------------------------------------------------------------------------
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+# ----------------------------------------------------------------------------
+
+# ----------------------------------------------------------------------------
+# Apache Maven Wrapper startup batch script, version 3.2.0
+#
+# Required ENV vars:
+# ------------------
+# JAVA_HOME - location of a JDK home dir
+#
+# Optional ENV vars
+# -----------------
+# MAVEN_OPTS - parameters passed to the Java VM when running Maven
+# e.g. to debug Maven itself, use
+# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
+# MAVEN_SKIP_RC - flag to disable loading of mavenrc files
+# ----------------------------------------------------------------------------
+
+if [ -z "$MAVEN_SKIP_RC" ] ; then
+
+ if [ -f /usr/local/etc/mavenrc ] ; then
+ . /usr/local/etc/mavenrc
+ fi
+
+ if [ -f /etc/mavenrc ] ; then
+ . /etc/mavenrc
+ fi
+
+ if [ -f "$HOME/.mavenrc" ] ; then
+ . "$HOME/.mavenrc"
+ fi
+
+fi
+
+# OS specific support. $var _must_ be set to either true or false.
+cygwin=false;
+darwin=false;
+mingw=false
+case "$(uname)" in
+ CYGWIN*) cygwin=true ;;
+ MINGW*) mingw=true;;
+ Darwin*) darwin=true
+ # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home
+ # See https://developer.apple.com/library/mac/qa/qa1170/_index.html
+ if [ -z "$JAVA_HOME" ]; then
+ if [ -x "/usr/libexec/java_home" ]; then
+ JAVA_HOME="$(/usr/libexec/java_home)"; export JAVA_HOME
+ else
+ JAVA_HOME="/Library/Java/Home"; export JAVA_HOME
+ fi
+ fi
+ ;;
+esac
+
+if [ -z "$JAVA_HOME" ] ; then
+ if [ -r /etc/gentoo-release ] ; then
+ JAVA_HOME=$(java-config --jre-home)
+ fi
+fi
+
+# For Cygwin, ensure paths are in UNIX format before anything is touched
+if $cygwin ; then
+ [ -n "$JAVA_HOME" ] &&
+ JAVA_HOME=$(cygpath --unix "$JAVA_HOME")
+ [ -n "$CLASSPATH" ] &&
+ CLASSPATH=$(cygpath --path --unix "$CLASSPATH")
+fi
+
+# For Mingw, ensure paths are in UNIX format before anything is touched
+if $mingw ; then
+ [ -n "$JAVA_HOME" ] && [ -d "$JAVA_HOME" ] &&
+ JAVA_HOME="$(cd "$JAVA_HOME" || (echo "cannot cd into $JAVA_HOME."; exit 1); pwd)"
+fi
+
+if [ -z "$JAVA_HOME" ]; then
+ javaExecutable="$(which javac)"
+ if [ -n "$javaExecutable" ] && ! [ "$(expr "\"$javaExecutable\"" : '\([^ ]*\)')" = "no" ]; then
+ # readlink(1) is not available as standard on Solaris 10.
+ readLink=$(which readlink)
+ if [ ! "$(expr "$readLink" : '\([^ ]*\)')" = "no" ]; then
+ if $darwin ; then
+ javaHome="$(dirname "\"$javaExecutable\"")"
+ javaExecutable="$(cd "\"$javaHome\"" && pwd -P)/javac"
+ else
+ javaExecutable="$(readlink -f "\"$javaExecutable\"")"
+ fi
+ javaHome="$(dirname "\"$javaExecutable\"")"
+ javaHome=$(expr "$javaHome" : '\(.*\)/bin')
+ JAVA_HOME="$javaHome"
+ export JAVA_HOME
+ fi
+ fi
+fi
+
+if [ -z "$JAVACMD" ] ; then
+ if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ else
+ JAVACMD="$(\unset -f command 2>/dev/null; \command -v java)"
+ fi
+fi
+
+if [ ! -x "$JAVACMD" ] ; then
+ echo "Error: JAVA_HOME is not defined correctly." >&2
+ echo " We cannot execute $JAVACMD" >&2
+ exit 1
+fi
+
+if [ -z "$JAVA_HOME" ] ; then
+ echo "Warning: JAVA_HOME environment variable is not set."
+fi
+
+# traverses directory structure from process work directory to filesystem root
+# first directory with .mvn subdirectory is considered project base directory
+find_maven_basedir() {
+ if [ -z "$1" ]
+ then
+ echo "Path not specified to find_maven_basedir"
+ return 1
+ fi
+
+ basedir="$1"
+ wdir="$1"
+ while [ "$wdir" != '/' ] ; do
+ if [ -d "$wdir"/.mvn ] ; then
+ basedir=$wdir
+ break
+ fi
+ # workaround for JBEAP-8937 (on Solaris 10/Sparc)
+ if [ -d "${wdir}" ]; then
+ wdir=$(cd "$wdir/.." || exit 1; pwd)
+ fi
+ # end of workaround
+ done
+ printf '%s' "$(cd "$basedir" || exit 1; pwd)"
+}
+
+# concatenates all lines of a file
+concat_lines() {
+ if [ -f "$1" ]; then
+ # Remove \r in case we run on Windows within Git Bash
+ # and check out the repository with auto CRLF management
+ # enabled. Otherwise, we may read lines that are delimited with
+ # \r\n and produce $'-Xarg\r' rather than -Xarg due to word
+ # splitting rules.
+ tr -s '\r\n' ' ' < "$1"
+ fi
+}
+
+log() {
+ if [ "$MVNW_VERBOSE" = true ]; then
+ printf '%s\n' "$1"
+ fi
+}
+
+BASE_DIR=$(find_maven_basedir "$(dirname "$0")")
+if [ -z "$BASE_DIR" ]; then
+ exit 1;
+fi
+
+MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}; export MAVEN_PROJECTBASEDIR
+log "$MAVEN_PROJECTBASEDIR"
+
+##########################################################################################
+# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
+# This allows using the maven wrapper in projects that prohibit checking in binary data.
+##########################################################################################
+wrapperJarPath="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar"
+if [ -r "$wrapperJarPath" ]; then
+ log "Found $wrapperJarPath"
+else
+ log "Couldn't find $wrapperJarPath, downloading it ..."
+
+ if [ -n "$MVNW_REPOURL" ]; then
+ wrapperUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar"
+ else
+ wrapperUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar"
+ fi
+ while IFS="=" read -r key value; do
+ # Remove '\r' from value to allow usage on windows as IFS does not consider '\r' as a separator ( considers space, tab, new line ('\n'), and custom '=' )
+ safeValue=$(echo "$value" | tr -d '\r')
+ case "$key" in (wrapperUrl) wrapperUrl="$safeValue"; break ;;
+ esac
+ done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties"
+ log "Downloading from: $wrapperUrl"
+
+ if $cygwin; then
+ wrapperJarPath=$(cygpath --path --windows "$wrapperJarPath")
+ fi
+
+ if command -v wget > /dev/null; then
+ log "Found wget ... using wget"
+ [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--quiet"
+ if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
+ wget $QUIET "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath"
+ else
+ wget $QUIET --http-user="$MVNW_USERNAME" --http-password="$MVNW_PASSWORD" "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath"
+ fi
+ elif command -v curl > /dev/null; then
+ log "Found curl ... using curl"
+ [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--silent"
+ if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
+ curl $QUIET -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath"
+ else
+ curl $QUIET --user "$MVNW_USERNAME:$MVNW_PASSWORD" -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath"
+ fi
+ else
+ log "Falling back to using Java to download"
+ javaSource="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.java"
+ javaClass="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.class"
+ # For Cygwin, switch paths to Windows format before running javac
+ if $cygwin; then
+ javaSource=$(cygpath --path --windows "$javaSource")
+ javaClass=$(cygpath --path --windows "$javaClass")
+ fi
+ if [ -e "$javaSource" ]; then
+ if [ ! -e "$javaClass" ]; then
+ log " - Compiling MavenWrapperDownloader.java ..."
+ ("$JAVA_HOME/bin/javac" "$javaSource")
+ fi
+ if [ -e "$javaClass" ]; then
+ log " - Running MavenWrapperDownloader.java ..."
+ ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$wrapperUrl" "$wrapperJarPath") || rm -f "$wrapperJarPath"
+ fi
+ fi
+ fi
+fi
+##########################################################################################
+# End of extension
+##########################################################################################
+
+# If specified, validate the SHA-256 sum of the Maven wrapper jar file
+wrapperSha256Sum=""
+while IFS="=" read -r key value; do
+ case "$key" in (wrapperSha256Sum) wrapperSha256Sum=$value; break ;;
+ esac
+done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties"
+if [ -n "$wrapperSha256Sum" ]; then
+ wrapperSha256Result=false
+ if command -v sha256sum > /dev/null; then
+ if echo "$wrapperSha256Sum $wrapperJarPath" | sha256sum -c > /dev/null 2>&1; then
+ wrapperSha256Result=true
+ fi
+ elif command -v shasum > /dev/null; then
+ if echo "$wrapperSha256Sum $wrapperJarPath" | shasum -a 256 -c > /dev/null 2>&1; then
+ wrapperSha256Result=true
+ fi
+ else
+ echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available."
+ echo "Please install either command, or disable validation by removing 'wrapperSha256Sum' from your maven-wrapper.properties."
+ exit 1
+ fi
+ if [ $wrapperSha256Result = false ]; then
+ echo "Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised." >&2
+ echo "Investigate or delete $wrapperJarPath to attempt a clean download." >&2
+ echo "If you updated your Maven version, you need to update the specified wrapperSha256Sum property." >&2
+ exit 1
+ fi
+fi
+
+MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS"
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin; then
+ [ -n "$JAVA_HOME" ] &&
+ JAVA_HOME=$(cygpath --path --windows "$JAVA_HOME")
+ [ -n "$CLASSPATH" ] &&
+ CLASSPATH=$(cygpath --path --windows "$CLASSPATH")
+ [ -n "$MAVEN_PROJECTBASEDIR" ] &&
+ MAVEN_PROJECTBASEDIR=$(cygpath --path --windows "$MAVEN_PROJECTBASEDIR")
+fi
+
+# Provide a "standardized" way to retrieve the CLI args that will
+# work with both Windows and non-Windows executions.
+MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $*"
+export MAVEN_CMD_LINE_ARGS
+
+WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
+
+# shellcheck disable=SC2086 # safe args
+exec "$JAVACMD" \
+ $MAVEN_OPTS \
+ $MAVEN_DEBUG_OPTS \
+ -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \
+ "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \
+ ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@"
diff --git a/examples/stm_dfu/mvnw.cmd b/examples/stm_dfu/mvnw.cmd
new file mode 100644
index 00000000..f80fbad3
--- /dev/null
+++ b/examples/stm_dfu/mvnw.cmd
@@ -0,0 +1,205 @@
+@REM ----------------------------------------------------------------------------
+@REM Licensed to the Apache Software Foundation (ASF) under one
+@REM or more contributor license agreements. See the NOTICE file
+@REM distributed with this work for additional information
+@REM regarding copyright ownership. The ASF licenses this file
+@REM to you under the Apache License, Version 2.0 (the
+@REM "License"); you may not use this file except in compliance
+@REM with the License. You may obtain a copy of the License at
+@REM
+@REM http://www.apache.org/licenses/LICENSE-2.0
+@REM
+@REM Unless required by applicable law or agreed to in writing,
+@REM software distributed under the License is distributed on an
+@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+@REM KIND, either express or implied. See the License for the
+@REM specific language governing permissions and limitations
+@REM under the License.
+@REM ----------------------------------------------------------------------------
+
+@REM ----------------------------------------------------------------------------
+@REM Apache Maven Wrapper startup batch script, version 3.2.0
+@REM
+@REM Required ENV vars:
+@REM JAVA_HOME - location of a JDK home dir
+@REM
+@REM Optional ENV vars
+@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands
+@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending
+@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven
+@REM e.g. to debug Maven itself, use
+@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
+@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files
+@REM ----------------------------------------------------------------------------
+
+@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on'
+@echo off
+@REM set title of command window
+title %0
+@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on'
+@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO%
+
+@REM set %HOME% to equivalent of $HOME
+if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%")
+
+@REM Execute a user defined script before this one
+if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre
+@REM check for pre script, once with legacy .bat ending and once with .cmd ending
+if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %*
+if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %*
+:skipRcPre
+
+@setlocal
+
+set ERROR_CODE=0
+
+@REM To isolate internal variables from possible post scripts, we use another setlocal
+@setlocal
+
+@REM ==== START VALIDATION ====
+if not "%JAVA_HOME%" == "" goto OkJHome
+
+echo.
+echo Error: JAVA_HOME not found in your environment. >&2
+echo Please set the JAVA_HOME variable in your environment to match the >&2
+echo location of your Java installation. >&2
+echo.
+goto error
+
+:OkJHome
+if exist "%JAVA_HOME%\bin\java.exe" goto init
+
+echo.
+echo Error: JAVA_HOME is set to an invalid directory. >&2
+echo JAVA_HOME = "%JAVA_HOME%" >&2
+echo Please set the JAVA_HOME variable in your environment to match the >&2
+echo location of your Java installation. >&2
+echo.
+goto error
+
+@REM ==== END VALIDATION ====
+
+:init
+
+@REM Find the project base dir, i.e. the directory that contains the folder ".mvn".
+@REM Fallback to current working directory if not found.
+
+set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR%
+IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir
+
+set EXEC_DIR=%CD%
+set WDIR=%EXEC_DIR%
+:findBaseDir
+IF EXIST "%WDIR%"\.mvn goto baseDirFound
+cd ..
+IF "%WDIR%"=="%CD%" goto baseDirNotFound
+set WDIR=%CD%
+goto findBaseDir
+
+:baseDirFound
+set MAVEN_PROJECTBASEDIR=%WDIR%
+cd "%EXEC_DIR%"
+goto endDetectBaseDir
+
+:baseDirNotFound
+set MAVEN_PROJECTBASEDIR=%EXEC_DIR%
+cd "%EXEC_DIR%"
+
+:endDetectBaseDir
+
+IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig
+
+@setlocal EnableExtensions EnableDelayedExpansion
+for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a
+@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS%
+
+:endReadAdditionalConfig
+
+SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe"
+set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar"
+set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
+
+set WRAPPER_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar"
+
+FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO (
+ IF "%%A"=="wrapperUrl" SET WRAPPER_URL=%%B
+)
+
+@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
+@REM This allows using the maven wrapper in projects that prohibit checking in binary data.
+if exist %WRAPPER_JAR% (
+ if "%MVNW_VERBOSE%" == "true" (
+ echo Found %WRAPPER_JAR%
+ )
+) else (
+ if not "%MVNW_REPOURL%" == "" (
+ SET WRAPPER_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar"
+ )
+ if "%MVNW_VERBOSE%" == "true" (
+ echo Couldn't find %WRAPPER_JAR%, downloading it ...
+ echo Downloading from: %WRAPPER_URL%
+ )
+
+ powershell -Command "&{"^
+ "$webclient = new-object System.Net.WebClient;"^
+ "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^
+ "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^
+ "}"^
+ "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%WRAPPER_URL%', '%WRAPPER_JAR%')"^
+ "}"
+ if "%MVNW_VERBOSE%" == "true" (
+ echo Finished downloading %WRAPPER_JAR%
+ )
+)
+@REM End of extension
+
+@REM If specified, validate the SHA-256 sum of the Maven wrapper jar file
+SET WRAPPER_SHA_256_SUM=""
+FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO (
+ IF "%%A"=="wrapperSha256Sum" SET WRAPPER_SHA_256_SUM=%%B
+)
+IF NOT %WRAPPER_SHA_256_SUM%=="" (
+ powershell -Command "&{"^
+ "$hash = (Get-FileHash \"%WRAPPER_JAR%\" -Algorithm SHA256).Hash.ToLower();"^
+ "If('%WRAPPER_SHA_256_SUM%' -ne $hash){"^
+ " Write-Output 'Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised.';"^
+ " Write-Output 'Investigate or delete %WRAPPER_JAR% to attempt a clean download.';"^
+ " Write-Output 'If you updated your Maven version, you need to update the specified wrapperSha256Sum property.';"^
+ " exit 1;"^
+ "}"^
+ "}"
+ if ERRORLEVEL 1 goto error
+)
+
+@REM Provide a "standardized" way to retrieve the CLI args that will
+@REM work with both Windows and non-Windows executions.
+set MAVEN_CMD_LINE_ARGS=%*
+
+%MAVEN_JAVA_EXE% ^
+ %JVM_CONFIG_MAVEN_PROPS% ^
+ %MAVEN_OPTS% ^
+ %MAVEN_DEBUG_OPTS% ^
+ -classpath %WRAPPER_JAR% ^
+ "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^
+ %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %*
+if ERRORLEVEL 1 goto error
+goto end
+
+:error
+set ERROR_CODE=1
+
+:end
+@endlocal & set ERROR_CODE=%ERROR_CODE%
+
+if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost
+@REM check for post script, once with legacy .bat ending and once with .cmd ending
+if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat"
+if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd"
+:skipRcPost
+
+@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on'
+if "%MAVEN_BATCH_PAUSE%"=="on" pause
+
+if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE%
+
+cmd /C exit /B %ERROR_CODE%
diff --git a/examples/stm_dfu/pom.xml b/examples/stm_dfu/pom.xml
new file mode 100644
index 00000000..0e3aa46d
--- /dev/null
+++ b/examples/stm_dfu/pom.xml
@@ -0,0 +1,90 @@
+
+
+ 4.0.0
+
+ net.codecrete.usb.examples
+ stm_dfu
+ 1.3.0
+
+ stm_dfu
+ https://github.com/manuelbl/JavaDoesUSB/examples/stm_dfu
+
+
+ UTF-8
+ 22
+ 22
+ 1.3.0
+
+
+
+
+ net.codecrete.usb
+ java-does-usb
+ ${java-does-usb.version}
+
+
+
+
+
+
+
+ maven-clean-plugin
+ 3.3.2
+
+
+
+ maven-resources-plugin
+ 3.3.1
+
+
+ maven-compiler-plugin
+ 3.12.1
+
+ 22
+ 22
+ 22
+
+
+
+ maven-jar-plugin
+ 3.3.0
+
+
+ maven-surefire-plugin
+ 3.2.5
+
+ --enable-native-access=ALL-UNNAMED
+
+
+
+ maven-shade-plugin
+ 3.5.1
+
+
+ package
+
+ shade
+
+
+
+
+
+
+ *:*
+
+ module-info.class
+ META-INF/MANIFEST.MF
+
+
+
+
+
+ net.codecrete.usb.dfu.DFU
+
+
+ false
+
+
+
+
+
diff --git a/examples/stm_dfu/src/main/java/net/codecrete/usb/dfu/DFU.java b/examples/stm_dfu/src/main/java/net/codecrete/usb/dfu/DFU.java
new file mode 100644
index 00000000..4ca4d2a3
--- /dev/null
+++ b/examples/stm_dfu/src/main/java/net/codecrete/usb/dfu/DFU.java
@@ -0,0 +1,77 @@
+//
+// Java Does USB
+// Copyright (c) 2022 Manuel Bleichenbacher
+// Licensed under MIT License
+// https://opensource.org/licenses/MIT
+//
+
+package net.codecrete.usb.dfu;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+/**
+ * Command line application for uploading firmware to STM32 microcontrollers.
+ *
+ * Only the STM32 variant of the DFU protocol is supported.
+ * Only binary firmware format is supported (no .hex or .dfu files).
+ *
+ */
+public class DFU {
+
+ /**
+ * Main function
+ * @param args arguments (path to firmware)
+ */
+ public static void main(String[] args) {
+ // check for single parameter
+ if (args.length != 1) {
+ System.err.println("Usage: dfu_upload ");
+ System.exit(1);
+ return;
+ }
+
+ // read firmware file
+ byte[] firmware;
+ try {
+ firmware = Files.readAllBytes(Path.of(args[0]));
+ } catch (IOException e) {
+ System.err.printf("Error: Cannot read firmware file %s%n", args[0]);
+ System.exit(2);
+ return;
+ }
+
+ // check for single DFU device
+ var devices = DFUDevice.getAll();
+ if (devices.isEmpty()) {
+ System.err.println("Error: No STM32 DFU device connected (or not in DFU mode)");
+ System.exit(4);
+ return;
+ } else if (devices.size() > 1) {
+ System.err.println("Error: Multiple STM32 DFU devices connected. Please connect only one.");
+ System.exit(4);
+ return;
+ }
+ var device = devices.getFirst();
+ System.out.printf("DFU device found with serial %s.%n", device.getSerialNumber());
+
+ // download and verify firmware
+ try {
+ device.open();
+ device.download(firmware);
+ device.verify(firmware);
+ System.out.println("Firmware successfully downloaded and verified");
+
+ device.startApplication();
+ device.waitForDisconnect();
+ System.out.println("DFU mode ended and firmware started");
+
+ device.close();
+
+ } catch (DFUException e) {
+ System.err.println("Error: " + e.getMessage());
+ System.exit(3);
+ }
+ }
+}
diff --git a/examples/stm_dfu/src/main/java/net/codecrete/usb/dfu/DFUDevice.java b/examples/stm_dfu/src/main/java/net/codecrete/usb/dfu/DFUDevice.java
new file mode 100644
index 00000000..4678e066
--- /dev/null
+++ b/examples/stm_dfu/src/main/java/net/codecrete/usb/dfu/DFUDevice.java
@@ -0,0 +1,396 @@
+//
+// Java Does USB
+// Copyright (c) 2022 Manuel Bleichenbacher
+// Licensed under MIT License
+// https://opensource.org/licenses/MIT
+//
+
+package net.codecrete.usb.dfu;
+
+import net.codecrete.usb.*;
+
+import java.util.Arrays;
+import java.util.List;
+
+import static net.codecrete.usb.UsbRecipient.INTERFACE;
+import static net.codecrete.usb.UsbRequestType.CLASS;
+
+/**
+ * DFU device.
+ *
+ * Implements the main DFU operations like download, upload etc.
+ *
+ */
+public class DFUDevice {
+
+ private final UsbDevice usbDevice;
+ private final int interfaceNumber;
+ private final int transferSize;
+ private final Version dfuVersion;
+
+ private List segments;
+
+ /**
+ * Gets all connected DFU devices
+ * @return List of DFU devices
+ */
+ public static List getAll() {
+ return Usb.findDevices(DFUDevice::hasDFUDescriptor)
+ .stream()
+ .map(DFUDevice::new)
+ .toList();
+ }
+
+ /**
+ * Checks if the device has a DFU functional descriptor.
+ * @param device USB device
+ * @return {@code true} if it has a DFU descriptor, {@code false} otherwise
+ */
+ public static boolean hasDFUDescriptor(UsbDevice device) {
+ return getDFUDescriptorOffset(device.getConfigurationDescriptor()) > 0
+ && getDFUInterfaceNumber(device) >= 0;
+ }
+
+ /**
+ * Gets the offset of the DFU functional descriptor within the USB configuration descriptor.
+ * @param descriptor USB configuration descriptor
+ * @return descriptor offset (in bytes), or -1 if not found
+ */
+ public static int getDFUDescriptorOffset(byte[] descriptor) {
+ int offset = 0;
+ while (offset < descriptor.length) {
+ if (descriptor[offset + 1] == 0x21)
+ return offset;
+ offset += descriptor[offset] & 255;
+ }
+ return -1;
+ }
+
+ /**
+ * Gets the DFU interface number.
+ * @param device the USB device
+ * @return the interface number, of -1 if not found
+ */
+ public static int getDFUInterfaceNumber(UsbDevice device) {
+ for (var intf : device.getInterfaces()) {
+ var alt = intf.getCurrentAlternate();
+ if (alt.getClassCode() == 0xFE && alt.getSubclassCode() == 0x01 && alt.getProtocolCode() == 0x02)
+ return intf.getNumber();
+ }
+
+ return -1;
+ }
+
+ /**
+ * Creates a new DFUDevice instance.
+ *
+ * The specified USB device must have a DFU descriptor and a DFU interface.
+ *
+ * @param usbDevice the USB device
+ */
+ public DFUDevice(UsbDevice usbDevice) {
+ this.usbDevice = usbDevice;
+ interfaceNumber = getDFUInterfaceNumber(usbDevice);
+
+ var configDesc = usbDevice.getConfigurationDescriptor();
+ int offset = getDFUDescriptorOffset(configDesc);
+ assert offset > 0;
+
+ transferSize = getInt16(configDesc, offset + 5);
+ dfuVersion = new Version(getInt16(configDesc, offset + 7));
+ }
+
+ /**
+ * Gets the DFU protocol version.
+ * @return the protocol version
+ */
+ public Version getDfuVersion() {
+ return dfuVersion;
+ }
+
+ /**
+ * Gets the device serial number.
+ * @return the serial number
+ */
+ public String getSerialNumber() {
+ return usbDevice.getSerialNumber();
+ }
+
+ /**
+ * Opens the DFU device for communication.
+ */
+ public void open() {
+ usbDevice.open();
+ usbDevice.claimInterface(interfaceNumber);
+ segments = Segment.getSegments(usbDevice, interfaceNumber);
+ clearErrorIfNeeded();
+ }
+
+ /**
+ * Closes the DFU device.
+ */
+ public void close() {
+ usbDevice.close();
+ }
+
+ /**
+ * Waits until the device disconnects.
+ *
+ * Disconnection is a side effect of leaving DFU mode.
+ *
+ *
+ * If the device does not disconnect after 5 seconds,
+ * an exception will be thrown.
+ *
+ */
+ public void waitForDisconnect() {
+ var waitingTime = 5000;
+ while (waitingTime > 0 && usbDevice.isConnected()) {
+ sleep(100);
+ waitingTime -= 100;
+ }
+
+ if (usbDevice.isConnected())
+ throw new DFUException("Device did not restart (try disconnecting and reconnecting it)");
+ }
+
+ /**
+ * Clears an error status.
+ */
+ public void clearStatus() {
+ var transfer = createDfuControlTransfer(DFURequest.CLEAR_STATUS, 0);
+ usbDevice.controlTransferOut(transfer, null);
+ }
+
+ /**
+ * Aborts the download mode.
+ */
+ public void abort() {
+ var transfer = createDfuControlTransfer(DFURequest.ABORT, 0);
+ usbDevice.controlTransferOut(transfer, null);
+ }
+
+ /**
+ * Gets the full DFU status
+ * @return the status
+ */
+ public DFUStatus getStatus() {
+ var transfer = createDfuControlTransfer(DFURequest.GET_STATUS, 0);
+ var response = usbDevice.controlTransferIn(transfer, 6);
+ if (response.length != 6)
+ throw new DFUException("Invalid response from GET_STATUS request");
+ return DFUStatus.fromBytes(response);
+ }
+
+ /**
+ * Reads from the device flash memory.
+ * @param address the memory start address
+ * @param length the length of memory to read
+ * @return the read data
+ */
+ public byte[] read(int address, int length) {
+ expectState(DeviceState.DFU_IDLE, DeviceState.DFU_DNLOAD_IDLE);
+ setAddress(address);
+ exitMode();
+ expectState(DeviceState.DFU_IDLE, DeviceState.DFU_UPLOAD_IDLE);
+
+ var result = new byte[length];
+
+ // read full chunks
+ int offset = 0;
+ int blockNum = 2;
+ while (offset < length) {
+ int chunkSize = Math.min(transferSize, length - offset);
+ var transfer = createDfuControlTransfer(DFURequest.UPLOAD, blockNum);
+ var chunk = usbDevice.controlTransferIn(transfer, chunkSize);
+ System.arraycopy(chunk, 0, result, offset, chunkSize);
+ offset += chunkSize;
+ blockNum += 1;
+ }
+
+ exitMode();
+
+ return result;
+ }
+
+ public void verify(byte[] firmware) {
+ byte[] firmware2 = read(STM32.FLASH_BASE_ADDRESS, firmware.length);
+ if (!Arrays.equals(firmware, firmware2))
+ throw new DFUException("Verification failed - content differs");
+ }
+
+ public void download(byte[] firmware) {
+ int length = firmware.length;
+
+ // validate that start and end address exist and are writable
+ int startAddress = STM32.FLASH_BASE_ADDRESS;
+ var firstPage = getWritablePage(startAddress);
+ getWritablePage(startAddress + length);
+
+ usbDevice.selectAlternateSetting(interfaceNumber, firstPage.segment().getAltSetting());
+ System.out.printf("Target memory segment: %s%n", firstPage.segment().getName());
+
+ // erase if needed
+ if (firstPage.isErasable())
+ erase(startAddress, length);
+
+ // download firmware
+ setAddress(startAddress);
+
+ int offset = 0;
+ int transaction = 2;
+ while (offset < length) {
+ int chunkSize = Math.min(length - offset, transferSize);
+
+ byte[] chunk = new byte[chunkSize];
+ System.arraycopy(firmware, offset, chunk, 0, chunkSize);
+
+ System.out.printf("Writing data at 0x%x (size 0x%x)%n", startAddress + offset, chunkSize);
+ var transfer = createDfuControlTransfer(DFURequest.DOWNLOAD, transaction);
+ usbDevice.controlTransferOut(transfer, chunk);
+
+ finishDownloadCommand("writing data");
+
+ offset += chunkSize;
+ transaction += 1;
+ }
+
+ exitMode();
+ }
+
+ /**
+ * Erases the specified range.
+ *
+ * Only applicable to erasable sector, i.e. flash memory.
+ *
+ *
+ * Only entire pages can be erased. If start and end address to not fall onto
+ * page boundaries, this method will extend the range to be erased.
+ *
+ * @param startAddress the start address of the range
+ * @param length the length of the range
+ */
+ public void erase(int startAddress, int length) {
+ int endAddress = startAddress + length;
+
+ while (startAddress < endAddress) {
+ var page = findPage(startAddress);
+ if (page == null)
+ throw new DFUException(String.format("No valid memory segment at address 0x%x", startAddress));
+ if (!page.isErasable())
+ throw new DFUException(String.format("Page at address 0x%x is not erasable", startAddress));
+
+ System.out.printf("Erasing page at 0x%x (size 0x%x)%n", page.startAddress(), page.pageSize());
+ erasePage(page.startAddress());
+ startAddress = page.getEndAddress();
+ }
+ }
+
+ public void erasePage(int address) {
+ executeSpecialCommand((byte) 0x41, "erasing page", address);
+ }
+
+ public void setAddress(int address) {
+ executeSpecialCommand((byte) 0x21, "setting address", address);
+ }
+
+ private void executeSpecialCommand(byte command, String action, int address) {
+ var transfer = createDfuControlTransfer(DFURequest.DOWNLOAD, 0);
+ var data = new byte[] {
+ command,
+ (byte) address,
+ (byte) (address >> 8),
+ (byte) (address >> 16),
+ (byte) (address >> 24)
+ };
+ usbDevice.controlTransferOut(transfer, data);
+
+ finishDownloadCommand(action);
+ }
+
+ private void finishDownloadCommand(String action) {
+ var status = getStatus();
+ if (status.state() != DeviceState.DFU_DNBUSY)
+ throw new DFUException("Unexpected state for " + action);
+
+ sleep(status.pollTimeout());
+
+ status = getStatus();
+ if (status.status() != DeviceStatus.OK)
+ throw new DFUException("Unexpected state after " + action);
+
+ sleep(status.pollTimeout());
+ }
+
+ private Page getWritablePage(int address) {
+ var page = findPage(address);
+ if (page == null)
+ throw new DFUException(String.format("No valid memory segment at address 0x%x", address));
+ if (!page.isWritable())
+ throw new DFUException(String.format("Page at address 0x%x is not writable", address));
+ return page;
+ }
+
+ private Page findPage(int address) {
+ return Segment.findPage(segments, address);
+ }
+
+ private void exitMode() {
+ abort();
+
+ var status = getStatus();
+ if (status.state() != DeviceState.DFU_IDLE)
+ throw new DFUException("Unexpected state after exiting from download mode");
+
+ sleep(status.pollTimeout());
+ }
+
+ public void startApplication() {
+ expectState(DeviceState.DFU_IDLE, DeviceState.DFU_DNLOAD_IDLE);
+
+ // By sending a zero-length download packet and querying the status,
+ // the device will leave DFU mode and restart.
+ var transfer = createDfuControlTransfer(DFURequest.DOWNLOAD, 0);
+ usbDevice.controlTransferOut(transfer, null);
+
+ var status = getStatus();
+ if (status.state() != DeviceState.DFU_MANIFEST)
+ throw new DFUException("Exiting DFU mode and starting firmware has failed");
+ }
+
+ private void clearErrorIfNeeded() {
+ var status = getStatus();
+ if (status.status() != DeviceStatus.OK) {
+ clearStatus();
+ sleep(status.pollTimeout());
+ status = getStatus();
+ if (status.status() != DeviceStatus.OK)
+ throw new DFUException("Cannot clear error status");
+ }
+ }
+
+ private void expectState(DeviceState state1, DeviceState state2) {
+ var status = getStatus();
+ if (status.state() != state1 && status.state() != state2)
+ throw new DFUException(
+ String.format("Expected state %s or %s but got %s", state1, state2, status.state()));
+ }
+
+ private int getInt16(byte[] config, int offset) {
+ return (config[offset] & 0xff) + 256 * (config[offset + 1] & 0xff);
+ }
+
+ private static void sleep(int millis) {
+ try {
+ Thread.sleep(millis);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ throw new DFUException("Sleep failed", e);
+ }
+ }
+
+ private UsbControlTransfer createDfuControlTransfer(DFURequest request, int value) {
+ return new UsbControlTransfer(CLASS, INTERFACE, request.ordinal(), value, interfaceNumber);
+ }
+
+}
diff --git a/examples/stm_dfu/src/main/java/net/codecrete/usb/dfu/DFUException.java b/examples/stm_dfu/src/main/java/net/codecrete/usb/dfu/DFUException.java
new file mode 100644
index 00000000..eaa4a111
--- /dev/null
+++ b/examples/stm_dfu/src/main/java/net/codecrete/usb/dfu/DFUException.java
@@ -0,0 +1,31 @@
+//
+// Java Does USB
+// Copyright (c) 2022 Manuel Bleichenbacher
+// Licensed under MIT License
+// https://opensource.org/licenses/MIT
+//
+
+package net.codecrete.usb.dfu;
+
+/**
+ * DFU exception thrown if a DFU operation fails.
+ */
+public class DFUException extends RuntimeException {
+
+ /**
+ * Creates a new instance with the given message
+ * @param message the message
+ */
+ DFUException(String message) {
+ super(message);
+ }
+
+ /**
+ * Creates a new instance with the given message and cause
+ * @param message the message
+ * @param cause the exception causing the DFU operation to fail
+ */
+ DFUException(String message, Throwable cause) {
+ super(message, cause);
+ }
+}
diff --git a/examples/stm_dfu/src/main/java/net/codecrete/usb/dfu/DFURequest.java b/examples/stm_dfu/src/main/java/net/codecrete/usb/dfu/DFURequest.java
new file mode 100644
index 00000000..c927e105
--- /dev/null
+++ b/examples/stm_dfu/src/main/java/net/codecrete/usb/dfu/DFURequest.java
@@ -0,0 +1,56 @@
+//
+// Java Does USB
+// Copyright (c) 2022 Manuel Bleichenbacher
+// Licensed under MIT License
+// https://opensource.org/licenses/MIT
+//
+
+package net.codecrete.usb.dfu;
+
+/**
+ * DFU request.
+ *
+ * See ST Microelectronics, application note AN3156.
+ *
+ */
+public enum DFURequest {
+ /**
+ * Requests the device to leave DFU mode and enter the application.
+ *
+ * The Detach request is not meaningful in the case of the bootloader. The bootloader starts
+ * with a system reset depending on the boot mode configuration settings, which means that
+ * no other application is running at that time.
+ *
+ */
+ DETACH,
+ /**
+ * Requests data transfer from Host to the device in order to load them
+ * into device internal flash memory. Includes also erase commands.
+ */
+ DOWNLOAD,
+ /**
+ * Requests data transfer from device to Host in order to load content
+ * of device internal flash memory into a Host file.
+ */
+ UPLOAD,
+ /**
+ * Requests device to send status report to the Host (including status
+ * resulting from the last request execution and the state the device
+ * enters immediately after this request).
+ */
+ GET_STATUS,
+ /**
+ * Requests device to clear error status and move to next step.
+ */
+ CLEAR_STATUS,
+ /**
+ * Requests the device to send only the state it enters immediately
+ * after this request.
+ */
+ GET_STATE,
+ /**
+ * Requests device to exit the current state/operation and enter idle
+ * state immediately
+ */
+ ABORT
+}
diff --git a/examples/stm_dfu/src/main/java/net/codecrete/usb/dfu/DFUStatus.java b/examples/stm_dfu/src/main/java/net/codecrete/usb/dfu/DFUStatus.java
new file mode 100644
index 00000000..d6b44b00
--- /dev/null
+++ b/examples/stm_dfu/src/main/java/net/codecrete/usb/dfu/DFUStatus.java
@@ -0,0 +1,24 @@
+//
+// Java Does USB
+// Copyright (c) 2022 Manuel Bleichenbacher
+// Licensed under MIT License
+// https://opensource.org/licenses/MIT
+//
+
+package net.codecrete.usb.dfu;
+
+/**
+ * DFU GET_STATUS response.
+ *
+ * See USB Device Class Specification for Device Firmware Upgrade, version 1.1.
+ *
+ */
+public record DFUStatus(DeviceStatus status, int pollTimeout, DeviceState state) {
+
+ public static DFUStatus fromBytes(byte[] data) {
+ var status = DeviceStatus.fromValue(data[0]);
+ var pollTimeout = (data[1] & 0xff) + 256 * (data[2] & 0xff) + 256 * 256 * (data[3] & 0xff);
+ var state = DeviceState.fromValue(data[4]);
+ return new DFUStatus(status, pollTimeout, state);
+ }
+}
diff --git a/examples/stm_dfu/src/main/java/net/codecrete/usb/dfu/DeviceState.java b/examples/stm_dfu/src/main/java/net/codecrete/usb/dfu/DeviceState.java
new file mode 100644
index 00000000..35532fd9
--- /dev/null
+++ b/examples/stm_dfu/src/main/java/net/codecrete/usb/dfu/DeviceState.java
@@ -0,0 +1,71 @@
+//
+// Java Does USB
+// Copyright (c) 2022 Manuel Bleichenbacher
+// Licensed under MIT License
+// https://opensource.org/licenses/MIT
+//
+
+package net.codecrete.usb.dfu;
+
+/**
+ * DFU device state.
+ *
+ * See USB Device Class Specification for Device Firmware Upgrade, version 1.1.
+ *
+ */
+public enum DeviceState {
+ /**
+ * Device is running its normal application.
+ */
+ APP_IDLE,
+ /**
+ * Device is running its normal application, has received the DFU_DETACH request,
+ * and is waiting for a USB reset.
+ */
+ APP_DETACH,
+ /**
+ * Device is operating in the DFU mode and is waiting for requests.
+ */
+ DFU_IDLE,
+ /**
+ * Device has received a block and is waiting for the host to solicit the status via DFU_GETSTATUS.
+ */
+ DFU_DNLOAD_SYNC,
+ /**
+ * Device is programming a control-write block into its nonvolatile memories.
+ */
+ DFU_DNBUSY,
+ /**
+ * Device is processing a download operation. Expecting DFU_DNLOAD requests.
+ */
+ DFU_DNLOAD_IDLE,
+ /**
+ * Device has received the final block of firmware from the host and is waiting for receipt of
+ * DFU_GETSTATUS to begin the Manifestation phase; or device has completed the Manifestation phase and is
+ * waiting for receipt of DFU_GETSTATUS. (Devices that can enter this state after the Manifestation phase
+ * set bmAttributes bit bitManifestationTolerant to 1.)
+ */
+ DFU_MANIFEST_SYNC,
+ /**
+ * Device is in the Manifestation phase. (Not all devices will be able to respond to DFU_GETSTATUS
+ * when in this state.
+ */
+ DFU_MANIFEST,
+ /**
+ * Device has programmed its memories and is waiting for a USB reset or a power on reset.
+ * (Devices that must enter this state clear bitManifestationTolerant to 0.)
+ */
+ DFU_MANIFEST_WAIT_RESET,
+ /**
+ * The device is processing an upload operation. Expecting DFU_UPLOAD requests.
+ */
+ DFU_UPLOAD_IDLE,
+ /**
+ * An error has occurred. Awaiting the DFU_CLRSTATUS request.
+ */
+ DFU_ERROR;
+
+ public static DeviceState fromValue(byte value) {
+ return values()[value];
+ }
+}
diff --git a/examples/stm_dfu/src/main/java/net/codecrete/usb/dfu/DeviceStatus.java b/examples/stm_dfu/src/main/java/net/codecrete/usb/dfu/DeviceStatus.java
new file mode 100644
index 00000000..c702112b
--- /dev/null
+++ b/examples/stm_dfu/src/main/java/net/codecrete/usb/dfu/DeviceStatus.java
@@ -0,0 +1,86 @@
+//
+// Java Does USB
+// Copyright (c) 2022 Manuel Bleichenbacher
+// Licensed under MIT License
+// https://opensource.org/licenses/MIT
+//
+
+package net.codecrete.usb.dfu;
+
+/**
+ * DFU device status.
+ *
+ * See USB Device Class Specification for Device Firmware Upgrade, version 1.1.
+ *
+ */
+public enum DeviceStatus {
+
+ /**
+ * No error condition is present
+ */
+ OK,
+ /**
+ * File is not targeted for use by this device.
+ */
+ ERR_TARGET,
+ /**
+ * File is for this device but fails some vendor-specific verification test.
+ */
+ ERR_FILE,
+ /**
+ * Device is unable to write memory.
+ */
+ ERR_WRITE,
+ /**
+ * Memory erase function failed.
+ */
+ ERR_ERASE,
+ /**
+ * Memory erase check failed.
+ */
+ ERR_CHECK_ERASED,
+ /**
+ * Program memory function failed.
+ */
+ ERR_PROG,
+ /**
+ * Programmed memory failed verification.
+ */
+ ERR_VERIFY,
+ /**
+ * Cannot program memory due to received address that is out of range.
+ */
+ ERR_ADDRESS,
+ /**
+ * Received DFU_DNLOAD with wLength = 0, but device does not think it has all of the data yet.
+ */
+ ERR_NOTDONE,
+ /**
+ * Device’s firmware is corrupt. It cannot return to run-time (non-DFU) operations.
+ */
+ ERR_FIRMWARE,
+ /**
+ * iString indicates a vendor-specific error.
+ */
+ ERR_VENDOR,
+ /**
+ * Device detected unexpected USB reset signaling.
+ */
+ ERR_USBR,
+ /**
+ * Device detected unexpected power on reset.
+ */
+ ERR_POR,
+ /**
+ * Something went wrong, but the device does not know what it was.
+ */
+ ERR_UNKNOWN,
+ /**
+ * Device stalled an unexpected request.
+ */
+ ERR_STALLEDPKT;
+
+ public static DeviceStatus fromValue(byte value) {
+ return values()[value];
+ }
+}
diff --git a/examples/stm_dfu/src/main/java/net/codecrete/usb/dfu/Page.java b/examples/stm_dfu/src/main/java/net/codecrete/usb/dfu/Page.java
new file mode 100644
index 00000000..92158670
--- /dev/null
+++ b/examples/stm_dfu/src/main/java/net/codecrete/usb/dfu/Page.java
@@ -0,0 +1,54 @@
+//
+// Java Does USB
+// Copyright (c) 2022 Manuel Bleichenbacher
+// Licensed under MIT License
+// https://opensource.org/licenses/MIT
+//
+
+package net.codecrete.usb.dfu;
+
+/**
+ * Page of flash memory, RAM or other type of memory.
+ *
+ * If count is > 1, it represents a sector consisting of multiple equal pages.
+ *
+ * @param segment the memory segment this page belongs to
+ * @param startAddress start address
+ * @param count number of pages
+ * @param pageSize page size (in bytes)
+ * @param attributes page attributes
+ */
+public record Page(Segment segment, int startAddress, int count, int pageSize, int attributes) {
+
+ /**
+ * Gets the end address of the page or sector.
+ * @return the end address
+ */
+ public int getEndAddress() {
+ return startAddress + count * pageSize;
+ }
+
+ /**
+ * Indicates if the page or sector is readable.
+ * @return {@code true} if it is readable
+ */
+ public boolean isReadable() {
+ return (attributes & 1) != 0;
+ }
+
+ /**
+ * Indicates if the page or sector is erasable.
+ * @return {@code true} if it is erasable
+ */
+ public boolean isErasable() {
+ return (attributes & 2) != 0;
+ }
+
+ /**
+ * Indicates if the page or sector is writable.
+ * @return {@code true} if it is writable
+ */
+ public boolean isWritable() {
+ return (attributes & 4) != 0;
+ }
+}
diff --git a/examples/stm_dfu/src/main/java/net/codecrete/usb/dfu/STM32.java b/examples/stm_dfu/src/main/java/net/codecrete/usb/dfu/STM32.java
new file mode 100644
index 00000000..a9f40b57
--- /dev/null
+++ b/examples/stm_dfu/src/main/java/net/codecrete/usb/dfu/STM32.java
@@ -0,0 +1,18 @@
+//
+// Java Does USB
+// Copyright (c) 2022 Manuel Bleichenbacher
+// Licensed under MIT License
+// https://opensource.org/licenses/MIT
+//
+
+package net.codecrete.usb.dfu;
+
+/**
+ * STM32 specific constants
+ */
+public class STM32 {
+
+ private STM32() { }
+
+ public static final int FLASH_BASE_ADDRESS = 0x08000000;
+}
diff --git a/examples/stm_dfu/src/main/java/net/codecrete/usb/dfu/Segment.java b/examples/stm_dfu/src/main/java/net/codecrete/usb/dfu/Segment.java
new file mode 100644
index 00000000..f3fdad2b
--- /dev/null
+++ b/examples/stm_dfu/src/main/java/net/codecrete/usb/dfu/Segment.java
@@ -0,0 +1,154 @@
+//
+// Java Does USB
+// Copyright (c) 2022 Manuel Bleichenbacher
+// Licensed under MIT License
+// https://opensource.org/licenses/MIT
+//
+
+package net.codecrete.usb.dfu;
+
+import net.codecrete.usb.UsbControlTransfer;
+import net.codecrete.usb.UsbDevice;
+
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.regex.Pattern;
+
+import static net.codecrete.usb.UsbRecipient.DEVICE;
+import static net.codecrete.usb.UsbRequestType.STANDARD;
+
+/**
+ * Represents a memory segment of the USB device, be it flash memory, RAM or any other type.
+ */
+public class Segment {
+
+ private static final Pattern SEGMENT_PATTERN = Pattern.compile("@([^/]+)/0x([0-9A-Fa-f]+)/");
+ private static final Pattern SECTOR_PATTERN = Pattern.compile(",?(\\d+)\\*(\\d+) ?([BKM]?)(.)");
+
+ /**
+ * Decodes the segment information from the USB interface description.
+ * @param device the USB device
+ * @param interfaceNumber the number of the DFU USB interface
+ * @return list of segments
+ */
+ public static List getSegments(UsbDevice device, int interfaceNumber) {
+ var result = new ArrayList();
+
+ // STM uses multiple alternate interface settings to represent segments.
+ // The alternate interface setting name describes the sectors within the segment.
+ var configDesc = device.getConfigurationDescriptor();
+ int offset = 0;
+ while (offset < configDesc.length) {
+ if (configDesc[offset + 1] == 4 && (configDesc[offset + 2] & 0xff) == interfaceNumber) {
+ int altSetting = configDesc[offset + 3] & 0xff;
+ int stringIndex = configDesc[offset + 8] & 0xff;
+ var altSettingName = getStringDescriptor(device, stringIndex);
+ result.add(new Segment(altSetting, altSettingName));
+ }
+ offset += configDesc[offset] & 0xff;
+ }
+
+ return result;
+ }
+
+ /**
+ * Retrieves a USB string.
+ * @param device the USB device
+ * @param index the index of the string descriptor
+ * @return the string
+ */
+ private static String getStringDescriptor(UsbDevice device, int index) {
+ var setup = new UsbControlTransfer(STANDARD, DEVICE, 6, (3 << 8) | index, 0);
+ byte[] stringDesc = device.controlTransferIn(setup, 255);
+ int descLen = stringDesc[0] & 0xff;
+ return new String(stringDesc, 2, descLen - 2, StandardCharsets.UTF_16LE);
+ }
+
+ /**
+ * Gets the page for the specified address.
+ * @param segments the list of segments
+ * @param address the address
+ * @return the page, or {@code null} if not found
+ */
+ public static Page findPage(List segments, int address) {
+ for (var seg : segments) {
+ for (var sec : seg.getSectors()) {
+ if (address >= sec.startAddress() && address < sec.getEndAddress()) {
+ int offset = address - sec.startAddress();
+ int pageNum = offset / sec.pageSize();
+ return new Page(seg, sec.startAddress() + pageNum * sec.pageSize(), 1, sec.pageSize(), sec.attributes());
+ }
+ }
+ }
+
+ return null;
+ }
+
+ private final int altSetting;
+ private final String name;
+
+ private final List sectors;
+
+
+ /**
+ * Creates a new instance.
+ *
+ * The segment descriptor is the name of the USB alternate interface setting.
+ *
+ * @param altSetting alternate interface setting number
+ * @param segmentDesc segment descriptor
+ */
+ private Segment(int altSetting, String segmentDesc) {
+ // The format is described in "UM0424 STM32 USB-FS-Device development kit", ch. 10.3.2
+ this.altSetting = altSetting;
+ sectors = new ArrayList<>();
+
+ var match = SEGMENT_PATTERN.matcher(segmentDesc);
+ if (!match.find())
+ throw new DFUException("Invalid segment description: " + segmentDesc);
+ this.name = match.group(1).trim();
+ var startAddress = (int) Long.parseLong(match.group(2), 16);
+
+ match = SECTOR_PATTERN.matcher(segmentDesc.substring(match.end()));
+ while (match.find()) {
+ var count = Integer.parseInt(match.group(1));
+ var size = Integer.parseInt(match.group(2));
+ var multiplier = match.group(3);
+ var attributes = match.group(4).charAt(0) - 0x60;
+
+ if (multiplier.equals("K")) {
+ size *= 1024;
+ } else if (multiplier.equals("M")) {
+ size *= 1024 * 1024;
+ }
+
+ sectors.add(new Page(this, startAddress, count, size, attributes));
+ startAddress += size;
+ }
+ }
+
+ /**
+ * Gets the alternative interface setting number
+ * @return the setting number
+ */
+ public int getAltSetting() {
+ return altSetting;
+ }
+
+ /**
+ * Gets the segment name.
+ * @return the name
+ */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * Gets the sectors withing the segment
+ * @return list of sectors
+ */
+ public List getSectors() {
+ return sectors;
+ }
+}
diff --git a/java-does-usb/.mvn/wrapper/maven-wrapper.properties b/java-does-usb/.mvn/wrapper/maven-wrapper.properties
new file mode 100644
index 00000000..d58dfb70
--- /dev/null
+++ b/java-does-usb/.mvn/wrapper/maven-wrapper.properties
@@ -0,0 +1,19 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+wrapperVersion=3.3.2
+distributionType=only-script
+distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip
diff --git a/java-does-usb/jextract/README.md b/java-does-usb/jextract/README.md
index cfd1632c..79d0f270 100644
--- a/java-does-usb/jextract/README.md
+++ b/java-does-usb/jextract/README.md
@@ -1,42 +1,37 @@
# Code Generation with *jextract*
-Some of the binding code for accessing native functions and data structures is generated with [jextract](https://jdk.java.net/jextract/). The tool is still under construction and has its limitations.
+A major part of the binding code for accessing native functions and data structures is generated with [jextract](https://jdk.java.net/jextract/). *jextract* is not bundled with the JDK. The binaries can be downloaded from [jdk.java.net/jextract](https://jdk.java.net/jextract/)
-In order to generate the code, the scripts in this directory have to be run (`gen_linux.sh`, `gen_macos.sh` and `gen_win_xxx.cmd`). Each script has to be run on that particular operating system.
+In order to generate the code, the scripts in the subdirectories have to be run (`linux/gen_linux.sh`, `macos/gen_macos.sh` and `windows/gen_win.cmd`). Each script has to be run on the particular operating system. The scripts expect the *jextract* binary to be in a *jextract* directory at the same parent directory as the *Java Does USB* project. If that is not the case, the *jextract* path can be modified at the top of the scripts.
-The code is generated in directories below `gen`, i.e. `main/java/net/codecrete/usb/linux/gen` and similar for the other operating systems. For each library (`xxx.so` or `xxx.dll`) and each macOS framework, a separate package is created.
+The code is generated in directories below `gen`, i.e. `main/java/net/codecrete/usb/linux/gen` and similarly for the other operating systems. For each library (`xxx.so` or `xxx.dll`) and each macOS framework, a separate package is created.
-The scripts explicitly specify the functions, structs etc. to include as generating code for entire operating system header files can result in an excessive amount of Java source files and classes.
+The scripts explicitly specify the functions, structs etc. to include as generating code for entire operating system header files will result in an excessive amount of Java source files and classes.
-The resulting code is then committed to the source code repository. Before the commit, imports are cleaned up to get rid of superfluous imports. Most IDEs provide a convenient command to execute this on entire directories.
+The resulting code is then committed to the source code repository.
## General limitations
-- Binaries of *jextract* can be downloaded from https://jdk.java.net/jextract/. x64 binaries are available but no ARM64 binaries. According to the mailing list, cross-compiling is not possible, i.e. ARM64 binaries are needed on macOS with Apple Silicon. But so far, the x64 binaries (using the Rosetta2 emulation) have worked without problems.
+- According to the jextract mailing list, it would be required to create separate code for Intel x64 and ARM64 architecture. And jextract would need to be run on each architecture separately (no cross-compilation). Fortunately, this doesn't seem to be the case. Linux code generated on Intel x64 also runs on ARM64 without change. The same holds for macOS. However, jextract needs to be run on each operating system separately.
-- `typedef` and `struct`:
-
- 1. If only the `typedef` is included (`--include-typedef`), an empty Java class is generated.
- 2. If both *typedef* and the `struct` it refers to are included, the `typedef` class inherits from the `struct` class, which contains all the `struct` members.
- 3. If the `typedef` refers to an unnamed `struct`, the generated class contains all the `struct` members.
-
- Case 1 looks like a bug.
+- The *Foreign Function And Memory* API has the abilitiy to save the thread-specific error values (`GetLastError()` on Windows, `errno` on Linux). This is required as the JVM calls operating system functions as well, which overwrite the result values. To save the values, an additional parameter must be added to function calls. Unfortunately, this is not supported by jextract. So a good number of function bindings have to be written manually.
- *jextract* is not really transparent about what it does. It often skips elements without providing any information. In particular, it will silently skip a requested element in these cases:
- `--include-var myvar` if `myvar` is declared as `static`.
- - `--include-var myvar` if `myvar` is an `enum` constant. `enum` constants must be requested with `--include-macro`.
- - `--include-macro MYMACRO` if `MYMACRO` is function-like, even if it evaluates to a constant.
+ - `--include-var myvar` if `myvar` is an `enum` constant. `enum` constants must be requested with `--include-constant`.
+ - `--include-constant MYCONSTANT` if `MYCONSTANT` is function-like, even if it evaluates to a constant.
- `--include-struct mystruct` if `mystruct` is actually a `typedef` to a `struct`.
- `--include-typedef mystruct` if `mystruct` is actually a `struct`.
- `--include-typedef mytypedef` if `mytypedef` is a `typedef` for a primitive type.
+- *jextract* resolves all _typedef_s to their actual types. So this library does not use any _--include-typedef_ option. And there does not seem any obvious use for it beyond cosmetics.
## Linux
-To run the script, most likely the header files for *libudev* must be installed (the library itself is most likely already installed):
+To run the script, the header files for *libudev* must be present. In most cases, they aren't install by default (in contrast to the library itself):
```
sudo apt-get install libudev-dev
@@ -44,64 +39,44 @@ sudo apt-get install libudev-dev
On Linux, the limitations are:
-- `usbdevice_fs.h`: The macro `USBDEVFS_CONTROL` and all similar ones are not generated. They are probably considered function-like macros. *jextract* does not generate code for function-like macros. But `USBDEVFS_CONTROL` evaluates to a constant.
+- `usbdevice_fs.h`: The macro `USBDEVFS_CONTROL` and all similar ones are not generated. They are probably considered function-like macros. *jextract* does not generate code for function-like macros. `USBDEVFS_CONTROL` would evaluate to a constant.
- `sd-device.h` (header file for *libsystemd*): *jextract* fails with *"Error: /usr/include/inttypes.h:290:8: error: unknown type name 'intmax_t'"*. The reason is yet unknown. This code is currently not needed as *libudev* is used instead of *libsystemd*. They are related, *libsystemd* is the future solution, but it is missing support for monitoring devices.
-- `libudev.h`: After code generation, the class `RuntimeHelper.java` in `.../linux/gen/udev` must be manually modified as the code to access the library does not work for the directory the library is located in. So replace:
-
-```
-System.loadLibrary("udev");
-SymbolLookup loaderLookup = SymbolLookup.loaderLookup();
-```
-
-with:
-
-```
-SymbolLookup loaderLookup = SymbolLookup.libraryLookup("libudev.so", MemorySession.openImplicit());
-```
## MacOS
-Most of the required native functions on macOS are part of a framework. Framework internally have a more complex file organization of header and binary files than appears on the outside. Thus they require a special logic to locate framework header files. *clang* supports it with the `-F`. *jextract* allows to specify the options via `compiler_flags.txt` file. Since the file must be in the local directory and since it does not apply to Linux and Windows, separate directories must be used for the operating systems.
+Most of the required native functions on macOS are part of a framework. Frameworks internally have a more complex file organization of header and binary files than appears from the outside. Thus, they require a special logic to locate framework header files. *clang* supports it with the `-F`. *jextract* allows to specify the options via `compiler_flags.txt` file. Since the file must be in the local directory and since it does not apply to Linux and Windows, separate directories must be used for the operating systems.
-The generated code has the same problem as the Linux code for *udev*. It must be manually changed to use `SymbolLookup.libraryLookup()` for the libraries `CoreFoundation.framework/CoreFoundation` and `IOKit.framework/IOKit` respectively.
## Windows
-Most Windows SDK header files are not independent. They require that `Windows.h` is included first. So instead of specifying the target header files directly, a helper header file (`windows_headers.h` in this directory) is specified.
+The Windows code is not generated with _jextract_ but with [Windows API Generator](https://github.com/manuelbl/WindowsApiGenerator)
+instead. It is run as a Maven plugin. The generated code is not committed to GitHub.
-The known limitations are:
-
-- Variable size `struct`: Several Windows struct are of variable size. The last member is an array. The `struct` definition specifies array length 1. But you are expected to allocate more space depending on the actual array size you need. *jextract* generates code for array length 1 and when access array members, the length is checked. So the generated code is difficult to use. Variable size `struct`s are a pain - in any language.
-
-- `USB_NODE_CONNECTION_INFORMATION_EX`: This struct uses a packed layout without considering alignment. The last four members are on an odd offset even though they are multiple bytes long. *jextract* creates the correct offsets but defines strict alignment constraints for the members. So the memory layout cannot be instantiated as it throws an exception.
-
-- GUID constants like `GUID_DEVINTERFACE_USB_DEVICE` do not work. While code is generated, the code fails at run-time as it is unable to locate the symbol. This is due to the fact that `GUID_DEVINTERFACE_USB_DEVICE` actually resolve to a variable definition and not to a variable declaration. It is not part of any library. Such constants should be skipped by *jextract*.
-
-- *jextract* is a batch script and turns off *echo mode*. If a single batch scripts has multiple calls of *jextract*, two things need to be considered:
-
- - If the regular command interpreter `cmd.exe` is used, *jextract* must be called using `call`, i.e. `call jextract header.h`.
- - If *PowerShell* is used instead, `call` is not needed but *PowerShell* must be configured to allow the execution of scripts.
- - *jextract* turns off *echo mode*. So the first call will behave differently than the following calls.
+Windows API Generator supports call state capturing (`GetLastError()`), structs with a
+variable size, GUID and device property key (`DEVPKEY`) constants etc.
## Code Size
*jextract* generates a comprehensive set of methods for each function, struct, struct member etc. Most of it will not be used as a typical application just uses a subset of struct members, might only read or write them etc. So a considerable amount of code is generated. For some types, it's a bit excessive.
-The worst example is [`IOUSBInterfaceStruct942`](https://github.com/manuelbl/JavaDoesUSB/blob/main/java-does-usb/src/main/java/net/codecrete/usb/macos/gen/iokit/IOUSBDeviceStruct942.java) (macOS). This is a `struct` consisting of about 75 member functions. It's bascially a vtable of a C++ class. *jextract* generates the same number of classes plus a huge class for the struct itself. The total code size (compiled) for this single `struct` is over 300 kByte.
+The worst example is [`IOUSBInterfaceStruct190`](https://github.com/manuelbl/JavaDoesUSB/blob/main/java-does-usb/src/main/java/net/codecrete/usb/macos/gen/iokit/IOUSBInterfaceStruct190.java) (macOS). This is a `struct` consisting of about 50 member functions. It's basically a vtable of a C++ class. For this single `struct`, *jextract* generates codes resulting in 100 class files with a total size of 213kByte.
+
+The table below shows class file size statistics for version 1.2.2 of the library:
+
+| Operating Systems | Manually Written | % | Generated | % | Total | % |
+|-------------------|-----------------:|------:|----------:|------:|----------:|--------:|
+| Linux | 58,393 | 4.5% | 154,398 | 11.8% | 212,791 | 16.3% |
+| macOS | 81,441 | 6.2% | 427,258 | 32.8% | 508,699 | 39.0% |
+| Windows | 84,099 | 6.5% | 384,452 | 29.5% | 468,551 | 35.9% |
+| Common | 113,568 | 8.7% | | | 113,568 | 8.7% |
+| Grand Total | 337,501 | 25.9% | 966,108 | 74.1% | 1,303,609 | 100.0% |
-The table below shows statictics for version 0.2.0 of the library:
-| Operating Systems | Manually Created | % | Generated | % | Total | % |
-| - | -:| -:| -:| -:| -:| -:|
-| Linux | 24,140 | 1.96% | 182,774 | 14.84% | 206,914 | 16.80% |
-| macOS | 57,788 | 4.69% | 666,037 | 54.08% | 723,825 | 58.77% |
-| Windows | 46,085 | 3.74% | 201,000 | 16.32% | 247,085 | 20.06% |
-| Common | 53,774 | 4.37% | 53,774 | 0.00% | | 4.37% |
-| Grand Total | 181,787 | 14.76% | 1,049,811 | 85.24% | 1,231,598 | 100.00% |
+*Class File Size (compiled), in bytes and percentage of total size*
-*Code Size (compiled), in bytes and percentage of total size*
+If *jextract* could generate code for error state capturing, there would be even more generated and less manually written code.
diff --git a/java-does-usb/jextract/linux/epoll.h b/java-does-usb/jextract/linux/epoll.h
new file mode 100644
index 00000000..c7c68b45
--- /dev/null
+++ b/java-does-usb/jextract/linux/epoll.h
@@ -0,0 +1,3 @@
+typedef unsigned int uint32_t;
+typedef unsigned long int uint64_t;
+#include
diff --git a/java-does-usb/jextract/linux/gen_linux.sh b/java-does-usb/jextract/linux/gen_linux.sh
index ce4b94c1..7e5eb5ec 100755
--- a/java-does-usb/jextract/linux/gen_linux.sh
+++ b/java-does-usb/jextract/linux/gen_linux.sh
@@ -1,40 +1,41 @@
#!/bin/sh
-JEXTRACT=../../../../jextract-19/bin/jextract
+JEXTRACT=../../../../jextract/bin/jextract
-# sd-device.h (install libsystemd-dev if file is missing)
-# Error: /usr/include/inttypes.h:290:8: error: unknown type name 'intmax_t'
-#$JEXTRACT --source --output ../src/main/java \
-#$JEXTRACT --source --output ../src/main/java \
-# --header-class-name sd_device \
-# --target-package net.codecrete.usb.linux.gen.sd-device
-# /usr/include/systemd/sd-device.h
+rm -rf ../../src/main/java/net/codecrete/usb/linux/gen
# errno.h
-$JEXTRACT --source --output ../../src/main/java \
+$JEXTRACT --output ../../src/main/java \
--header-class-name errno \
--target-package net.codecrete.usb.linux.gen.errno \
- --include-function __errno_location \
+ --include-constant EPIPE \
+ --include-constant EAGAIN \
+ --include-constant EBADF \
+ --include-constant ECANCELED \
+ --include-constant EINVAL \
+ --include-constant ENODEV \
+ --include-constant EINTR \
+ --include-constant ENOENT \
/usr/include/errno.h
-# ioctl.h
-$JEXTRACT --source --output ../../src/main/java \
- --header-class-name ioctl \
- --target-package net.codecrete.usb.linux.gen.ioctl \
- --include-function ioctl \
- /usr/include/x86_64-linux-gnu/sys/ioctl.h
+# string.h
+$JEXTRACT --output ../../src/main/java \
+ --header-class-name string \
+ --target-package net.codecrete.usb.linux.gen.string \
+ --include-function strerror \
+ /usr/include/string.h
# fcntl.h
-$JEXTRACT --source --output ../../src/main/java \
+$JEXTRACT --output ../../src/main/java \
--header-class-name fcntl \
--target-package net.codecrete.usb.linux.gen.fcntl \
- --include-function open \
- --include-macro O_CLOEXEC \
- --include-macro O_RDWR \
+ --include-constant O_CLOEXEC \
+ --include-constant O_RDWR \
+ --include-constant FD_CLOEXEC \
/usr/include/fcntl.h
# unistd.h
-$JEXTRACT --source --output ../../src/main/java \
+$JEXTRACT --output ../../src/main/java \
--header-class-name unistd \
--target-package net.codecrete.usb.linux.gen.unistd \
--include-function close \
@@ -42,22 +43,29 @@ $JEXTRACT --source --output ../../src/main/java \
# usbdevice_fs.h
# Missing constants like USBDEVFS_CLAIMINTERFACE
-$JEXTRACT --source --output ../../src/main/java \
+$JEXTRACT --output ../../src/main/java \
--header-class-name usbdevice_fs \
--target-package net.codecrete.usb.linux.gen.usbdevice_fs \
--include-struct usbdevfs_bulktransfer \
--include-struct usbdevfs_ctrltransfer \
- --include-macro USBDEVFS_CONTROL \
- --include-macro USBDEVFS_BULK \
- --include-macro USBDEVFS_CLAIMINTERFACE \
- --include-macro USBDEVFS_RELEASEINTERFACE \
+ --include-struct usbdevfs_setinterface \
+ --include-struct usbdevfs_urb \
+ --include-struct usbdevfs_disconnect_claim \
+ --include-struct usbdevfs_ioctl \
+ --include-struct usbdevfs_iso_packet_desc \
+ --include-constant USBDEVFS_URB_TYPE_INTERRUPT \
+ --include-constant USBDEVFS_URB_TYPE_CONTROL \
+ --include-constant USBDEVFS_URB_TYPE_BULK \
+ --include-constant USBDEVFS_URB_TYPE_ISO \
+ --include-constant USBDEVFS_DISCONNECT_CLAIM_EXCEPT_DRIVER \
/usr/include/linux/usbdevice_fs.h
# libudev.h
-$JEXTRACT --source --output ../../src/main/java \
+# (install libudev-dev if file is missing)
+$JEXTRACT --output ../../src/main/java \
--header-class-name udev \
--target-package net.codecrete.usb.linux.gen.udev \
- -l udev \
+ -l :libudev.so.1 \
--include-function udev_new \
--include-function udev_enumerate_new \
--include-function udev_enumerate_add_match_subsystem \
@@ -79,10 +87,14 @@ $JEXTRACT --source --output ../../src/main/java \
--include-function udev_monitor_get_fd \
/usr/include/libudev.h
-# select.h
-$JEXTRACT --source --output ../../src/main/java \
- --header-class-name select \
- --target-package net.codecrete.usb.linux.gen.select \
- --include-function select \
- --include-typedef fd_set \
- /usr/include/x86_64-linux-gnu/sys/select.h
+# epoll.h
+$JEXTRACT --output ../../src/main/java \
+ --header-class-name epoll \
+ --target-package net.codecrete.usb.linux.gen.epoll \
+ --include-constant EPOLL_CTL_ADD \
+ --include-constant EPOLL_CTL_DEL \
+ --include-constant EPOLLIN \
+ --include-constant EPOLLOUT \
+ --include-constant EPOLLWAKEUP \
+ epoll.h
+
diff --git a/java-does-usb/jextract/macos/gen_macos.sh b/java-does-usb/jextract/macos/gen_macos.sh
index c90d5cbe..d74e38c2 100755
--- a/java-does-usb/jextract/macos/gen_macos.sh
+++ b/java-does-usb/jextract/macos/gen_macos.sh
@@ -1,17 +1,19 @@
#!/bin/sh
-JEXTRACT=../../../../jextract-19/bin/jextract
+JEXTRACT=../../../../jextract/bin/jextract
# If SDK_DIR is changed, it needs to be changed in compile_flags.txt as well.
SDK_DIR=/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk
+rm -rf ../../src/main/java/net/codecrete/usb/macos/gen
+
# CoreFoundation
-$JEXTRACT --source --output ../../src/main/java \
+$JEXTRACT --output ../../src/main/java \
-I $SDK_DIR/usr/include \
- -lCoreFoundation.framework \
+ -l :/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation \
--header-class-name CoreFoundation \
--target-package net.codecrete.usb.macos.gen.corefoundation \
- --include-typedef CFRange \
- --include-typedef CFUUIDBytes \
+ --include-struct CFRange \
+ --include-struct CFUUIDBytes \
--include-function CFUUIDCreateFromUUIDBytes \
--include-function CFRelease \
--include-function CFStringGetLength \
@@ -23,24 +25,31 @@ $JEXTRACT --source --output ../../src/main/java \
--include-function CFNumberGetValue \
--include-function CFRunLoopGetCurrent \
--include-function CFRunLoopAddSource \
+ --include-function CFRunLoopRemoveSource \
--include-function CFRunLoopRun \
+ --include-function CFMessagePortCreateLocal \
+ --include-function CFMessagePortCreateRunLoopSource \
+ --include-function CFMessagePortCreateRemote \
+ --include-function CFMessagePortSendRequest \
+ --include-function CFDataCreate \
+ --include-function CFDataGetBytePtr \
--include-function CFUUIDGetUUIDBytes \
- --include-macro kCFNumberSInt32Type \
+ --include-constant kCFNumberSInt32Type \
cf_helper.h
# IOKit
-$JEXTRACT --source --output ../../src/main/java \
+$JEXTRACT --output ../../src/main/java \
-I $SDK_DIR/usr/include \
- -lIOKit.framework \
+ -l :/System/Library/Frameworks/IOKit.framework/IOKit \
--header-class-name IOKit \
--target-package net.codecrete.usb.macos.gen.iokit \
--include-var kIOMasterPortDefault \
- --include-macro kIOUSBDeviceClassName \
- --include-macro kIOFirstMatchNotification \
- --include-macro kIOTerminatedNotification \
+ --include-constant kIOUSBDeviceClassName \
+ --include-constant kIOFirstMatchNotification \
+ --include-constant kIOTerminatedNotification \
+ --include-constant kIOReturnExclusiveAccess \
--include-var kCFRunLoopDefaultMode \
--include-struct IOCFPlugInInterfaceStruct \
- --include-typedef IOCFPlugInInterface \
--include-function IOObjectRelease \
--include-function IOIteratorNext \
--include-function IOCreatePlugInInterfaceForService \
@@ -50,19 +59,27 @@ $JEXTRACT --source --output ../../src/main/java \
--include-function IORegistryEntryGetRegistryEntryID \
--include-function IOServiceAddMatchingNotification \
--include-function IOServiceMatching \
- --include-struct IOUSBDeviceStruct942 \
- --include-typedef IOUSBDeviceInterface \
- --include-macro kIOUSBFindInterfaceDontCare \
- --include-typedef IOUSBFindInterfaceRequest \
- --include-typedef IOUSBDevRequest \
- --include-struct IOUSBInterfaceStruct942 \
- --include-typedef IOUSBInterfaceInterface \
+ --include-struct IOUSBDeviceStruct187 \
+ --include-constant kIOUSBFindInterfaceDontCare \
+ --include-struct IOUSBFindInterfaceRequest \
+ --include-struct IOUSBDevRequest \
+ --include-struct IOUSBInterfaceStruct190 \
+ --include-constant kIOUSBTransactionTimeout \
+ --include-constant kIOReturnAborted \
+ --include-constant kIOUSBPipeStalled \
+ --include-constant kUSBReEnumerateCaptureDeviceMask \
+ --include-constant kUSBReEnumerateReleaseDeviceMask \
+ --include-struct CFUUIDBytes \
iokit_helper.h
# mach.h
-$JEXTRACT --source --output ../../src/main/java \
+$JEXTRACT --output ../../src/main/java \
-I $SDK_DIR/usr/include \
--header-class-name mach \
--target-package net.codecrete.usb.macos.gen.mach \
--include-function mach_error_string \
$SDK_DIR/usr/include/mach/mach.h
+
+sed -i '' -E -f remove_fp_upcall.sed ../../src/main/java/net/codecrete/usb/macos/gen/iokit/IOUSBDeviceStruct187.java
+sed -i '' -E -f remove_fp_upcall.sed ../../src/main/java/net/codecrete/usb/macos/gen/iokit/IOUSBInterfaceStruct190.java
+sed -i '' -E -f remove_fp_upcall.sed ../../src/main/java/net/codecrete/usb/macos/gen/iokit/IOCFPlugInInterfaceStruct.java
diff --git a/java-does-usb/jextract/macos/remove_fp_upcall.sed b/java-does-usb/jextract/macos/remove_fp_upcall.sed
new file mode 100644
index 00000000..d90e01d3
--- /dev/null
+++ b/java-does-usb/jextract/macos/remove_fp_upcall.sed
@@ -0,0 +1,6 @@
+/The function pointer signature/,/ \}/ {
+ s/^.*function pointer signature.*$/ *\//p
+ d
+
+}
+/MethodHandle UP\$MH = /,/ \}/d
diff --git a/java-does-usb/jextract/windows/gen_win.cmd b/java-does-usb/jextract/windows/gen_win.cmd
deleted file mode 100644
index c69865d2..00000000
--- a/java-does-usb/jextract/windows/gen_win.cmd
+++ /dev/null
@@ -1,154 +0,0 @@
-set JEXTRACT=..\..\..\..\jextract-19\bin\jextract.bat
-set SDK_DIR=C:\Program Files (x86)\Windows Kits\10\Include\10.0.22000.0
-
-call %JEXTRACT% --source --output ../../src/main/java ^
- -D _AMD64_ -D _M_AMD64=100 -D UNICODE -D _UNICODE ^
- -I "%SDK_DIR%\um" ^
- -I "%SDK_DIR%\shared" ^
- -l Kernel32 ^
- --header-class-name Kernel32 ^
- --target-package net.codecrete.usb.windows.gen.kernel32 ^
- --include-function CreateFileW ^
- --include-function CloseHandle ^
- --include-function DeviceIoControl ^
- --include-function GetLastError ^
- --include-function GetModuleHandleW ^
- --include-function FormatMessageW ^
- --include-function LocalFree ^
- --include-macro ERROR_SUCCESS ^
- --include-macro ERROR_NO_MORE_ITEMS ^
- --include-macro ERROR_MORE_DATA ^
- --include-macro ERROR_INSUFFICIENT_BUFFER ^
- --include-macro ERROR_FILE_NOT_FOUND ^
- --include-macro GENERIC_READ ^
- --include-macro GENERIC_WRITE ^
- --include-macro FILE_SHARE_READ ^
- --include-macro FILE_SHARE_WRITE ^
- --include-macro FILE_ATTRIBUTE_NORMAL ^
- --include-macro FILE_FLAG_OVERLAPPED ^
- --include-macro OPEN_EXISTING ^
- --include-macro FORMAT_MESSAGE_ALLOCATE_BUFFER ^
- --include-macro FORMAT_MESSAGE_FROM_SYSTEM ^
- --include-macro FORMAT_MESSAGE_IGNORE_INSERTS ^
- --include-struct _GUID ^
- --include-typedef GUID ^
- windows_headers.h
-
-call %JEXTRACT% --source --output ../../src/main/java ^
- -D _AMD64_ -D _M_AMD64=100 -D UNICODE -D _UNICODE ^
- -I "%SDK_DIR%\um" ^
- -I "%SDK_DIR%\shared" ^
- -l SetupAPI ^
- --header-class-name SetupAPI ^
- --target-package net.codecrete.usb.windows.gen.setupapi ^
- --include-function SetupDiGetClassDevsW ^
- --include-function SetupDiDestroyDeviceInfoList ^
- --include-function SetupDiEnumDeviceInfo ^
- --include-function SetupDiEnumDeviceInterfaces ^
- --include-function SetupDiGetDeviceInterfaceDetailW ^
- --include-function SetupDiGetDeviceRegistryPropertyW ^
- --include-function SetupDiGetDevicePropertyW ^
- --include-function SetupDiOpenDeviceInterfaceW ^
- --include-function SetupDiCreateDeviceInfoList ^
- --include-function SetupDiOpenDeviceInfoW ^
- --include-function SetupDiOpenDevRegKey ^
- --include-function SetupDiDeleteDeviceInterfaceData ^
- --include-struct _SP_DEVINFO_DATA ^
- --include-typedef SP_DEVINFO_DATA ^
- --include-struct _SP_DEVICE_INTERFACE_DATA ^
- --include-typedef SP_DEVICE_INTERFACE_DATA ^
- --include-struct _SP_DEVICE_INTERFACE_DETAIL_DATA_W ^
- --include-typedef SP_DEVICE_INTERFACE_DETAIL_DATA_W ^
- --include-macro DIGCF_PRESENT ^
- --include-macro DIGCF_DEVICEINTERFACE ^
- --include-macro SPDRP_ADDRESS ^
- --include-macro DEVPROP_TYPE_UINT32 ^
- --include-macro DEVPROP_TYPE_STRING ^
- --include-macro DEVPROP_TYPEMOD_LIST ^
- --include-macro DICS_FLAG_GLOBAL ^
- --include-macro DIREG_DEV ^
- windows_headers.h
-
-call %JEXTRACT% --source --output ../../src/main/java ^
- -D _AMD64_ -D _M_AMD64=100 -D UNICODE -D _UNICODE ^
- --header-class-name StdLib ^
- --target-package net.codecrete.usb.windows.gen.stdlib ^
- --include-function wcslen ^
- windows_headers.h
-
-call %JEXTRACT% --source --output ../../src/main/java ^
- -D _AMD64_ -D _M_AMD64=100 -D UNICODE -D _UNICODE ^
- -I "%SDK_DIR%\um" ^
- -I "%SDK_DIR%\shared" ^
- --header-class-name USBIoctl ^
- --target-package net.codecrete.usb.windows.gen.usbioctl ^
- --include-macro IOCTL_USB_GET_NODE_CONNECTION_INFORMATION_EX ^
- --include-macro IOCTL_USB_GET_DESCRIPTOR_FROM_NODE_CONNECTION ^
- windows_headers.h
-
-call %JEXTRACT% --source --output ../../src/main/java ^
- -D _AMD64_ -D _M_AMD64=100 -D UNICODE -D _UNICODE ^
- -I "%SDK_DIR%\um" ^
- -I "%SDK_DIR%\shared" ^
- -l User32 ^
- --header-class-name User32 ^
- --target-package net.codecrete.usb.windows.gen.user32 ^
- --include-function RegisterClassExW ^
- --include-function CreateWindowExW ^
- --include-function RegisterDeviceNotificationW ^
- --include-function GetMessageW ^
- --include-function DefWindowProcW ^
- --include-macro DEVICE_NOTIFY_WINDOW_HANDLE ^
- --include-macro HWND_MESSAGE ^
- --include-macro WM_DEVICECHANGE ^
- --include-macro DBT_DEVICEARRIVAL ^
- --include-macro DBT_DEVICEREMOVECOMPLETE ^
- --include-macro DBT_DEVTYP_DEVICEINTERFACE ^
- --include-struct tagMSG ^
- --include-typedef MSG ^
- --include-struct tagWNDCLASSEXW ^
- --include-typedef WNDCLASSEXW ^
- --include-struct _DEV_BROADCAST_HDR ^
- --include-typedef DEV_BROADCAST_HDR ^
- --include-struct _DEV_BROADCAST_DEVICEINTERFACE_W ^
- --include-typedef DEV_BROADCAST_DEVICEINTERFACE_W ^
- windows_headers.h
-
-call %JEXTRACT% --source --output ../../src/main/java ^
- -D _AMD64_ -D _M_AMD64=100 -D UNICODE -D _UNICODE ^
- -I "%SDK_DIR%\um" ^
- -I "%SDK_DIR%\shared" ^
- -l Winusb ^
- --header-class-name WinUSB ^
- --target-package net.codecrete.usb.windows.gen.winusb ^
- --include-function WinUsb_Initialize ^
- --include-function WinUsb_Free ^
- --include-function WinUsb_GetDescriptor ^
- --include-function WinUsb_ControlTransfer ^
- --include-function WinUsb_WritePipe ^
- --include-function WinUsb_ReadPipe ^
- --include-function WinUsb_GetAssociatedInterface ^
- windows_headers.h
-
-call %JEXTRACT% --source --output ../../src/main/java ^
- -D _AMD64_ -D _M_AMD64=100 -D UNICODE -D _UNICODE ^
- -I "%SDK_DIR%\um" ^
- -I "%SDK_DIR%\shared" ^
- -l Advapi32 ^
- --header-class-name Advapi32 ^
- --target-package net.codecrete.usb.windows.gen.advapi32 ^
- --include-function RegQueryValueExW ^
- --include-function RegCloseKey ^
- --include-macro REG_MULTI_SZ ^
- --include-macro KEY_READ ^
- windows_headers.h
-
-call %JEXTRACT% --source --output ../../src/main/java ^
- -D _AMD64_ -D _M_AMD64=100 -D UNICODE -D _UNICODE ^
- -I "%SDK_DIR%\um" ^
- -I "%SDK_DIR%\shared" ^
- -l Ole32 ^
- --header-class-name Ole32 ^
- --target-package net.codecrete.usb.windows.gen.ole32 ^
- --include-function CLSIDFromString ^
- windows_headers.h
diff --git a/java-does-usb/jextract/windows/windows_headers.h b/java-does-usb/jextract/windows/windows_headers.h
deleted file mode 100644
index fbc95ecb..00000000
--- a/java-does-usb/jextract/windows/windows_headers.h
+++ /dev/null
@@ -1,6 +0,0 @@
-#include
-#include
-#include
-#include
-#include
-#include
diff --git a/java-does-usb/mvnw b/java-does-usb/mvnw
new file mode 100755
index 00000000..19529ddf
--- /dev/null
+++ b/java-does-usb/mvnw
@@ -0,0 +1,259 @@
+#!/bin/sh
+# ----------------------------------------------------------------------------
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+# ----------------------------------------------------------------------------
+
+# ----------------------------------------------------------------------------
+# Apache Maven Wrapper startup batch script, version 3.3.2
+#
+# Optional ENV vars
+# -----------------
+# JAVA_HOME - location of a JDK home dir, required when download maven via java source
+# MVNW_REPOURL - repo url base for downloading maven distribution
+# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven
+# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output
+# ----------------------------------------------------------------------------
+
+set -euf
+[ "${MVNW_VERBOSE-}" != debug ] || set -x
+
+# OS specific support.
+native_path() { printf %s\\n "$1"; }
+case "$(uname)" in
+CYGWIN* | MINGW*)
+ [ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")"
+ native_path() { cygpath --path --windows "$1"; }
+ ;;
+esac
+
+# set JAVACMD and JAVACCMD
+set_java_home() {
+ # For Cygwin and MinGW, ensure paths are in Unix format before anything is touched
+ if [ -n "${JAVA_HOME-}" ]; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ]; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ JAVACCMD="$JAVA_HOME/jre/sh/javac"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ JAVACCMD="$JAVA_HOME/bin/javac"
+
+ if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then
+ echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2
+ echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2
+ return 1
+ fi
+ fi
+ else
+ JAVACMD="$(
+ 'set' +e
+ 'unset' -f command 2>/dev/null
+ 'command' -v java
+ )" || :
+ JAVACCMD="$(
+ 'set' +e
+ 'unset' -f command 2>/dev/null
+ 'command' -v javac
+ )" || :
+
+ if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then
+ echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2
+ return 1
+ fi
+ fi
+}
+
+# hash string like Java String::hashCode
+hash_string() {
+ str="${1:-}" h=0
+ while [ -n "$str" ]; do
+ char="${str%"${str#?}"}"
+ h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296))
+ str="${str#?}"
+ done
+ printf %x\\n $h
+}
+
+verbose() { :; }
+[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; }
+
+die() {
+ printf %s\\n "$1" >&2
+ exit 1
+}
+
+trim() {
+ # MWRAPPER-139:
+ # Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds.
+ # Needed for removing poorly interpreted newline sequences when running in more
+ # exotic environments such as mingw bash on Windows.
+ printf "%s" "${1}" | tr -d '[:space:]'
+}
+
+# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties
+while IFS="=" read -r key value; do
+ case "${key-}" in
+ distributionUrl) distributionUrl=$(trim "${value-}") ;;
+ distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;;
+ esac
+done <"${0%/*}/.mvn/wrapper/maven-wrapper.properties"
+[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in ${0%/*}/.mvn/wrapper/maven-wrapper.properties"
+
+case "${distributionUrl##*/}" in
+maven-mvnd-*bin.*)
+ MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/
+ case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in
+ *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;;
+ :Darwin*x86_64) distributionPlatform=darwin-amd64 ;;
+ :Darwin*arm64) distributionPlatform=darwin-aarch64 ;;
+ :Linux*x86_64*) distributionPlatform=linux-amd64 ;;
+ *)
+ echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2
+ distributionPlatform=linux-amd64
+ ;;
+ esac
+ distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip"
+ ;;
+maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;;
+*) MVN_CMD="mvn${0##*/mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;;
+esac
+
+# apply MVNW_REPOURL and calculate MAVEN_HOME
+# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/
+[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}"
+distributionUrlName="${distributionUrl##*/}"
+distributionUrlNameMain="${distributionUrlName%.*}"
+distributionUrlNameMain="${distributionUrlNameMain%-bin}"
+MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}"
+MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")"
+
+exec_maven() {
+ unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || :
+ exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD"
+}
+
+if [ -d "$MAVEN_HOME" ]; then
+ verbose "found existing MAVEN_HOME at $MAVEN_HOME"
+ exec_maven "$@"
+fi
+
+case "${distributionUrl-}" in
+*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;;
+*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;;
+esac
+
+# prepare tmp dir
+if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then
+ clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; }
+ trap clean HUP INT TERM EXIT
+else
+ die "cannot create temp dir"
+fi
+
+mkdir -p -- "${MAVEN_HOME%/*}"
+
+# Download and Install Apache Maven
+verbose "Couldn't find MAVEN_HOME, downloading and installing it ..."
+verbose "Downloading from: $distributionUrl"
+verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName"
+
+# select .zip or .tar.gz
+if ! command -v unzip >/dev/null; then
+ distributionUrl="${distributionUrl%.zip}.tar.gz"
+ distributionUrlName="${distributionUrl##*/}"
+fi
+
+# verbose opt
+__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR=''
+[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v
+
+# normalize http auth
+case "${MVNW_PASSWORD:+has-password}" in
+'') MVNW_USERNAME='' MVNW_PASSWORD='' ;;
+has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;;
+esac
+
+if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then
+ verbose "Found wget ... using wget"
+ wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl"
+elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then
+ verbose "Found curl ... using curl"
+ curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl"
+elif set_java_home; then
+ verbose "Falling back to use Java to download"
+ javaSource="$TMP_DOWNLOAD_DIR/Downloader.java"
+ targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName"
+ cat >"$javaSource" <<-END
+ public class Downloader extends java.net.Authenticator
+ {
+ protected java.net.PasswordAuthentication getPasswordAuthentication()
+ {
+ return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() );
+ }
+ public static void main( String[] args ) throws Exception
+ {
+ setDefault( new Downloader() );
+ java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() );
+ }
+ }
+ END
+ # For Cygwin/MinGW, switch paths to Windows format before running javac and java
+ verbose " - Compiling Downloader.java ..."
+ "$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java"
+ verbose " - Running Downloader.java ..."
+ "$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")"
+fi
+
+# If specified, validate the SHA-256 sum of the Maven distribution zip file
+if [ -n "${distributionSha256Sum-}" ]; then
+ distributionSha256Result=false
+ if [ "$MVN_CMD" = mvnd.sh ]; then
+ echo "Checksum validation is not supported for maven-mvnd." >&2
+ echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2
+ exit 1
+ elif command -v sha256sum >/dev/null; then
+ if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c >/dev/null 2>&1; then
+ distributionSha256Result=true
+ fi
+ elif command -v shasum >/dev/null; then
+ if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then
+ distributionSha256Result=true
+ fi
+ else
+ echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2
+ echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2
+ exit 1
+ fi
+ if [ $distributionSha256Result = false ]; then
+ echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2
+ echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2
+ exit 1
+ fi
+fi
+
+# unzip and move
+if command -v unzip >/dev/null; then
+ unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip"
+else
+ tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar"
+fi
+printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/mvnw.url"
+mv -- "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME"
+
+clean || :
+exec_maven "$@"
diff --git a/java-does-usb/mvnw.cmd b/java-does-usb/mvnw.cmd
new file mode 100644
index 00000000..249bdf38
--- /dev/null
+++ b/java-does-usb/mvnw.cmd
@@ -0,0 +1,149 @@
+<# : batch portion
+@REM ----------------------------------------------------------------------------
+@REM Licensed to the Apache Software Foundation (ASF) under one
+@REM or more contributor license agreements. See the NOTICE file
+@REM distributed with this work for additional information
+@REM regarding copyright ownership. The ASF licenses this file
+@REM to you under the Apache License, Version 2.0 (the
+@REM "License"); you may not use this file except in compliance
+@REM with the License. You may obtain a copy of the License at
+@REM
+@REM http://www.apache.org/licenses/LICENSE-2.0
+@REM
+@REM Unless required by applicable law or agreed to in writing,
+@REM software distributed under the License is distributed on an
+@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+@REM KIND, either express or implied. See the License for the
+@REM specific language governing permissions and limitations
+@REM under the License.
+@REM ----------------------------------------------------------------------------
+
+@REM ----------------------------------------------------------------------------
+@REM Apache Maven Wrapper startup batch script, version 3.3.2
+@REM
+@REM Optional ENV vars
+@REM MVNW_REPOURL - repo url base for downloading maven distribution
+@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven
+@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output
+@REM ----------------------------------------------------------------------------
+
+@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0)
+@SET __MVNW_CMD__=
+@SET __MVNW_ERROR__=
+@SET __MVNW_PSMODULEP_SAVE=%PSModulePath%
+@SET PSModulePath=
+@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @(
+ IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B)
+)
+@SET PSModulePath=%__MVNW_PSMODULEP_SAVE%
+@SET __MVNW_PSMODULEP_SAVE=
+@SET __MVNW_ARG0_NAME__=
+@SET MVNW_USERNAME=
+@SET MVNW_PASSWORD=
+@IF NOT "%__MVNW_CMD__%"=="" (%__MVNW_CMD__% %*)
+@echo Cannot start maven from wrapper >&2 && exit /b 1
+@GOTO :EOF
+: end batch / begin powershell #>
+
+$ErrorActionPreference = "Stop"
+if ($env:MVNW_VERBOSE -eq "true") {
+ $VerbosePreference = "Continue"
+}
+
+# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties
+$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl
+if (!$distributionUrl) {
+ Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties"
+}
+
+switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) {
+ "maven-mvnd-*" {
+ $USE_MVND = $true
+ $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip"
+ $MVN_CMD = "mvnd.cmd"
+ break
+ }
+ default {
+ $USE_MVND = $false
+ $MVN_CMD = $script -replace '^mvnw','mvn'
+ break
+ }
+}
+
+# apply MVNW_REPOURL and calculate MAVEN_HOME
+# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/
+if ($env:MVNW_REPOURL) {
+ $MVNW_REPO_PATTERN = if ($USE_MVND) { "/org/apache/maven/" } else { "/maven/mvnd/" }
+ $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace '^.*'+$MVNW_REPO_PATTERN,'')"
+}
+$distributionUrlName = $distributionUrl -replace '^.*/',''
+$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$',''
+$MAVEN_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain"
+if ($env:MAVEN_USER_HOME) {
+ $MAVEN_HOME_PARENT = "$env:MAVEN_USER_HOME/wrapper/dists/$distributionUrlNameMain"
+}
+$MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join ''
+$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME"
+
+if (Test-Path -Path "$MAVEN_HOME" -PathType Container) {
+ Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME"
+ Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"
+ exit $?
+}
+
+if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) {
+ Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl"
+}
+
+# prepare tmp dir
+$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile
+$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir"
+$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null
+trap {
+ if ($TMP_DOWNLOAD_DIR.Exists) {
+ try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
+ catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
+ }
+}
+
+New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null
+
+# Download and Install Apache Maven
+Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..."
+Write-Verbose "Downloading from: $distributionUrl"
+Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName"
+
+$webclient = New-Object System.Net.WebClient
+if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) {
+ $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD)
+}
+[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
+$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null
+
+# If specified, validate the SHA-256 sum of the Maven distribution zip file
+$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum
+if ($distributionSha256Sum) {
+ if ($USE_MVND) {
+ Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties."
+ }
+ Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash
+ if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) {
+ Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property."
+ }
+}
+
+# unzip and move
+Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null
+Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -NewName $MAVEN_HOME_NAME | Out-Null
+try {
+ Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null
+} catch {
+ if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) {
+ Write-Error "fail to move MAVEN_HOME"
+ }
+} finally {
+ try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
+ catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
+}
+
+Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"
diff --git a/java-does-usb/pom.xml b/java-does-usb/pom.xml
index 5b6c4ace..e8ad6aec 100644
--- a/java-does-usb/pom.xml
+++ b/java-does-usb/pom.xml
@@ -6,14 +6,17 @@
net.codecrete.usbjava-does-usb
- 0.3.0
+ 1.3.1-SNAPSHOT
- 19
- 19
+ 25
+ 25UTF-8
+ 0.8.0
+ jar
+
Java Does USBhttps://github.com/manuelbl/JavaDoesUSBAccess USB devices from Java without additional libraries
@@ -38,42 +41,113 @@
https://github.com/manuelbl/JavaDoesUSB
-
-
- ossrh
- https://oss.sonatype.org/content/repositories/snapshots
-
-
- ossrh
- https://oss.sonatype.org/service/local/staging/deploy/maven2/
-
-
-
+
+ net.codecrete.windows-api
+ windowsapi-maven-plugin
+
+
+
+ windows-api
+
+
+
+ CLSIDFromString
+ CreateFileW
+ CreateIoCompletionPort
+ CreateWindowExW
+ DefWindowProcW
+ DeviceIoControl
+ FormatMessageW
+ GetMessageW
+ GetModuleHandleW
+ GetQueuedCompletionStatus
+ LocalFree
+ RegCloseKey
+ RegQueryValueExW
+ RegisterClassExW
+ RegisterDeviceNotificationW
+ SetupDiCreateDeviceInfoList
+ SetupDiDeleteDeviceInterfaceData
+ SetupDiDestroyDeviceInfoList
+ SetupDiEnumDeviceInfo
+ SetupDiEnumDeviceInterfaces
+ SetupDiGetClassDevsW
+ SetupDiGetDeviceInterfaceDetailW
+ SetupDiGetDevicePropertyW
+ SetupDiOpenDevRegKey
+ SetupDiOpenDeviceInfoW
+ SetupDiOpenDeviceInterfaceW
+ WinUsb_AbortPipe
+ WinUsb_Free
+ WinUsb_GetAssociatedInterface
+ WinUsb_Initialize
+ WinUsb_ReadPipe
+ WinUsb_ResetPipe
+ WinUsb_SetCurrentAlternateSetting
+ WinUsb_SetPipePolicy
+ WinUsb_WritePipe
+
+
+ DEV_BROADCAST_DEVICEINTERFACE_W
+ DEV_BROADCAST_HDR
+ USB_DESCRIPTOR_REQUEST
+ USB_NODE_CONNECTION_INFORMATION_EX
+
+
+ DEV_BROADCAST_HDR_DEVICE_TYPE
+ FORMAT_MESSAGE_OPTIONS
+ GENERIC_ACCESS_RIGHTS
+ REG_SAM_FLAGS
+ SETUP_DI_PROPERTY_CHANGE_SCOPE
+
+
+ DBT_DEVICEARRIVAL
+ DBT_DEVICEREMOVECOMPLETE
+ DEVPKEY_Device_Address
+ DEVPKEY_Device_Children
+ DEVPKEY_Device_HardwareIds
+ DEVPKEY_Device_InstanceId
+ DEVPKEY_Device_Parent
+ DEVPKEY_Device_Service
+ DIREG_DEV
+ GUID_DEVINTERFACE_USB_DEVICE
+ GUID_DEVINTERFACE_USB_HUB
+ HWND_MESSAGE
+ INFINITE
+ IOCTL_USB_GET_DESCRIPTOR_FROM_NODE_CONNECTION
+ IOCTL_USB_GET_NODE_CONNECTION_INFORMATION_EX
+ STATUS_UNSUCCESSFUL
+ USB_REQUEST_GET_DESCRIPTOR
+ WM_DEVICECHANGE
+
+
+
+
+ org.apache.maven.pluginsmaven-compiler-plugin
- 3.10.1
+ 3.12.1
- 19
- --enable-preview
- 19
- 19
+ 25
+ 25
+ 25org.apache.maven.pluginsmaven-surefire-plugin
- 3.0.0-M7
+ 3.2.5
- --enable-preview --enable-native-access=ALL-UNNAMED
+ --enable-native-access=ALL-UNNAMEDorg.apache.maven.pluginsmaven-javadoc-plugin
- 3.4.1
+ 3.6.3attach-javadocs
@@ -83,15 +157,15 @@
- 19
- --enable-preview
+ 25${java.home}/bin/javadoc
+ net.codecrete.usb.linux.gen.*:net.codecrete.usb.macos.gen.*:windows.*:systemorg.apache.maven.pluginsmaven-gpg-plugin
- 3.0.1
+ 3.1.0sign-artifacts
@@ -105,7 +179,7 @@
org.apache.maven.pluginsmaven-source-plugin
- 3.2.1
+ 3.3.0attach-sources
@@ -115,30 +189,58 @@
+
+ org.sonatype.central
+ central-publishing-maven-plugin
+ ${sonatype-central-publishing.version}
+ true
+
+ central
+ true
+
+
- org.sonatype.plugins
- nexus-staging-maven-plugin
- 1.6.13
- true
-
- ossrh
- https://s01.oss.sonatype.org/
- true
-
+ net.codecrete.windows-api
+ windowsapi-maven-plugin
+ 0.8.5
+
+ org.jetbrains
+ annotations
+ 24.1.0
+ compile
+ org.junit.jupiterjunit-jupiter
- 5.9.0
+ 5.10.2
+ test
+
+
+ org.assertj
+ assertj-core
+ 3.25.3
+ test
+
+
+ org.tinylog
+ tinylog-impl
+ 2.7.0
+ test
+
+
+ org.tinylog
+ jsl-tinylog
+ 2.7.0test
diff --git a/java-does-usb/src/main/java/module-info.java b/java-does-usb/src/main/java/module-info.java
index cf4daec9..496c87ec 100644
--- a/java-does-usb/src/main/java/module-info.java
+++ b/java-does-usb/src/main/java/module-info.java
@@ -9,5 +9,6 @@
* Java Does USB – work with USB devices
*/
module net.codecrete.usb {
+ requires org.jetbrains.annotations;
exports net.codecrete.usb;
}
diff --git a/java-does-usb/src/main/java/net/codecrete/usb/USB.java b/java-does-usb/src/main/java/net/codecrete/usb/USB.java
deleted file mode 100644
index d39007c0..00000000
--- a/java-does-usb/src/main/java/net/codecrete/usb/USB.java
+++ /dev/null
@@ -1,126 +0,0 @@
-//
-// Java Does USB
-// Copyright (c) 2022 Manuel Bleichenbacher
-// Licensed under MIT License
-// https://opensource.org/licenses/MIT
-//
-
-package net.codecrete.usb;
-
-import net.codecrete.usb.common.USBDeviceRegistry;
-import net.codecrete.usb.linux.LinuxUSBDeviceRegistry;
-import net.codecrete.usb.macos.MacosUSBDeviceRegistry;
-import net.codecrete.usb.windows.WindowsUSBDeviceRegistry;
-
-import java.util.List;
-import java.util.function.Consumer;
-import java.util.stream.Collectors;
-
-/**
- * Provides access to USB devices.
- */
-public class USB {
-
- private static USBDeviceRegistry createInstance() {
- String osName = System.getProperty("os.name");
- String osArch = System.getProperty("os.arch");
-
- USBDeviceRegistry impl;
- if (osName.equals("Mac OS X") && (osArch.equals("x86_64") || osArch.equals("aarch64"))) {
- impl = new MacosUSBDeviceRegistry();
- } else if (osName.startsWith("Windows") && osArch.equals("amd64")) {
- impl = new WindowsUSBDeviceRegistry();
- } else if (osName.equals("Linux") && (osArch.equals("amd64") || osArch.equals("aarch64"))) {
- impl = new LinuxUSBDeviceRegistry();
- } else {
- throw new UnsupportedOperationException(String.format("JavaCanDoUsb is not implemented for architecture " + "%s/%s", osName, osArch));
- }
- return impl;
- }
-
- private static USBDeviceRegistry _instance = null;
-
- private static synchronized USBDeviceRegistry instance() {
- if (_instance == null) {
- _instance = createInstance();
- _instance.start();
- }
- return _instance;
- }
-
- // Private, so no instance can be created
- private USB() {
- }
-
- /**
- * Gets a list of all connected USB devices.
- *
- *
- * Depending on the operating system, the list might or might not include
- * USB hubs and USB host controllers.
- *
- *
- * @return list of USB devices
- */
- public static List getAllDevices() {
- return instance().getAllDevices();
- }
-
- /**
- * Gets a list of connected USB devices matching the specified filter.
- *
- * @param filter device filter
- * @return list of USB devices
- */
- public static List getDevices(USBDeviceFilter filter) {
- return instance().getAllDevices().stream().filter(filter::matches).collect(Collectors.toList());
- }
-
- /**
- * Gets a list of connected USB devices matching any of the specified filters.
- *
- * @param filters list of device filters
- * @return list of USB devices
- */
- public static List getDevices(List filters) {
- return instance().getAllDevices().stream().filter(dev -> USBDeviceFilter.matchesAny(dev, filters)).collect(Collectors.toList());
- }
-
- /**
- * Gets the first connected USB device matching the specified filter.
- *
- * @param filter device filter
- * @return USB device, or {@code null} if no device matches
- */
- public static USBDevice getDevice(USBDeviceFilter filter) {
- return instance().getAllDevices().stream().filter(filter::matches).findFirst().orElse(null);
- }
-
- /**
- * Gets the first connected USB device matching any of the specified filters.
- *
- * @param filters list of device filters
- * @return USB device, or {@code null} if no device matches
- */
- public static USBDevice getDevice(List filters) {
- return instance().getAllDevices().stream().filter(dev -> USBDeviceFilter.matchesAny(dev, filters)).findFirst().orElse(null);
- }
-
- /**
- * Sets the handler to be called when a USB device is connected.
- *
- * @param handler handler function, or {@code null} to remove a previous handler
- */
- public static void setOnDeviceConnected(Consumer handler) {
- instance().setOnDeviceConnected(handler);
- }
-
- /**
- * Sets the handler to be called when a USB device is disconnected.
- *
- * @param handler handler function, or {@code null} to remove a previous handler
- */
- public static void setOnDeviceDisconnected(Consumer handler) {
- instance().setOnDeviceDisconnected(handler);
- }
-}
diff --git a/java-does-usb/src/main/java/net/codecrete/usb/USBControlTransfer.java b/java-does-usb/src/main/java/net/codecrete/usb/USBControlTransfer.java
deleted file mode 100644
index ba5678f1..00000000
--- a/java-does-usb/src/main/java/net/codecrete/usb/USBControlTransfer.java
+++ /dev/null
@@ -1,24 +0,0 @@
-//
-// Java Does USB
-// Copyright (c) 2022 Manuel Bleichenbacher
-// Licensed under MIT License
-// https://opensource.org/licenses/MIT
-//
-
-package net.codecrete.usb;
-
-/**
- * USB control transfer parameters.
- *
- * See USB specification for additional information
- *
- *
- * @param requestType request type
- * @param recipient recipient
- * @param request request code
- * @param value value
- * @param index index
- */
-public record USBControlTransfer(USBRequestType requestType, USBRecipient recipient, byte request, short value,
- short index) {
-}
diff --git a/java-does-usb/src/main/java/net/codecrete/usb/USBDevice.java b/java-does-usb/src/main/java/net/codecrete/usb/USBDevice.java
deleted file mode 100644
index 5b664945..00000000
--- a/java-does-usb/src/main/java/net/codecrete/usb/USBDevice.java
+++ /dev/null
@@ -1,210 +0,0 @@
-//
-// Java Does USB
-// Copyright (c) 2022 Manuel Bleichenbacher
-// Licensed under MIT License
-// https://opensource.org/licenses/MIT
-//
-
-package net.codecrete.usb;
-
-import java.util.List;
-
-/**
- * USB device.
- *
- * In order to make control requests and transfer data, the device must be
- * opened and an interface must be claimed. In the open state, this current
- * process has exclusive access to the device.
- *
- *
- * Information about the device can be queried in both the open and the
- * closed state.
- *
- */
-public interface USBDevice {
-
- /**
- * USB product ID.
- *
- * @return product ID
- */
- int productId();
-
- /**
- * USB vendor ID.
- *
- * @return vendor ID
- */
- int vendorId();
-
- /**
- * Product name.
- *
- * @return product name or {@code null} if not provided by the device
- */
- String product();
-
- /**
- * Manufacturer name
- *
- * @return manufacturer name or {@code null} if not provided by the device
- */
- String manufacturer();
-
- /**
- * Serial number
- *
- * Even though this is supposed to be a human-readable string,
- * some devices are known to provide binary data.
- *
- *
- * @return serial number or {@code null} if not provided by the device
- */
- String serialNumber();
-
- /**
- * USB device class code ({@code bDeviceClass} from device descriptor).
- *
- * @return class code
- */
- int classCode();
-
- /**
- * USB device subclass code ({@code bDeviceSubClass} from device descriptor).
- *
- * @return subclass code
- */
- int subclassCode();
-
- /**
- * USB device protocol ({@code bDeviceProtocol} from device descriptor).
- *
- * @return protocol code
- */
- int protocolCode();
-
- /**
- * USB protocol version supported by this device.
- *
- * @return version
- */
- Version usbVersion();
-
- /**
- * Device version (as declared by the manufacturer).
- *
- * @return version
- */
- Version deviceVersion();
-
- /**
- * Opens the device for communication.
- */
- void open();
-
- /**
- * Indicates if the device is open.
- *
- * @return {@code true} if the device is open, {@code false} if it is closed.
- */
- boolean isOpen();
-
- /**
- * Closes the device.
- */
- void close();
-
- /**
- * Gets the interfaces of this device.
- *
- * @return a list of USB interfaces
- */
- List interfaces();
-
- /**
- * Claims the specified interface for exclusive use.
- *
- * @param interfaceNumber the interface number
- */
- void claimInterface(int interfaceNumber);
-
- /**
- * Releases the specified interface from exclusive use.
- *
- * @param interfaceNumber the interface number
- */
- void releaseInterface(int interfaceNumber);
-
- /**
- * Requests data from the control endpoint.
- *
- * This method blocks until the device has responded or an error has occurred.
- *
- *
- * The control transfer request is sent to endpoint 0. The transfer is expected to
- * have a Data In stage.
- *
- *
- * Requests with an interface or an endpoint as recipient are expected to
- * have the interface and endpoint number, respectively, in the lower byte of
- * {@code wIndex}. This convention is enforced by Windows. The addressed interface
- * or the interface of the addressed endpoint must have been claimed.
- *
- *
- * @param setup control transfer setup parameters
- * @param length maximum length of expected data
- * @return received data.
- */
- byte[] controlTransferIn(USBControlTransfer setup, int length);
-
- /**
- * Executes a control transfer request and optionally sends data.
- *
- * This method blocks until the device has acknowledge the request or an error has occurred.
- *
- *
- * The control transfer request is sent to endpoint 0. The transfer is expected to either have
- * no data stage or a Data Out stage.
- *
- *
- * Requests with an interface or an endpoint as recipient are expected to
- * have the interface and endpoint number, respectively, in the lower byte of
- * {@code wIndex}. This convention is enforced by Windows. The addressed interface
- * or the interface of the addressed endpoint must have been claimed.
- *
- *
- * @param setup control transfer setup parameters
- * @param data data to send, or {@code null} if the transfer has no data stage.
- */
- void controlTransferOut(USBControlTransfer setup, byte[] data);
-
- /**
- * Sends data to this device.
- *
- * This method blocks until the data has been sent or an error has occurred.
- *
- *
- * This method can send data to bulk and interrupt endpoints.
- *
- *
- * @param endpointNumber endpoint number (in the range between 1 and 127)
- * @param data data to send
- */
- void transferOut(int endpointNumber, byte[] data);
-
- /**
- * Receives data from this device.
- *
- * This method blocks until at least a packet has been received or an error has occurred.
- * The minimum value for {@code maxLength} is the maximum size of packets sent on the endpoint.
- *
- *
- * This method can receive data from bulk and interrupt endpoints.
- *
- *
- * @param endpointNumber endpoint number (in the range between 1 and 127, i.e. without the direction bit)
- * @param maxLength the maximum data length to receive (in number of bytes)
- * @return received data
- */
- byte[] transferIn(int endpointNumber, int maxLength);
-}
diff --git a/java-does-usb/src/main/java/net/codecrete/usb/USBDeviceFilter.java b/java-does-usb/src/main/java/net/codecrete/usb/USBDeviceFilter.java
deleted file mode 100644
index b7f784d0..00000000
--- a/java-does-usb/src/main/java/net/codecrete/usb/USBDeviceFilter.java
+++ /dev/null
@@ -1,199 +0,0 @@
-//
-// Java Does USB
-// Copyright (c) 2022 Manuel Bleichenbacher
-// Licensed under MIT License
-// https://opensource.org/licenses/MIT
-//
-
-package net.codecrete.usb;
-
-import java.util.List;
-
-/**
- * Filter condition for matching USB devices.
- *
- * In order to match this condition, all non-null properties
- * of this instance must be equal to the same properties of the USB device.
- *
- *
- * For a well implemented USB device, the combination of vendor ID,
- * product ID and serial number is globally unique.
- *
- */
-public class USBDeviceFilter {
- private Integer vendorId_;
- private Integer productId_;
- private Integer classCode_;
- private Integer subclassCode_;
- private Integer protocolCode_;
- private String serialNumber_;
-
- /**
- * Creates a new instance.
- */
- public USBDeviceFilter() {
- }
-
- /**
- * Creates a new instance that matches the specified vendor and product ID.
- *
- * @param vendorId vendor ID
- * @param productId product ID
- */
- public USBDeviceFilter(int vendorId, int productId) {
- vendorId_ = vendorId;
- productId_ = productId;
- }
-
- /**
- * Creates a new instance that matches the specified vendor ID, product ID and serial number.
- *
- * @param vendorId vendor ID
- * @param productId product ID
- * @param serialNumber serial number
- */
- public USBDeviceFilter(int vendorId, int productId, String serialNumber) {
- vendorId_ = vendorId;
- productId_ = productId;
- serialNumber_ = serialNumber;
- }
-
- /**
- * Gets the USB vendor ID.
- *
- * @return vendor ID, or {@code null} if the vendor ID is not relevant for matching
- */
- public Integer vendorId() {
- return vendorId_;
- }
-
- /**
- * Sets the USB vendor ID.
- *
- * @param vendorId vendor ID, or {@code null} if the vendor ID is not relevant for matching
- */
- public void setVendorId(Integer vendorId) {
- vendorId_ = vendorId;
- }
-
- /**
- * Gets the USB product ID.
- *
- * @return product ID, or {@code null} if the product ID is not relevant for matching
- */
- public Integer productId() {
- return productId_;
- }
-
- /**
- * Sets the USB product ID.
- *
- * @param productId product ID, or {@code null} if the product ID is not relevant for matching
- */
- public void setProductId(Integer productId) {
- productId_ = productId;
- }
-
- /**
- * Gets the USB device class code.
- *
- * @return class code, or {@code null} if the class code is not relevant for matching
- */
- public Integer classCode() {
- return classCode_;
- }
-
- /**
- * Sets the USB device class code.
- *
- * @param classCode class code, or {@code null} if the class code is not relevant for matching
- */
- public void setClassCode(Integer classCode) {
- classCode_ = classCode;
- }
-
- /**
- * Gets the USB device subclass code.
- *
- * @return subclass code, or {@code null} if the subclass code is not relevant for matching
- */
- public Integer subclassCode() {
- return subclassCode_;
- }
-
- /**
- * Sets the USB device subclass code.
- *
- * @param subclassCode subclass code, or {@code null} if the subclass code is not relevant for matching
- */
- public void setSubclassCode(Integer subclassCode) {
- subclassCode_ = subclassCode;
- }
-
- /**
- * Gets the USB device protocol code.
- *
- * @return protocol code, or {@code null} if the protocol code is not relevant for matching
- */
- public Integer protocolCode() {
- return protocolCode_;
- }
-
- /**
- * Sets the USB device protocol code.
- *
- * @param protocolCode protocol code, or {@code null} if the protocol code is not relevant for matching
- */
- public void setProtocolCode_(Integer protocolCode) {
- protocolCode_ = protocolCode;
- }
-
- /**
- * Gets the device serial number.
- *
- * @return serial number, or {@code null} if the serial number is not relevant for matching
- */
- public String serialNumber() {
- return serialNumber_;
- }
-
- /**
- * Sets the device serial number.
- *
- * @param serialNumber serial number, or {@code null} if the serial number is not relevant for matching
- */
- public void setSerialNumber(String serialNumber) {
- serialNumber_ = serialNumber;
- }
-
- /**
- * Tests if the specified USB device matches this filter.
- *
- * @param device USB device
- * @return {@code true} if it matches, {@code false} otherwise
- */
- public boolean matches(USBDevice device) {
- if (vendorId_ != null && device.vendorId() != vendorId_)
- return false;
- if (productId_ != null && device.productId() != productId_)
- return false;
- if (serialNumber_ != null && !serialNumber_.equals(device.serialNumber()))
- return false;
- if (classCode_ != null && device.classCode() != classCode_)
- return false;
- if (subclassCode_ != null && device.subclassCode() != subclassCode_)
- return false;
- return protocolCode_ == null || device.protocolCode() == protocolCode_;
- }
-
- /**
- * Test if the USB devices matches any of the filter conditions.
- *
- * @param device the USB device
- * @param filters a list of filter conditions
- * @return {@code true} if it matches, {@code false} otherwise
- */
- public static boolean matchesAny(USBDevice device, List filters) {
- return filters.stream().anyMatch(filter -> filter.matches(device));
- }
-}
diff --git a/java-does-usb/src/main/java/net/codecrete/usb/Usb.java b/java-does-usb/src/main/java/net/codecrete/usb/Usb.java
new file mode 100644
index 00000000..b17fc604
--- /dev/null
+++ b/java-does-usb/src/main/java/net/codecrete/usb/Usb.java
@@ -0,0 +1,153 @@
+//
+// Java Does USB
+// Copyright (c) 2022 Manuel Bleichenbacher
+// Licensed under MIT License
+// https://opensource.org/licenses/MIT
+//
+
+package net.codecrete.usb;
+
+import net.codecrete.usb.common.UsbDeviceRegistry;
+import net.codecrete.usb.linux.LinuxUsbDeviceRegistry;
+import net.codecrete.usb.macos.MacosUsbDeviceRegistry;
+import net.codecrete.usb.windows.WindowsUsbDeviceRegistry;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.jetbrains.annotations.Unmodifiable;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+import java.util.function.Consumer;
+
+/**
+ * Provides access to USB devices.
+ */
+public class Usb {
+
+ @SuppressWarnings("java:S1192")
+ private static UsbDeviceRegistry createInstance() {
+ var osName = System.getProperty("os.name");
+ var osArch = System.getProperty("os.arch");
+
+ UsbDeviceRegistry impl;
+ if (osName.equals("Mac OS X") && (osArch.equals("x86_64") || osArch.equals("aarch64"))) {
+ impl = new MacosUsbDeviceRegistry();
+ } else if (osName.startsWith("Windows") && (osArch.equals("amd64") || osArch.equals("aarch64"))) {
+ impl = new WindowsUsbDeviceRegistry();
+ } else if (osName.equals("Linux") && (osArch.equals("amd64") || osArch.equals("aarch64"))) {
+ impl = new LinuxUsbDeviceRegistry();
+ } else {
+ throw new UnsupportedOperationException(String.format(
+ "The \"Java Does USB\" library has no implementation for JRE/JDK %s/%s",
+ osName, osArch));
+ }
+ return impl;
+ }
+
+ private static UsbDeviceRegistry singletonInstance = null;
+
+ private static synchronized UsbDeviceRegistry instance() {
+ if (singletonInstance == null) {
+ singletonInstance = createInstance();
+ singletonInstance.start();
+ }
+ return singletonInstance;
+ }
+
+ // Private, so no instance can be created
+ private Usb() {
+ }
+
+ /**
+ * Gets a list of all connected USB devices.
+ *
+ *
+ * Depending on the operating system, the list might or might not include
+ * USB hubs and USB host controllers.
+ *
+ *
+ * @return list of USB devices
+ */
+ public static @NotNull @Unmodifiable Collection getDevices() {
+ return Collections.unmodifiableCollection(instance().getAllDevices());
+ }
+
+ /**
+ * Gets a list of connected USB devices matching the specified predicate.
+ *
+ * @param predicate device predicate
+ * @return list of USB devices
+ */
+ public static @NotNull @Unmodifiable List findDevices(@NotNull UsbDevicePredicate predicate) {
+ return instance().getAllDevices().stream().filter(predicate::matches).toList();
+ }
+
+ /**
+ * Gets the first connected USB device matching the specified predicate.
+ *
+ * @param predicate device predicate
+ * @return optional USB device
+ */
+ public static Optional findDevice(@NotNull UsbDevicePredicate predicate) {
+ return instance().getAllDevices().stream().filter(predicate::matches).findFirst();
+ }
+
+ /**
+ * Gets the first connected USB device with the specified vendor and product ID.
+ *
+ * @param vendorId vendor ID
+ * @param productId product ID
+ * @return optional USB device
+ */
+ public static Optional findDevice(int vendorId, int productId) {
+ return findDevice(device -> device.getVendorId() == vendorId && device.getProductId() == productId);
+ }
+
+ /**
+ * Sets the handler to be called when a USB device is connected.
+ *
+ * The handler is called from a background thread.
+ *
+ *
+ * The handler should not execute any time-consuming operations but rather return quickly.
+ * While the handler is being executed, maintaining the list of connected devices is paused,
+ * methods of this class (such as {@link #getDevices()}) will possibly work with an outdated list
+ * of connected devices and handlers for connect and disconnect events will not be called.
+ *
+ *
+ * @param handler handler function, or {@code null} to remove a previous handler
+ */
+ public static void setOnDeviceConnected(@Nullable Consumer handler) {
+ instance().setOnDeviceConnected(handler);
+ }
+
+ /**
+ * Sets the handler to be called when a USB device is disconnected.
+ *
+ * The handler is called from a background thread.
+ *
+ *
+ * When the handler is called, the {@link UsbDevice} instance has already been closed.
+ * Descriptive information (such as vendor and product ID, serial number, interfaces, endpoints)
+ * can still be accessed.
+ *
+ *
+ * If the application was communicating with the device when it was disconnected, it will also receive
+ * an error for those operations. Due to the concurrency of the USB stack, there is no particular order
+ * for the disconnect event and the transmission errors.
+ *
+ *
+ * The handler should not execute any time-consuming operations but rather return quickly.
+ * While the handler is being executed, maintaining the list of connected devices is paused,
+ * methods of this class (such as {@link #getDevices()}) will possibly work with an outdated list
+ * of connected devices and handlers for connect and disconnect events will not be called.
+ *
+ *
+ * @param handler handler function, or {@code null} to remove a previous handler
+ */
+ public static void setOnDeviceDisconnected(@Nullable Consumer handler) {
+ instance().setOnDeviceDisconnected(handler);
+ }
+}
diff --git a/java-does-usb/src/main/java/net/codecrete/usb/USBAlternateInterface.java b/java-does-usb/src/main/java/net/codecrete/usb/UsbAlternateInterface.java
similarity index 59%
rename from java-does-usb/src/main/java/net/codecrete/usb/USBAlternateInterface.java
rename to java-does-usb/src/main/java/net/codecrete/usb/UsbAlternateInterface.java
index 40041360..835f68aa 100644
--- a/java-does-usb/src/main/java/net/codecrete/usb/USBAlternateInterface.java
+++ b/java-does-usb/src/main/java/net/codecrete/usb/UsbAlternateInterface.java
@@ -7,6 +7,9 @@
package net.codecrete.usb;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Unmodifiable;
+
import java.util.List;
/**
@@ -15,16 +18,17 @@
* Instances of this class describe an alternate setting of a USB interface.
*
* It is equal to the {@code bAlternateSetting} field of the interface descriptor.
+ *
*
* @return the alternate setting number
*/
- int number();
+ int getNumber();
/**
* Gets the interface class.
@@ -34,7 +38,7 @@ public interface USBAlternateInterface {
*
* @return the interface class
*/
- int classCode();
+ int getClassCode();
/**
* Gets the interface subclass.
@@ -44,7 +48,7 @@ public interface USBAlternateInterface {
*
* @return the interface subclass
*/
- int subclassCode();
+ int getSubclassCode();
/**
* Gets the interface protocol.
@@ -54,16 +58,32 @@ public interface USBAlternateInterface {
*
* @return the interface protocol
*/
- int protocolCode();
+ int getProtocolCode();
/**
- * Gets the endpoints of this alternate interface settings.
+ * Gets the endpoints of this alternate interface setting.
*
* The endpoint list does not include endpoint 0, which
* is always available and reserved for control transfers.
*
+ *
+ * The returned list is sorted by endpoint number.
+ *
*
* @return a list of endpoints.
*/
- List endpoints();
+ @NotNull
+ @Unmodifiable
+ List getEndpoints();
+
+ /**
+ * Gets the endpoint with the specified number and direction.
+ *
+ * @param endpointNumber endpoint number (in the range between 1 and 127, without the direction bit)
+ * @param direction endpoint direction
+ * @return the endpoint
+ * @exception UsbException if the endpoint does not exist
+ */
+ @NotNull UsbEndpoint getEndpoint(int endpointNumber, UsbDirection direction);
+
}
diff --git a/java-does-usb/src/main/java/net/codecrete/usb/UsbControlTransfer.java b/java-does-usb/src/main/java/net/codecrete/usb/UsbControlTransfer.java
new file mode 100644
index 00000000..dbc2e042
--- /dev/null
+++ b/java-does-usb/src/main/java/net/codecrete/usb/UsbControlTransfer.java
@@ -0,0 +1,34 @@
+//
+// Java Does USB
+// Copyright (c) 2022 Manuel Bleichenbacher
+// Licensed under MIT License
+// https://opensource.org/licenses/MIT
+//
+
+package net.codecrete.usb;
+
+/**
+ * USB control transfer parameters.
+ *
+ * See description of "Setup Data" in the chapter "USB Device Requests" of the
+ * USB specification for additional information.
+ *
+ *
+ * For control requests directed to an interface or endpoint,
+ * the lower byte of {@code index} contains the interface or endpoint number.
+ *
+ *
+ * @param requestType request type (bits 5 and 6 of {@code bmRequestType})
+ * @param recipient recipient (bits 0–4 of {@code bmRequestType})
+ * @param request request code (value between 0 and 255, called {@code bRequest} in the USB specification)
+ * @param value value (value between 0 and 65535, called {@code wValue} in the USB specification)
+ * @param index index (value between 0 and 65535, called {@code wIndex} in the USB specification)
+ */
+public record UsbControlTransfer(
+ UsbRequestType requestType,
+ UsbRecipient recipient,
+ int request,
+ int value,
+ int index
+) {
+}
diff --git a/java-does-usb/src/main/java/net/codecrete/usb/UsbDevice.java b/java-does-usb/src/main/java/net/codecrete/usb/UsbDevice.java
new file mode 100644
index 00000000..786956ba
--- /dev/null
+++ b/java-does-usb/src/main/java/net/codecrete/usb/UsbDevice.java
@@ -0,0 +1,490 @@
+//
+// Java Does USB
+// Copyright (c) 2022 Manuel Bleichenbacher
+// Licensed under MIT License
+// https://opensource.org/licenses/MIT
+//
+
+package net.codecrete.usb;
+
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Unmodifiable;
+
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.List;
+
+/**
+ * USB device.
+ *
+ * In order to make control requests and transfer data, the device must be
+ * opened and an interface must be claimed. In the open state, the current
+ * process has exclusive access to the device.
+ *
+ *
+ * Information about the device can be queried in both the open and the
+ * closed state.
+ *
+ */
+public interface UsbDevice {
+
+ /**
+ * USB product ID.
+ *
+ * @return product ID
+ */
+ int getProductId();
+
+ /**
+ * USB vendor ID.
+ *
+ * @return vendor ID
+ */
+ int getVendorId();
+
+ /**
+ * Product name.
+ *
+ * @return product name or {@code null} if not provided by the device
+ */
+ String getProduct();
+
+ /**
+ * Manufacturer name.
+ *
+ * @return manufacturer name or {@code null} if not provided by the device
+ */
+ String getManufacturer();
+
+ /**
+ * Serial number.
+ *
+ * Even though this is supposed to be a human-readable string,
+ * some devices are known to provide binary data.
+ *
+ *
+ * @return serial number or {@code null} if not provided by the device
+ */
+ String getSerialNumber();
+
+ /**
+ * USB device class code ({@code bDeviceClass} from device descriptor).
+ *
+ * @return class code
+ */
+ int getClassCode();
+
+ /**
+ * USB device subclass code ({@code bDeviceSubClass} from device descriptor).
+ *
+ * @return subclass code
+ */
+ int getSubclassCode();
+
+ /**
+ * USB device protocol ({@code bDeviceProtocol} from device descriptor).
+ *
+ * @return protocol code
+ */
+ int getProtocolCode();
+
+ /**
+ * USB protocol version supported by this device.
+ *
+ * @return version
+ */
+ @NotNull Version getUsbVersion();
+
+ /**
+ * Device version (as declared by the manufacturer).
+ *
+ * @return version
+ */
+ @NotNull Version getDeviceVersion();
+
+ /**
+ * Detaches the standard operating-system drivers of this device.
+ *
+ * By detaching the standard drivers, the operating system releases the exclusive access to the device
+ * and/or some or all of the device's interfaces. This allows the application to open the device and claim
+ * interfaces. It is relevant for device and interfaces implementing standard USB classes, such as HID, CDC
+ * and mass storage.
+ *
+ *
+ * This method should be called before the device is opened. After the device has been closed,
+ * {@link #attachStandardDrivers()} should be called to restore the previous state.
+ *
+ *
+ * On macOS, all device drivers are immediately detached from the device. To execute it, the application must
+ * be run as root. Without root privileges, the method does nothing.
+ *
+ *
+ * On Linux, this method changes the behavior of {@link #claimInterface(int)} for this device. The standard drivers
+ * will be detached interface by interface when the interface is claimed.
+ *
+ *
+ * On Windows, this method does nothing. It is not possible to temporarily change the drivers.
+ *
+ */
+ void detachStandardDrivers();
+
+ /**
+ * Reattaches the standard operating-system drivers to this device.
+ *
+ * By attaching the standard drivers, the operating system claims the device and/or its interfaces if they
+ * implement standard USB classes, such as HID, CDC and mass storage. It is used to restore the state before
+ * calling {@link #detachStandardDrivers()}.
+ *
+ *
+ * This method should be called after the device has been closed.
+ *
+ *
+ * On macOS, the application must be run as root. Without root privileges, the method does nothing.
+ *
+ *
+ * On Linux, this method changes the behavior of {@link #claimInterface(int)}. Standard drivers will no longer be
+ * detached when the interface is claimed. Standard drivers are automatically reattached when the interfaces
+ * are released, at the latest when the device is closed.
+ *
+ *
+ * On Windows, this method does nothing.
+ *
+ */
+ void attachStandardDrivers();
+
+ /**
+ * Indicates if the device is connected.
+ *
+ * When a {@link UsbDevice} instance is initially returned by {@link Usb#getDevices()} and related methods,
+ * it is connected. When the user unplugs the device, the application can still hold on to instance of
+ * {@link UsbDevice} even though the actual USB device is gone. This method can be used to check if the
+ * device is still connected.
+ *
+ * @return {@code true} if the device is connected, {@code false} if it is no longer connected
+ */
+ boolean isConnected();
+
+ /**
+ * Opens the device for communication.
+ */
+ void open();
+
+ /**
+ * Indicates if the device is open.
+ *
+ * @return {@code true} if the device is open, {@code false} if it is closed.
+ */
+ boolean isOpened();
+
+ /**
+ * Closes the device.
+ */
+ void close();
+
+ /**
+ * Gets the interfaces of this device.
+ *
+ * The returned list is sorted by interface number.
+ *
+ *
+ * @return a list of USB interfaces
+ */
+ @NotNull
+ @Unmodifiable
+ List getInterfaces();
+
+ /**
+ * Gets the interface with the specified number.
+ *
+ * @param interfaceNumber the interface number
+ * @return the interface
+ * @exception UsbException if the interface does not exist
+ */
+ @NotNull UsbInterface getInterface(int interfaceNumber);
+
+ /**
+ * Gets the endpoint with the specified number.
+ *
+ * @param direction the endpoint direction
+ * @param endpointNumber the endpoint number (between 1 and 127)
+ * @return the endpoint
+ * @exception UsbException if the endpoint does not exist
+ */
+ @NotNull UsbEndpoint getEndpoint(UsbDirection direction, int endpointNumber);
+
+ /**
+ * Claims the specified interface for exclusive use.
+ *
+ * @param interfaceNumber the interface number
+ */
+ void claimInterface(int interfaceNumber);
+
+ /**
+ * Selects the alternate settings for the specified interface.
+ *
+ * The device must be open and the interface must be claimed for exclusive access.
+ *
+ *
+ * @param interfaceNumber interface number
+ * @param alternateNumber alternate setting number
+ */
+ void selectAlternateSetting(int interfaceNumber, int alternateNumber);
+
+ /**
+ * Releases the specified interface from exclusive use.
+ *
+ * @param interfaceNumber the interface number
+ */
+ void releaseInterface(int interfaceNumber);
+
+ /**
+ * Requests data from the control endpoint.
+ *
+ * This method blocks until the device has responded or an error has occurred.
+ *
+ *
+ * The control transfer request is sent to endpoint 0. The transfer is expected to
+ * have a Data In stage.
+ *
+ *
+ * Requests with an interface or an endpoint as recipient are expected to
+ * have the interface and endpoint number, respectively, in the lower byte of
+ * {@code wIndex}. This convention is enforced by Windows. The addressed interface
+ * or the interface of the addressed endpoint must have been claimed.
+ *
+ *
+ * @param transfer control transfer setup parameters
+ * @param length maximum length of expected data
+ * @return received data.
+ */
+ byte @NotNull [] controlTransferIn(@NotNull UsbControlTransfer transfer, int length);
+
+ /**
+ * Executes a control transfer request and optionally sends data.
+ *
+ * This method blocks until the device has acknowledged the request or an error has occurred.
+ *
+ *
+ * The control transfer request is sent to endpoint 0. The transfer is expected to either have
+ * no data stage or a Data Out stage.
+ *
+ *
+ * Requests with an interface or an endpoint as recipient are expected to
+ * have the interface and endpoint number, respectively, in the lower byte of
+ * {@code wIndex}. This convention is enforced by Windows. The addressed interface
+ * or the interface of the addressed endpoint must have been claimed.
+ *
+ *
+ * @param transfer control transfer setup parameters
+ * @param data data to send, or {@code null} if the transfer has no data stage.
+ */
+ void controlTransferOut(@NotNull UsbControlTransfer transfer, byte[] data);
+
+ /**
+ * Sends data to this device.
+ *
+ * This method blocks until the data has been sent or an error has occurred.
+ *
+ *
+ * This method can send data to bulk and interrupt endpoints.
+ *
+ *
+ * If the sent data length is a multiple of the packet size, it is often
+ * required to send an additional zero-length packet (ZLP) for the device
+ * to actually process the data. This method will not do it automatically.
+ *
+ *
+ * @param endpointNumber endpoint number (in the range between 1 and 127)
+ * @param data data to send
+ */
+ void transferOut(int endpointNumber, byte @NotNull [] data);
+
+ /**
+ * Sends data to this device.
+ *
+ * This method blocks until the data has been sent, the timeout period has expired
+ * or an error has occurred. If the timeout expires, a {@link UsbTimeoutException} is thrown.
+ *
+ *
+ * This method can send data to bulk and interrupt endpoints.
+ *
+ *
+ * If the sent data length is a multiple of the packet size, it is often
+ * required to send an additional zero-length packet (ZLP) for the device
+ * to actually process the data. This method will not do it automatically.
+ *
+ *
+ * @param endpointNumber the endpoint number (in the range between 1 and 127)
+ * @param data data to send
+ * @param timeout the timeout period, in milliseconds (0 for no timeout)
+ */
+ void transferOut(int endpointNumber, byte @NotNull [] data, int timeout);
+
+ /**
+ * Sends data to this device.
+ *
+ * This method blocks until the data has been sent, the timeout period has expired
+ * or an error has occurred. If the timeout expires, a {@link UsbTimeoutException} is thrown.
+ *
+ *
+ * This method can send data to bulk and interrupt endpoints.
+ *
+ *
+ * If the sent data length is a multiple of the packet size, it is often
+ * required to send an additional zero-length packet (ZLP) for the device
+ * to actually process the data. This method will not do it automatically.
+ *
+ *
+ * @param endpointNumber the endpoint number (in the range between 1 and 127)
+ * @param data buffer containing data to send
+ * @param offset offset of the first byte to send
+ * @param length number of bytes to send
+ * @param timeout the timeout period, in milliseconds (0 for no timeout)
+ */
+ void transferOut(int endpointNumber, byte @NotNull [] data, int offset, int length, int timeout);
+
+ /**
+ * Receives data from this device.
+ *
+ * This method blocks until at least a packet has been received or an error has occurred.
+ *
+ *
+ * The returned data is the payload of a packet. It can have a length of 0 if the USB device
+ * sends zero-length packets to indicate the end of a data unit.
+ *
+ *
+ * This method can receive data from bulk and interrupt endpoints.
+ *
+ *
+ * @param endpointNumber endpoint number (in the range between 1 and 127, i.e. without the direction bit)
+ * @return received data
+ */
+ byte @NotNull [] transferIn(int endpointNumber);
+
+ /**
+ * Receives data from this device.
+ *
+ * This method blocks until at least a packet has been received, the timeout period has expired
+ * or an error has occurred. If the timeout expired, a {@link UsbTimeoutException} is thrown.
+ *
+ *
+ * The returned data is the payload of a packet. It can have a length of 0 if the USB device
+ * sends zero-length packets to indicate the end of a data unit.
+ *
+ *
+ * This method can receive data from bulk and interrupt endpoints.
+ *
+ *
+ * @param endpointNumber the endpoint number (in the range between 1 and 127, i.e. without the direction bit)
+ * @param timeout the timeout period, in milliseconds (0 for no timeout)
+ * @return received data
+ */
+ byte @NotNull [] transferIn(int endpointNumber, int timeout);
+
+ /**
+ * Opens a new output stream to send data to a bulk endpoint.
+ *
+ * All data written to this output stream is sent to the specified bulk endpoint.
+ * Buffering and concurrent IO requests are used to achieve a high throughput.
+ *
+ *
+ * The stream will insert zero-length packets if {@link OutputStream#flush()} is called
+ * and the last packet size was equal to maximum packet size of the endpoint.
+ *
+ *
+ * If {@link #transferOut(int, byte[])} and an output stream or multiple output streams
+ * are used concurrently for the same endpoint, the behavior is unpredictable.
+ *
+ *
+ * @param endpointNumber bulk endpoint number (in the range between 1 and 127)
+ * @param bufferSize approximate buffer size (in bytes)
+ * @return the new output stream
+ */
+ @NotNull OutputStream openOutputStream(int endpointNumber, int bufferSize);
+
+ /**
+ * Opens a new output stream to send data to a bulk endpoint.
+ *
+ * The buffer is configured with minimal size. In all other aspects, this method
+ * works like {@link #openOutputStream(int, int)}.
+ *
+ * @param endpointNumber bulk endpoint number (in the range between 1 and 127)
+ * @return the new output stream
+ */
+ default @NotNull OutputStream openOutputStream(int endpointNumber) {
+ return openOutputStream(endpointNumber, 1);
+ }
+
+ /**
+ * Opens a new input stream to receive data from a bulk endpoint.
+ *
+ * All data received from the specified bulk endpoint can be read using this input stream.
+ * Buffering and concurrent IO requests are used to achieve a high throughput.
+ *
+ *
+ * If the buffers contain data when the stream is closed, this data will be discarded.
+ * If {@link #transferIn(int)} and an input stream or multiple input streams
+ * are used concurrently for the same endpoint, the behavior is unpredictable.
+ *
+ *
+ * @param endpointNumber bulk endpoint number (in the range between 1 and 127, i.e. without the direction bit)
+ * @param bufferSize approximate buffer size (in bytes)
+ * @return the new input stream
+ */
+ @NotNull InputStream openInputStream(int endpointNumber, int bufferSize);
+
+ /**
+ * Opens a new input stream to receive data from a bulk endpoint.
+ *
+ * The buffer is configured with minimal size. In all other aspects, this method
+ * works like {@link #openInputStream(int, int)}.
+ *
+ *
+ * @param endpointNumber bulk endpoint number (in the range between 1 and 127, i.e. without the direction bit)
+ * @return the new input stream
+ */
+ default @NotNull InputStream openInputStream(int endpointNumber) {
+ return openInputStream(endpointNumber, 1);
+ }
+
+ /**
+ * Aborts all transfers on an endpoint.
+ *
+ * This operation is not valid on the control endpoint 0.
+ *
+ *
+ * @param direction endpoint direction
+ * @param endpointNumber endpoint number (in the range between 1 and 127)
+ */
+ void abortTransfers(UsbDirection direction, int endpointNumber);
+
+ /**
+ * Clears an endpoint's halt condition.
+ *
+ * An endpoint is halted (aka stalled) if an error occurs in the communication. Before the
+ * communication can resume, the halt condition must be cleared. A halt condition can exist
+ * in a single direction only.
+ *
+ *
+ * Control endpoint 0 will never be halted.
+ *
+ *
+ * @param direction endpoint direction
+ * @param endpointNumber endpoint number (in the range between 1 and 127)
+ */
+ void clearHalt(UsbDirection direction, int endpointNumber);
+
+ /**
+ * Gets the device descriptor.
+ *
+ * @return the device descriptor (as a byte array)
+ */
+ byte @NotNull [] getDeviceDescriptor();
+
+ /**
+ * Gets the configuration descriptor.
+ *
+ * @return the configuration descriptor (as a byte array)
+ */
+ byte @NotNull [] getConfigurationDescriptor();
+}
diff --git a/java-does-usb/src/main/java/net/codecrete/usb/UsbDevicePredicate.java b/java-does-usb/src/main/java/net/codecrete/usb/UsbDevicePredicate.java
new file mode 100644
index 00000000..687db9ed
--- /dev/null
+++ b/java-does-usb/src/main/java/net/codecrete/usb/UsbDevicePredicate.java
@@ -0,0 +1,40 @@
+//
+// Java Does USB
+// Copyright (c) 2022 Manuel Bleichenbacher
+// Licensed under MIT License
+// https://opensource.org/licenses/MIT
+//
+
+package net.codecrete.usb;
+
+import org.jetbrains.annotations.NotNull;
+
+import java.util.List;
+
+/**
+ * Represents a predicate (boolean-valued function) of one argument, evaluated for a given USB device.
+ *
+ * This is a functional interface whose functional method is {@link #matches(UsbDevice)}.
+ *
+ */
+@FunctionalInterface
+public interface UsbDevicePredicate {
+ /**
+ * Evaluates this predicate on the given USB device.
+ *
+ * @param device the USB device
+ * @return {@code true} if the device matches the predicate, otherwise {@code false}
+ */
+ boolean matches(@NotNull UsbDevice device);
+
+ /**
+ * Tests whether the USB device matches any of the filter conditions.
+ *
+ * @param device the USB device
+ * @param predicates a list of filter predicates
+ * @return {@code true} if it matches, {@code false} otherwise
+ */
+ static boolean matchesAny(@NotNull UsbDevice device, @NotNull List predicates) {
+ return predicates.stream().anyMatch(predicate -> predicate.matches(device));
+ }
+}
diff --git a/java-does-usb/src/main/java/net/codecrete/usb/USBDirection.java b/java-does-usb/src/main/java/net/codecrete/usb/UsbDirection.java
similarity index 92%
rename from java-does-usb/src/main/java/net/codecrete/usb/USBDirection.java
rename to java-does-usb/src/main/java/net/codecrete/usb/UsbDirection.java
index 8e2e7981..8e561dc4 100644
--- a/java-does-usb/src/main/java/net/codecrete/usb/USBDirection.java
+++ b/java-does-usb/src/main/java/net/codecrete/usb/UsbDirection.java
@@ -10,7 +10,7 @@
/**
* USB endpoint data direction enumeration.
*/
-public enum USBDirection {
+public enum UsbDirection {
/**
* Direction OUT (host to device)
*/
diff --git a/java-does-usb/src/main/java/net/codecrete/usb/USBEndpoint.java b/java-does-usb/src/main/java/net/codecrete/usb/UsbEndpoint.java
similarity index 87%
rename from java-does-usb/src/main/java/net/codecrete/usb/USBEndpoint.java
rename to java-does-usb/src/main/java/net/codecrete/usb/UsbEndpoint.java
index 8ccd271d..1cebc06b 100644
--- a/java-does-usb/src/main/java/net/codecrete/usb/USBEndpoint.java
+++ b/java-does-usb/src/main/java/net/codecrete/usb/UsbEndpoint.java
@@ -13,7 +13,7 @@
* Instances of this class describe a USB endpoint.
*
*/
-public interface USBEndpoint {
+public interface UsbEndpoint {
/**
* Gets the USB endpoint number.
@@ -24,12 +24,12 @@ public interface USBEndpoint {
*
*
* Use this number when calling any of the transfer methods of a
- * {@link USBDevice} instace.
+ * {@link UsbDevice} instance.
*
*
* @return the endpoint number
*/
- int number();
+ int getNumber();
/**
* Gets the direction of the endpoint.
@@ -40,14 +40,14 @@ public interface USBEndpoint {
*
* @return the direction
*/
- USBDirection direction();
+ UsbDirection getDirection();
/**
* Gets the USB endpoint transfer type.
*
* @return the transfer type
*/
- USBTransferType transferType();
+ UsbTransferType getTransferType();
/**
* Gets the packet size.
@@ -58,5 +58,5 @@ public interface USBEndpoint {
*
* @return the packet size, in bytes.
*/
- int packetSize();
+ int getPacketSize();
}
diff --git a/java-does-usb/src/main/java/net/codecrete/usb/USBException.java b/java-does-usb/src/main/java/net/codecrete/usb/UsbException.java
similarity index 66%
rename from java-does-usb/src/main/java/net/codecrete/usb/USBException.java
rename to java-does-usb/src/main/java/net/codecrete/usb/UsbException.java
index 31827e62..e0c62acb 100644
--- a/java-does-usb/src/main/java/net/codecrete/usb/USBException.java
+++ b/java-does-usb/src/main/java/net/codecrete/usb/UsbException.java
@@ -7,23 +7,27 @@
package net.codecrete.usb;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
/**
* USB exception, thrown if an operation with USB devices fails.
*/
-public class USBException extends RuntimeException {
+public class UsbException extends RuntimeException {
/**
* Error code.
*/
- private int errorCode_ = -1;
+ private final int code;
/**
* Creates a new instance with a message.
*
* @param message the message
*/
- public USBException(String message) {
+ public UsbException(@NotNull String message) {
super(message);
+ code = -1;
}
/**
@@ -32,9 +36,9 @@ public USBException(String message) {
* @param message the message
* @param errorCode the error code
*/
- public USBException(String message, int errorCode) {
+ public UsbException(@NotNull String message, int errorCode) {
super(message + " (error code: " + errorCode + ")");
- errorCode_ = errorCode;
+ code = errorCode;
}
/**
@@ -43,8 +47,9 @@ public USBException(String message, int errorCode) {
* @param message the message
* @param cause the causal exception
*/
- public USBException(String message, Throwable cause) {
+ public UsbException(@NotNull String message, @Nullable Throwable cause) {
super(message, cause);
+ code = -1;
}
/**
@@ -52,7 +57,7 @@ public USBException(String message, Throwable cause) {
*
* @return the error code
*/
- public int errorCode() {
- return errorCode_;
+ public int getErrorCode() {
+ return code;
}
}
diff --git a/java-does-usb/src/main/java/net/codecrete/usb/USBInterface.java b/java-does-usb/src/main/java/net/codecrete/usb/UsbInterface.java
similarity index 50%
rename from java-does-usb/src/main/java/net/codecrete/usb/USBInterface.java
rename to java-does-usb/src/main/java/net/codecrete/usb/UsbInterface.java
index bfb5f68a..67fde72f 100644
--- a/java-does-usb/src/main/java/net/codecrete/usb/USBInterface.java
+++ b/java-does-usb/src/main/java/net/codecrete/usb/UsbInterface.java
@@ -7,6 +7,9 @@
package net.codecrete.usb;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Unmodifiable;
+
import java.util.List;
/**
@@ -15,20 +18,20 @@
* Instances of this class describe an interface of a USB device.
*
*/
-public interface USBInterface {
+public interface UsbInterface {
/**
* Gets the interface number.
*
* It is equal to the {@code bInterfaceNumber} field of the interface descriptor.
- *
+ *
*
* @return the interface number
*/
- int number();
+ int getNumber();
/**
- * Indicates if this interface is currently claimed for exclusive access.
+ * Indicates if this interface has been claimed by this program for exclusive access.
*
* @return {@code true} if it is claimed, {@code false} otherwise.
*/
@@ -37,17 +40,31 @@ public interface USBInterface {
/**
* Gets the currently selected alternate interface setting.
*
- * Initially, the alternate settings with number 0 is selected.
+ * Initially, the alternate setting with number 0 is selected.
*
*
* @return the alternate interface setting.
*/
- USBAlternateInterface alternate();
+ @NotNull UsbAlternateInterface getCurrentAlternate();
+
+ /**
+ * Gets the alternate interface setting with the specified number.
+ *
+ * @param alternateNumber alternate setting number
+ * @return alternate interface setting
+ * @throws UsbException if the alternate setting does not exist
+ */
+ @NotNull UsbAlternateInterface getAlternate(int alternateNumber);
/**
* Gets all alternate settings of this interface.
+ *
+ * The returned list is sorted by alternate setting number.
+ *
*
* @return a list of the alternate settings
*/
- List alternates();
+ @NotNull
+ @Unmodifiable
+ List getAlternates();
}
diff --git a/java-does-usb/src/main/java/net/codecrete/usb/USBRecipient.java b/java-does-usb/src/main/java/net/codecrete/usb/UsbRecipient.java
similarity index 93%
rename from java-does-usb/src/main/java/net/codecrete/usb/USBRecipient.java
rename to java-does-usb/src/main/java/net/codecrete/usb/UsbRecipient.java
index 250238a2..756027a2 100644
--- a/java-does-usb/src/main/java/net/codecrete/usb/USBRecipient.java
+++ b/java-does-usb/src/main/java/net/codecrete/usb/UsbRecipient.java
@@ -10,7 +10,7 @@
/**
* USB control transfer recipient enumeration.
*/
-public enum USBRecipient {
+public enum UsbRecipient {
/**
* USB device
*/
diff --git a/java-does-usb/src/main/java/net/codecrete/usb/USBRequestType.java b/java-does-usb/src/main/java/net/codecrete/usb/UsbRequestType.java
similarity index 93%
rename from java-does-usb/src/main/java/net/codecrete/usb/USBRequestType.java
rename to java-does-usb/src/main/java/net/codecrete/usb/UsbRequestType.java
index 05ede7e6..08dec0d6 100644
--- a/java-does-usb/src/main/java/net/codecrete/usb/USBRequestType.java
+++ b/java-does-usb/src/main/java/net/codecrete/usb/UsbRequestType.java
@@ -10,7 +10,7 @@
/**
* USB control transfer request type enumeration.
*/
-public enum USBRequestType {
+public enum UsbRequestType {
/**
* Standard request type
*/
diff --git a/java-does-usb/src/main/java/net/codecrete/usb/UsbStallException.java b/java-does-usb/src/main/java/net/codecrete/usb/UsbStallException.java
new file mode 100644
index 00000000..e60da595
--- /dev/null
+++ b/java-does-usb/src/main/java/net/codecrete/usb/UsbStallException.java
@@ -0,0 +1,32 @@
+//
+// Java Does USB
+// Copyright (c) 2022 Manuel Bleichenbacher
+// Licensed under MIT License
+// https://opensource.org/licenses/MIT
+//
+
+package net.codecrete.usb;
+
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * Exception thrown if communication on a USB endpoint fails.
+ *
+ * If a USB endpoint stalls, it is halted and the halt condition must be cleared
+ * using {@link UsbDevice#clearHalt(UsbDirection, int)} before communication can resume.
+ *
+ *
+ * If the control endpoint 0 stalls, this exception is thrown but the endpoint is not halted.
+ *
+ */
+public class UsbStallException extends UsbException {
+
+ /**
+ * Creates a new instance with a message.
+ *
+ * @param message the message
+ */
+ public UsbStallException(@NotNull String message) {
+ super(message);
+ }
+}
diff --git a/java-does-usb/src/main/java/net/codecrete/usb/UsbTimeoutException.java b/java-does-usb/src/main/java/net/codecrete/usb/UsbTimeoutException.java
new file mode 100644
index 00000000..28ae2f72
--- /dev/null
+++ b/java-does-usb/src/main/java/net/codecrete/usb/UsbTimeoutException.java
@@ -0,0 +1,25 @@
+//
+// Java Does USB
+// Copyright (c) 2022 Manuel Bleichenbacher
+// Licensed under MIT License
+// https://opensource.org/licenses/MIT
+//
+
+package net.codecrete.usb;
+
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * Exception thrown if a USB operation times out.
+ */
+public class UsbTimeoutException extends UsbException {
+
+ /**
+ * Creates a new instance with a message.
+ *
+ * @param message the message
+ */
+ public UsbTimeoutException(@NotNull String message) {
+ super(message);
+ }
+}
diff --git a/java-does-usb/src/main/java/net/codecrete/usb/USBTransferType.java b/java-does-usb/src/main/java/net/codecrete/usb/UsbTransferType.java
similarity index 82%
rename from java-does-usb/src/main/java/net/codecrete/usb/USBTransferType.java
rename to java-does-usb/src/main/java/net/codecrete/usb/UsbTransferType.java
index 3acdc2c1..f08a2e40 100644
--- a/java-does-usb/src/main/java/net/codecrete/usb/USBTransferType.java
+++ b/java-does-usb/src/main/java/net/codecrete/usb/UsbTransferType.java
@@ -10,7 +10,11 @@
/**
* USB endpoint transfer type enumeration.
*/
-public enum USBTransferType {
+public enum UsbTransferType {
+ /**
+ * Control transfer
+ */
+ CONTROL,
/**
* Bulk transfer
*/
diff --git a/java-does-usb/src/main/java/net/codecrete/usb/Version.java b/java-does-usb/src/main/java/net/codecrete/usb/Version.java
index dc4a314d..90b507f5 100644
--- a/java-does-usb/src/main/java/net/codecrete/usb/Version.java
+++ b/java-does-usb/src/main/java/net/codecrete/usb/Version.java
@@ -14,25 +14,51 @@ public final class Version {
private final int bcdVersion;
+ /**
+ * Creates a new instance.
+ *
+ * {@code bcdVersion} contains the version: the high byte is the major
+ * version. The low byte is split into two nibbles (4 bits), the high one
+ * is the minor version, the low one is the subminor version. As an example,
+ * 0x0321 represents the version 3.2.1.
+ *
+ *
+ * @param bcdVersion version, encoded as described above
+ */
public Version(int bcdVersion) {
this.bcdVersion = bcdVersion;
}
- public int major() {
+ /**
+ * Major version
+ *
+ * @return major version
+ */
+ public int getMajor() {
return bcdVersion >> 8;
}
- public int minor() {
+ /**
+ * Minor version
+ *
+ * @return minor version
+ */
+ public int getMinor() {
return (bcdVersion >> 4) & 0x0f;
}
- public int subminor() {
+ /**
+ * Subminor version
+ *
+ * @return subminor version
+ */
+ public int getSubminor() {
return bcdVersion & 0x0f;
}
@Override
public String toString() {
- return String.format("%d.%d.%d", major(), minor(), subminor());
+ return String.format("%d.%d.%d", getMajor(), getMinor(), getSubminor());
}
@Override
@@ -41,7 +67,7 @@ public boolean equals(Object o) {
return true;
if (o == null || getClass() != o.getClass())
return false;
- Version version = (Version) o;
+ var version = (Version) o;
return bcdVersion == version.bcdVersion;
}
diff --git a/java-does-usb/src/main/java/net/codecrete/usb/common/CompositeFunction.java b/java-does-usb/src/main/java/net/codecrete/usb/common/CompositeFunction.java
new file mode 100644
index 00000000..b6e751a4
--- /dev/null
+++ b/java-does-usb/src/main/java/net/codecrete/usb/common/CompositeFunction.java
@@ -0,0 +1,84 @@
+//
+// Java Does USB
+// Copyright (c) 2022 Manuel Bleichenbacher
+// Licensed under MIT License
+// https://opensource.org/licenses/MIT
+//
+
+package net.codecrete.usb.common;
+
+/**
+ * Describes a function of a composite USB device.
+ *
+ * A composite USB device can have multiple functions, e.g. a mass
+ * storage function and a virtual serial port function. Each function
+ * will appear as a separate device in Window.
+ *
+ *
+ * A function consists of one or more interfaces. Functions with
+ * multiple interfaces must have consecutive interface numbers. The
+ * interfaces after the first one are called associated interfaces.
+ *
+ */
+public class CompositeFunction {
+ private final int firstIntfNumber;
+ private final int interfaceCount;
+ private final int functionCode;
+ private final int functionSubclass;
+ private final int functionProtocol;
+
+ /**
+ * Creates a new instance.
+ *
+ * @param firstInterfaceNumber the number of the first interface
+ * @param numInterfaces the number of interfaces
+ * @param classCode the function class
+ * @param subclassCode the function subclass
+ * @param protocolCode the function protocol
+ */
+ public CompositeFunction(int firstInterfaceNumber, int numInterfaces, int classCode, int subclassCode,
+ int protocolCode) {
+ firstIntfNumber = firstInterfaceNumber;
+ interfaceCount = numInterfaces;
+ functionCode = classCode;
+ functionSubclass = subclassCode;
+ functionProtocol = protocolCode;
+ }
+
+ /**
+ * Gets the number of the first interface contained in this function.
+ * @return the interface number
+ */
+ public int firstInterfaceNumber() {
+ return firstIntfNumber;
+ }
+
+ /**
+ * Gets the number of interfaces contained in this function.
+ * @return the number of interfaces
+ */
+ public int numInterfaces() {
+ return interfaceCount;
+ }
+
+ /**
+ * Indicates if this function contains the specified interface.
+ * @param interfaceNumber the interface number
+ * @return {@code true} if it is contained, {@code false} otherwise
+ */
+ public boolean containsInterface(int interfaceNumber) {
+ return interfaceNumber >= firstIntfNumber && interfaceNumber < firstIntfNumber + interfaceCount;
+ }
+
+ public int classCode() {
+ return functionCode;
+ }
+
+ public int subclassCode() {
+ return functionSubclass;
+ }
+
+ public int protocolCode() {
+ return functionProtocol;
+ }
+}
diff --git a/java-does-usb/src/main/java/net/codecrete/usb/common/Configuration.java b/java-does-usb/src/main/java/net/codecrete/usb/common/Configuration.java
new file mode 100644
index 00000000..dc5317d1
--- /dev/null
+++ b/java-does-usb/src/main/java/net/codecrete/usb/common/Configuration.java
@@ -0,0 +1,70 @@
+//
+// Java Does USB
+// Copyright (c) 2022 Manuel Bleichenbacher
+// Licensed under MIT License
+// https://opensource.org/licenses/MIT
+//
+
+package net.codecrete.usb.common;
+
+import net.codecrete.usb.UsbInterface;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Describes a device configuration.
+ */
+public class Configuration {
+ private final List functionList;
+ private final List interfaceList;
+ private final int configurationValue;
+ private final int configurationAttributes;
+ private final int configurationMaxPower;
+
+ public Configuration(int configValue, int attributes, int maxPower) {
+ configurationValue = configValue;
+ configurationAttributes = attributes;
+ configurationMaxPower = maxPower;
+ functionList = new ArrayList<>();
+ interfaceList = new ArrayList<>();
+ }
+
+ public int configValue() {
+ return configurationValue;
+ }
+
+ public int attributes() {
+ return configurationAttributes;
+ }
+
+ public int maxPower() {
+ return configurationMaxPower;
+ }
+
+ public List interfaces() {
+ return interfaceList;
+ }
+
+ public List functions() {
+ return functionList;
+ }
+
+ public void addInterface(UsbInterface intf) {
+ interfaceList.add(intf);
+ }
+
+ public UsbInterfaceImpl findInterfaceByNumber(int number) {
+ return (UsbInterfaceImpl) interfaceList.stream().filter(intf -> intf.getNumber() == number)
+ .findFirst().orElse(null);
+ }
+
+ public void addFunction(CompositeFunction function) {
+ functionList.add(function);
+ }
+
+ public CompositeFunction findFunction(int interfaceNumber) {
+ return functionList.stream().filter(f -> f.containsInterface(interfaceNumber))
+ .findFirst().orElse(null);
+ }
+}
diff --git a/java-does-usb/src/main/java/net/codecrete/usb/common/ConfigurationParser.java b/java-does-usb/src/main/java/net/codecrete/usb/common/ConfigurationParser.java
new file mode 100644
index 00000000..f8023292
--- /dev/null
+++ b/java-does-usb/src/main/java/net/codecrete/usb/common/ConfigurationParser.java
@@ -0,0 +1,177 @@
+//
+// Java Does USB
+// Copyright (c) 2022 Manuel Bleichenbacher
+// Licensed under MIT License
+// https://opensource.org/licenses/MIT
+//
+
+package net.codecrete.usb.common;
+
+import net.codecrete.usb.UsbAlternateInterface;
+import net.codecrete.usb.UsbDirection;
+import net.codecrete.usb.UsbException;
+import net.codecrete.usb.UsbTransferType;
+import net.codecrete.usb.usbstandard.ConfigurationDescriptor;
+import net.codecrete.usb.usbstandard.EndpointDescriptor;
+import net.codecrete.usb.usbstandard.InterfaceAssociationDescriptor;
+import net.codecrete.usb.usbstandard.InterfaceDescriptor;
+
+import java.lang.foreign.MemorySegment;
+import java.lang.foreign.ValueLayout;
+import java.util.ArrayList;
+
+import static net.codecrete.usb.usbstandard.Constants.CONFIGURATION_DESCRIPTOR_TYPE;
+import static net.codecrete.usb.usbstandard.Constants.ENDPOINT_DESCRIPTOR_TYPE;
+import static net.codecrete.usb.usbstandard.Constants.INTERFACE_ASSOCIATION_DESCRIPTOR_TYPE;
+import static net.codecrete.usb.usbstandard.Constants.INTERFACE_DESCRIPTOR_TYPE;
+
+/**
+ * Parser for USB configuration descriptors.
+ *
+ *
+ * It extracts the information about endpoints, interfaces (incl. alternate interfaces) and associations
+ * between interfaces to derive the functions. Other descriptor types are ignored.
+ *
+ */
+public class ConfigurationParser {
+
+ /**
+ * Parses a USB configuration descriptor (incl. interface and endpoint descriptors)
+ *
+ * @param desc configuration descriptor
+ * @return parsed configuration data
+ */
+ public static Configuration parseConfigurationDescriptor(MemorySegment desc) {
+ var parser = new ConfigurationParser(desc);
+ return parser.parse();
+ }
+
+ private final MemorySegment descriptor;
+ private Configuration configuration;
+
+ /**
+ * Creates a new parser for USB configuration descriptors (incl. interface and endpoint descriptors)
+ *
+ * @param descriptor configuration descriptor
+ */
+ public ConfigurationParser(MemorySegment descriptor) {
+ this.descriptor = descriptor;
+ }
+
+ public Configuration parse() {
+ parseHeader();
+
+ UsbAlternateInterfaceImpl lastAlternate = null;
+ var offset = peekDescLength(0);
+
+ while (offset < descriptor.byteSize()) {
+
+ var descLength = peekDescLength(offset);
+ var descType = peekDescType(offset);
+
+ if (descType == INTERFACE_DESCRIPTOR_TYPE) {
+ var intf = parseInterface(offset);
+
+ var parent = configuration.findInterfaceByNumber(intf.getNumber());
+ if (parent != null) {
+ parent.addAlternate(intf.getCurrentAlternate());
+ } else {
+ configuration.addInterface(intf);
+ }
+ lastAlternate = (UsbAlternateInterfaceImpl) intf.getCurrentAlternate();
+
+ var function = configuration.findFunction(intf.getNumber());
+ if (function == null) {
+ function = new CompositeFunction(intf.getNumber(), 1, lastAlternate.getClassCode(),
+ lastAlternate.getSubclassCode(), lastAlternate.getProtocolCode());
+ configuration.addFunction(function);
+ }
+
+ } else if (descType == ENDPOINT_DESCRIPTOR_TYPE) {
+ var endpoint = parseEndpoint(offset);
+ if (lastAlternate != null)
+ lastAlternate.addEndpoint(endpoint);
+
+ } else if (descType == INTERFACE_ASSOCIATION_DESCRIPTOR_TYPE) {
+ parseIAD(offset);
+ }
+
+ offset += descLength;
+ }
+
+ return configuration;
+ }
+
+ private void parseHeader() {
+ var desc = new ConfigurationDescriptor(descriptor);
+ if (CONFIGURATION_DESCRIPTOR_TYPE != desc.descriptorType())
+ throw new UsbException("invalid USB configuration descriptor");
+
+ var totalLength = desc.totalLength();
+ if (descriptor.byteSize() != totalLength)
+ throw new UsbException("invalid USB configuration descriptor (invalid length)");
+
+ configuration = new Configuration(desc.configurationValue(), desc.attributes(), desc.maxPower());
+ }
+
+ private UsbInterfaceImpl parseInterface(int offset) {
+ var desc = new InterfaceDescriptor(descriptor, offset);
+ var alternate = new UsbAlternateInterfaceImpl(desc.alternateSetting(), desc.interfaceClass(),
+ desc.interfaceSubClass(), desc.interfaceProtocol(), new ArrayList<>());
+ var alternates = new ArrayList();
+ alternates.add(alternate);
+ return new UsbInterfaceImpl(desc.interfaceNumber(), alternates);
+ }
+
+ private void parseIAD(int offset) {
+ var desc = new InterfaceAssociationDescriptor(descriptor, offset);
+ var function = new CompositeFunction(desc.firstInterface(), desc.interfaceCount(), desc.functionClass(),
+ desc.functionSubClass(), desc.functionProtocol());
+ configuration.addFunction(function);
+ }
+
+ private UsbEndpointImpl parseEndpoint(int offset) {
+ var desc = new EndpointDescriptor(descriptor, offset);
+ var address = desc.endpointAddress();
+ return new UsbEndpointImpl(getEndpointNumber(address), getEndpointDirection(address),
+ getEndpointType(desc.attributes()), desc.maxPacketSize());
+ }
+
+ private static UsbDirection getEndpointDirection(int address) {
+ return (address & 0x80) != 0 ? UsbDirection.IN : UsbDirection.OUT;
+ }
+
+ private static int getEndpointNumber(int address) {
+ return address & 0x7f;
+ }
+
+ private static UsbTransferType getEndpointType(int attributes) {
+ return switch (attributes & 0x3) {
+ case 1 -> UsbTransferType.ISOCHRONOUS;
+ case 2 -> UsbTransferType.BULK;
+ case 3 -> UsbTransferType.INTERRUPT;
+ default -> null;
+ };
+ }
+
+ /**
+ * Get descriptor length.
+ *
+ * @param offset offset to the descriptor of interest
+ * @return descriptor length (in bytes)
+ */
+ private int peekDescLength(int offset) {
+ return 0xff & descriptor.get(ValueLayout.JAVA_BYTE, offset);
+ }
+
+ /**
+ * Get descriptor type.
+ *
+ * @param offset offset to the descriptor of interest
+ * @return descriptor type
+ */
+ private int peekDescType(int offset) {
+ return 0xff & descriptor.get(ValueLayout.JAVA_BYTE, offset + 1L);
+ }
+
+}
diff --git a/java-does-usb/src/main/java/net/codecrete/usb/common/DescriptorParser.java b/java-does-usb/src/main/java/net/codecrete/usb/common/DescriptorParser.java
deleted file mode 100644
index f6f7ab6c..00000000
--- a/java-does-usb/src/main/java/net/codecrete/usb/common/DescriptorParser.java
+++ /dev/null
@@ -1,167 +0,0 @@
-//
-// Java Does USB
-// Copyright (c) 2022 Manuel Bleichenbacher
-// Licensed under MIT License
-// https://opensource.org/licenses/MIT
-//
-
-package net.codecrete.usb.common;
-
-import net.codecrete.usb.*;
-
-import java.lang.foreign.MemorySegment;
-import java.lang.foreign.ValueLayout;
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * Parser for USB descriptors
- */
-public class DescriptorParser {
-
- /**
- * Parse a USB configuration descriptor (incl. interface and endpoint descriptors)
- *
- * @param desc configuration descriptor
- * @return parsed configuration data
- */
- public static Configuration parseConfigurationDescriptor(MemorySegment desc, int vendorID, int productID) {
- var config = parseConfiguration(desc);
-
- USBAlternateInterfaceImpl lastAlternate = null;
- USBEndpointImpl lastEndpoint;
- int offset = peekDescLength(desc, 0);
-
- while (offset < desc.byteSize()) {
-
- int descLength = peekDescLength(desc, offset);
- int descType = peekDescType(desc, offset);
-
- if (descType == USBDescriptors.INTERFACE_DESCRIPTOR_TYPE) {
- var intf = parseInterface(desc, offset);
- var parent = config.findInterfaceByNumber(intf.number());
- if (parent != null) {
- parent.addAlternate(intf.alternate());
- } else {
- config.addInterface(intf);
- }
- lastAlternate = (USBAlternateInterfaceImpl) intf.alternate();
-
- } else if (descType == USBDescriptors.ENDPOINT_DESCRIPTOR_TYPE) {
- lastEndpoint = parseEndpoint(desc, offset);
- if (lastAlternate != null)
- lastAlternate.addEndpoint(lastEndpoint);
-
- } else //noinspection StatementWithEmptyBody
- if (descType == USBDescriptors.INTERFACE_ASSOCIATION_DESCRIPTOR_TYPE) {
- // TODO: interface associations
-
- } else //noinspection StatementWithEmptyBody
- if (descType == USBDescriptors.HID_DESCRIPTOR_TYPE || descType == USBDescriptors.CS_INTERFACE_DESCRIPTOR_TYPE || descType == USBDescriptors.CS_ENDPOINT_DESCRIPTOR_TYPE) {
- // known descriptor but not relevant
-
- } else {
- // TODO: Remove warning once the relevant descriptors are processed
- System.err.printf("Info: [JavaDoesUSB] unsupported USB descriptor type 0x%02x of device " +
- "0x%04x/0x%04x - ignoring descriptor%n", descType, vendorID, productID);
- }
-
- offset += descLength;
- }
-
- return config;
- }
-
- private static Configuration parseConfiguration(MemorySegment descriptor) {
- var desc = descriptor.asSlice(0, USBDescriptors.Configuration.byteSize());
- var config = new Configuration();
- if (USBDescriptors.CONFIGURATION_DESCRIPTOR_TYPE != (byte) USBDescriptors.Configuration_bDescriptorType.get(desc))
- throw new USBException("Invalid USB configuration descriptor");
-
- short totalLength = (short) USBDescriptors.Configuration_wTotalLength.get(desc);
- if (descriptor.byteSize() != totalLength)
- throw new USBException("Invalid USB configuration descriptor length");
-
- config.configValue = (byte) USBDescriptors.Configuration_bConfigurationValue.get(desc);
- config.attributes = (byte) USBDescriptors.Configuration_bmAttributes.get(desc);
- config.maxPower = (byte) USBDescriptors.Configuration_bMaxPower.get(desc);
- config.interfaces = new ArrayList<>();
- return config;
- }
-
- private static USBInterfaceImpl parseInterface(MemorySegment descriptor, int offset) {
- var desc = descriptor.asSlice(offset, USBDescriptors.Interface.byteSize());
- var number = 255 & (byte) USBDescriptors.Interface_bInterfaceNumber.get(desc);
- var altSetting = 255 & (byte) USBDescriptors.Interface_bAlternateSetting.get(desc);
- var classCode = 255 & (byte) USBDescriptors.Interface_bInterfaceClass.get(desc);
- var subclassCode = 255 & (byte) USBDescriptors.Interface_bInterfaceSubClass.get(desc);
- var protocol = 255 & (byte) USBDescriptors.Interface_bInterfaceProtocol.get(desc);
- var alternate = new USBAlternateInterfaceImpl(altSetting, classCode, subclassCode, protocol, new ArrayList<>());
- var alternates = new ArrayList();
- alternates.add(alternate);
- return new USBInterfaceImpl(number, alternates);
- }
-
- private static USBEndpointImpl parseEndpoint(MemorySegment descriptor, int offset) {
- var desc = descriptor.asSlice(offset, USBDescriptors.Endpoint.byteSize());
- var address = (byte) USBDescriptors.Endpoint_bEndpointAddress.get(desc);
- var attributes = (byte) USBDescriptors.Endpoint_bmAttributes.get(desc);
- var maxPacketSize = (short) USBDescriptors.Endpoint_wMaxPacketSize.get(desc);
- return new USBEndpointImpl(getEndpointNumber(address), getEndpointDirection(address),
- getEndpointType(attributes), maxPacketSize);
- }
-
- private static USBDirection getEndpointDirection(byte address) {
- return (address & 0x80) != 0 ? USBDirection.IN : USBDirection.OUT;
- }
-
- private static int getEndpointNumber(byte address) {
- return address & 0x7f;
- }
-
- private static USBTransferType getEndpointType(byte attributes) {
- return switch (attributes & 0x3) {
- case 1 -> USBTransferType.ISOCHRONOUS;
- case 2 -> USBTransferType.BULK;
- case 3 -> USBTransferType.INTERRUPT;
- default -> null;
- };
- }
-
- /**
- * Get descriptor length.
- *
- * @param desc byte array containing multiple descriptors
- * @param offset offset to the descriptor of interest
- * @return descriptor length (in bytes)
- */
- private static int peekDescLength(MemorySegment desc, int offset) {
- return 255 & desc.get(ValueLayout.JAVA_BYTE, offset);
- }
-
- /**
- * Get descriptor type.
- *
- * @param desc byte array containing multiple descriptors
- * @param offset offset to the descriptor of interest
- * @return descriptor type
- */
- private static int peekDescType(MemorySegment desc, int offset) {
- return 255 & desc.get(ValueLayout.JAVA_BYTE, offset + 1);
- }
-
- public static class Configuration {
- public List interfaces;
- public byte configValue;
- public byte attributes;
- public byte maxPower;
-
- void addInterface(USBInterface intf) {
- interfaces.add(intf);
- }
-
- public USBInterfaceImpl findInterfaceByNumber(int number) {
- return (USBInterfaceImpl) interfaces.stream().filter((intf) -> intf.number() == number).findFirst().orElse(null);
- }
- }
-}
diff --git a/java-does-usb/src/main/java/net/codecrete/usb/common/EndpointInputStream.java b/java-does-usb/src/main/java/net/codecrete/usb/common/EndpointInputStream.java
new file mode 100644
index 00000000..167b041c
--- /dev/null
+++ b/java-does-usb/src/main/java/net/codecrete/usb/common/EndpointInputStream.java
@@ -0,0 +1,298 @@
+//
+// Java Does USB
+// Copyright (c) 2023 Manuel Bleichenbacher
+// Licensed under MIT License
+// https://opensource.org/licenses/MIT
+//
+
+package net.codecrete.usb.common;
+
+import net.codecrete.usb.UsbDirection;
+import net.codecrete.usb.UsbException;
+import org.jetbrains.annotations.NotNull;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.foreign.Arena;
+import java.lang.foreign.MemorySegment;
+import java.util.Objects;
+import java.util.concurrent.ArrayBlockingQueue;
+import java.util.concurrent.TimeUnit;
+
+import static net.codecrete.usb.common.EndpointStreams.toIOException;
+
+import static java.lang.System.Logger.Level.WARNING;
+import static java.lang.foreign.ValueLayout.JAVA_BYTE;
+
+/**
+ * Input stream for bulk endpoints – optimized for high throughput.
+ *
+ *
+ * Multiple asynchronous transfers are submitted to achieve a good
+ * degree of concurrency between the USB communication handled by the operating
+ * system and the consuming application code.
+ *
+ *
+ * For thread synchronization (between the background thread handling IO completions
+ * and the consuming application thread) a blocking queue is used. When an transfer
+ * completes, the background thread adds it to the queue. The consuming code
+ * waits for the next item in the queue.
+ *
+ */
+public abstract class EndpointInputStream extends InputStream {
+
+ private static final System.Logger LOG = System.getLogger(EndpointInputStream.class.getName());
+
+ // Maximum time (ms) to wait for outstanding transfers to complete during teardown.
+ // A completion that is never delivered (e.g. after an unplug) then degrades to a
+ // logged warning instead of a permanent hang.
+ private static final long TEARDOWN_TIMEOUT_MS = 1000;
+
+ protected UsbDeviceImpl device;
+ protected final int endpointNumber;
+ // Arena to allocate buffers and completion handlers
+ protected final Arena arena;
+ // Transfer size (multiple of packet size)
+ protected final int transferSize;
+ // Queue of completed transfers
+ private final ArrayBlockingQueue completedTransferQueue;
+ // Number of outstanding transfers (includes transfers pending with the
+ // operating system and transfers in the completed queue)
+ private int numOutstandingTransfers;
+ // Transfer and associated buffer being currently read from
+ private Transfer currentTransfer;
+ // Read offset within current transfer buffer
+ private int readOffset;
+
+ /**
+ * Creates a new instance
+ *
+ * @param device USB device
+ * @param endpointNumber endpoint number
+ * @param bufferSize approximate buffer size (in bytes)
+ */
+ protected EndpointInputStream(UsbDeviceImpl device, int endpointNumber, int bufferSize) {
+ this.device = device;
+ this.endpointNumber = endpointNumber;
+ //arena = Arena.ofShared(); // not supported by GraalVM
+ arena = Arena.ofAuto();
+
+ var packetSize = device.getEndpoint(UsbDirection.IN, endpointNumber).getPacketSize();
+
+ // use between 4 and 32 packets per transfer (256B to 2KB for FS, 2KB to 16KB for HS)
+ var numPacketsPerTransfer = (int) Math.round(Math.sqrt((double) bufferSize / packetSize));
+ numPacketsPerTransfer = Math.clamp(numPacketsPerTransfer, 4, 32);
+ transferSize = numPacketsPerTransfer * packetSize;
+
+ // use at least 2 outstanding transfers (3 in total)
+ var maxOutstandingTransfers = Math.max((bufferSize + transferSize / 2) / transferSize, 3);
+
+ configureEndpoint();
+
+ completedTransferQueue = new ArrayBlockingQueue<>(maxOutstandingTransfers);
+
+ // create all transfers, and submit them except one
+ try {
+ for (var i = 0; i < maxOutstandingTransfers; i++) {
+ final var transfer = device.createTransfer();
+ transfer.setData(arena.allocate(transferSize, 8));
+ transfer.setDataSize(transferSize);
+ transfer.setCompletion(this::onCompletion);
+
+ if (i == 0) {
+ currentTransfer = transfer;
+ } else {
+ submitTransfer(transfer);
+ }
+ }
+ } catch (Exception t) {
+ collectOutstandingTransfers();
+ throw t;
+ }
+ }
+
+ private boolean isClosed() {
+ return device == null;
+ }
+
+ @SuppressWarnings("RedundantThrows")
+ @Override
+ public void close() throws IOException {
+ if (isClosed())
+ return;
+
+ // abort all transfers on endpoint
+ try {
+ device.abortTransfers(UsbDirection.IN, endpointNumber);
+
+ } catch (UsbException _) {
+ // If aborting the transfer is not possible, the device has
+ // likely been closed or unplugged. So all outstanding
+ // transfers will terminate anyway.
+ }
+ device = null;
+
+ collectOutstandingTransfers();
+ }
+
+ @Override
+ public int read() throws IOException {
+ ensureOpen();
+
+ try {
+ if (bufferedBytes() == 0)
+ receiveMoreData();
+
+ var b = currentTransfer.data().get(JAVA_BYTE, readOffset) & 0xff;
+ readOffset += 1;
+ return b;
+
+ } catch (UsbException e) {
+ throw toIOException(e);
+ }
+ }
+
+ @Override
+ public int read(byte @NotNull [] b, int off, int len) throws IOException {
+ Objects.checkFromIndexSize(off, len, b.length);
+ ensureOpen();
+ if (len == 0)
+ return 0;
+
+ try {
+ var numRead = 0;
+ do {
+ if (bufferedBytes() == 0)
+ receiveMoreData();
+
+ // copy data to receiving buffer
+ var n = Math.min(len - numRead, currentTransfer.resultSize() - readOffset);
+ MemorySegment.copy(currentTransfer.data(), readOffset, MemorySegment.ofArray(b), (long) off + numRead, n);
+ readOffset += n;
+ numRead += n;
+
+ } while (numRead < len && hasMoreTransfers());
+
+ return numRead;
+
+ } catch (UsbException e) {
+ throw toIOException(e);
+ }
+ }
+
+ @Override
+ public int available() throws IOException {
+ ensureOpen();
+ return bufferedBytes();
+ }
+
+ // Bytes buffered in the current transfer, without a closed-stream check.
+ // Callers on the read path guard with ensureOpen() first.
+ private int bufferedBytes() {
+ return currentTransfer.resultSize() - readOffset;
+ }
+
+ private void ensureOpen() throws IOException {
+ if (isClosed())
+ throw new IOException("input stream has been closed");
+ }
+
+ private boolean hasMoreTransfers() {
+ return !completedTransferQueue.isEmpty();
+ }
+
+ private void receiveMoreData() throws IOException {
+ try {
+ // loop until non-ZLP has been received
+ do {
+ // the current transfer has no more data to process and
+ // can be submitted to read more data
+ submitTransfer(currentTransfer);
+
+ currentTransfer = waitForCompletedTransfer();
+ readOffset = 0;
+
+ // check for error
+ if (currentTransfer.resultCode() != 0)
+ device.throwOSException(currentTransfer.resultCode(), "error occurred while reading from endpoint %d",
+ endpointNumber);
+
+ } while (currentTransfer.resultSize() <= 0);
+
+ } catch (Exception t) {
+ close();
+ throw t;
+ }
+ }
+
+ private Transfer waitForCompletedTransfer() {
+ // Defer interruption: keep a local flag instead of re-asserting the interrupt
+ // inside the loop (which would make the next take() throw immediately and
+ // busy-spin). Re-assert once the completion has actually arrived.
+ var wasInterrupted = false;
+ try {
+ while (true) {
+ try {
+ var transfer = completedTransferQueue.take();
+ numOutstandingTransfers -= 1;
+ return transfer;
+ } catch (InterruptedException _) {
+ wasInterrupted = true;
+ }
+ }
+ } finally {
+ if (wasInterrupted)
+ Thread.currentThread().interrupt();
+ }
+ }
+
+ private void submitTransfer(Transfer transfer) {
+ submitTransferIn(transfer);
+ numOutstandingTransfers += 1;
+ }
+
+ private void onCompletion(Transfer transfer) {
+ completedTransferQueue.add(transfer);
+ }
+
+ @SuppressWarnings("java:S2142")
+ private void collectOutstandingTransfers() {
+ // Wait until the completion handlers have been called. This is a teardown path,
+ // so the wait is bounded: if a completion is never delivered (device unplugged,
+ // or the completion is lost in a source-removal race), abandon the transfer and
+ // log a warning instead of blocking the application thread forever.
+ var deadline = System.currentTimeMillis() + TEARDOWN_TIMEOUT_MS;
+ var wasInterrupted = false;
+
+ while (numOutstandingTransfers > 0) {
+ var remaining = deadline - System.currentTimeMillis();
+ if (remaining <= 0) {
+ LOG.log(WARNING,
+ "abandoning {0} outstanding transfer(s) during input stream teardown - no completion within {1} ms",
+ numOutstandingTransfers, TEARDOWN_TIMEOUT_MS);
+ break;
+ }
+
+ try {
+ if (completedTransferQueue.poll(remaining, TimeUnit.MILLISECONDS) != null)
+ numOutstandingTransfers -= 1;
+ } catch (InterruptedException _) {
+ // defer the interrupt: keep polling the remaining time without re-setting
+ // the flag (avoids a busy-spin), then re-assert it once we are done
+ wasInterrupted = true;
+ }
+ }
+
+ if (wasInterrupted)
+ Thread.currentThread().interrupt();
+
+ completedTransferQueue.clear();
+ currentTransfer = null;
+ //arena.close();
+ }
+
+ protected abstract void submitTransferIn(Transfer transfer);
+
+ protected void configureEndpoint() {
+ }
+}
diff --git a/java-does-usb/src/main/java/net/codecrete/usb/common/EndpointOutputStream.java b/java-does-usb/src/main/java/net/codecrete/usb/common/EndpointOutputStream.java
new file mode 100644
index 00000000..c371939e
--- /dev/null
+++ b/java-does-usb/src/main/java/net/codecrete/usb/common/EndpointOutputStream.java
@@ -0,0 +1,414 @@
+//
+// Java Does USB
+// Copyright (c) 2023 Manuel Bleichenbacher
+// Licensed under MIT License
+// https://opensource.org/licenses/MIT
+//
+
+package net.codecrete.usb.common;
+
+import net.codecrete.usb.UsbDirection;
+import net.codecrete.usb.UsbException;
+import org.jetbrains.annotations.NotNull;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.lang.foreign.Arena;
+import java.lang.foreign.MemorySegment;
+import java.util.Arrays;
+import java.util.Objects;
+import java.util.concurrent.ArrayBlockingQueue;
+import java.util.concurrent.TimeUnit;
+
+import static net.codecrete.usb.common.EndpointStreams.toIOException;
+
+import static java.lang.System.Logger.Level.WARNING;
+import static java.lang.foreign.ValueLayout.JAVA_BYTE;
+
+/**
+ * Output stream for bulk endpoints – optimized for high throughput.
+ *
+ *
+ * Multiple asynchronous transfers are submitted to achieve a good
+ * degree of concurrency between the USB communication handled by the operating
+ * system and the producing application code. The number of concurrent transfers is limited
+ * in order to retain flow control and keep memory usage at a reasonable size.
+ *
+ *
+ * For thread synchronization (between the background thread handling IO completion
+ * and the producing application thread) a blocking queue is used. It is prefilled with
+ * transfer instances ready to be used. The background thread adds the completed
+ * instances back to this queue. The producing application thread waits until a transfer instance
+ * is available for use.
+ *
+ */
+public abstract class EndpointOutputStream extends OutputStream {
+
+ private static final System.Logger LOG = System.getLogger(EndpointOutputStream.class.getName());
+
+ // Maximum time (ms) to wait for outstanding transfers to complete during teardown.
+ // A completion that is never delivered (e.g. after an unplug) then degrades to a
+ // logged warning instead of a permanent hang.
+ private static final long TEARDOWN_TIMEOUT_MS = 1000;
+
+ protected UsbDeviceImpl device;
+ protected final int endpointNumber;
+ protected final Arena arena;
+ // Endpoint packet size
+ private final int packetSize;
+ // Transfer size (multiple of packet size)
+ private final int transferSize;
+ // Blocking queue of available transfers (to limit the number of submitted transfers)
+ private final ArrayBlockingQueue availableTransferQueue;
+ private boolean needsZlp;
+ private Transfer currentTransfer;
+ private int writeOffset;
+ private int numOutstandingTransfers;
+ private boolean hasError;
+
+
+ /**
+ * Creates a new instance
+ *
+ * @param device USB device
+ * @param endpointNumber endpoint number
+ * @param bufferSize approximate buffer size (in bytes)
+ */
+ protected EndpointOutputStream(UsbDeviceImpl device, int endpointNumber, int bufferSize) {
+ this.device = device;
+ this.endpointNumber = endpointNumber;
+ //arena = Arena.ofShared(); // not supported by GraalVM
+ arena = Arena.ofAuto();
+
+ packetSize = device.getEndpoint(UsbDirection.OUT, endpointNumber).getPacketSize();
+
+ // use between 4 and 32 packets per transfer (256B to 2KB for FS, 2KB to 16KB for HS)
+ var numPacketsPerTransfer = (int) Math.round(Math.sqrt((double) bufferSize / packetSize));
+ numPacketsPerTransfer = Math.clamp(numPacketsPerTransfer, 4, 32);
+ transferSize = numPacketsPerTransfer * packetSize;
+
+ // use at least 2 outstanding transfers (3 in total)
+ var maxOutstandingTransfers = Math.max((bufferSize + transferSize / 2) / transferSize, 3);
+
+ configureEndpoint();
+
+ availableTransferQueue = new ArrayBlockingQueue<>(maxOutstandingTransfers);
+
+ // prefill transfer queue
+ for (var i = 0; i < maxOutstandingTransfers; i++) {
+ final var transfer = device.createTransfer();
+ transfer.setData(arena.allocate(transferSize, 8));
+ transfer.setCompletion(this::onCompletion);
+
+ if (i == 0) {
+ currentTransfer = transfer;
+ } else {
+ availableTransferQueue.add(transfer);
+ }
+ }
+ }
+
+ private boolean isClosed() {
+ return device == null;
+ }
+
+ @Override
+ public void close() throws IOException {
+ if (isClosed())
+ return;
+
+ // Teardown path: every wait is bounded by a single deadline so a lost completion
+ // (device unplugged, or completion dropped in a source-removal race) degrades to a
+ // logged warning instead of hanging the application thread. Unlike the public
+ // flush(), this must not route through the unbounded waits.
+ var deadline = System.currentTimeMillis() + TEARDOWN_TIMEOUT_MS;
+
+ try {
+ if (!hasError) {
+ // best-effort: transmit any remaining buffered data (and a ZLP if needed)
+ if (writeOffset > 0)
+ submitForClose(writeOffset, deadline);
+ if (needsZlp && currentTransfer != null)
+ submitForClose(0, deadline);
+ }
+
+ drainOutstandingTransfers(deadline);
+
+ } catch (Exception e) {
+ // teardown must not fail; data-path errors are already surfaced by write()/flush()
+ LOG.log(WARNING, "error while closing output stream - ignoring", e);
+
+ } finally {
+ device = null;
+ availableTransferQueue.clear();
+ currentTransfer = null;
+ //arena.close();
+ }
+ }
+
+ /**
+ * Submits the current transfer during teardown and acquires a replacement,
+ * both bounded by the given deadline.
+ *
+ * Unlike {@link #submitTransfer(int)} this does not recurse into {@link #close()} on error,
+ * and it does not block indefinitely when acquiring the next transfer instance.
+ *
+ *
+ * @param size size of data to be transmitted
+ * @param deadline absolute deadline (ms since epoch) for acquiring the next transfer
+ */
+ private void submitForClose(int size, long deadline) {
+ currentTransfer.setDataSize(size);
+ submitTransferOut(currentTransfer);
+
+ synchronized (this) {
+ numOutstandingTransfers += 1;
+ }
+
+ needsZlp = size == packetSize;
+ writeOffset = 0;
+ // if no transfer becomes available within the deadline, currentTransfer stays null,
+ // the drain below still bounded-waits for the in-flight transfer to complete
+ currentTransfer = pollAvailableTransfer(deadline);
+ }
+
+ /**
+ * Waits for all outstanding transfers to complete, bounded by the given deadline.
+ *
+ * If a completion is not delivered in time, the remaining transfers are abandoned and a
+ * warning is logged.
+ *
+ *
+ * @param deadline absolute deadline (ms since epoch)
+ */
+ private void drainOutstandingTransfers(long deadline) {
+ int numTransfers;
+ synchronized (this) {
+ numTransfers = numOutstandingTransfers + availableTransferQueue.size();
+ }
+
+ for (var i = 0; i < numTransfers; i++) {
+ if (pollAvailableTransfer(deadline) == null) {
+ int abandoned;
+ synchronized (this) {
+ abandoned = numOutstandingTransfers;
+ }
+ LOG.log(WARNING,
+ "abandoning {0} outstanding transfer(s) during output stream teardown - no completion within {1} ms",
+ abandoned, TEARDOWN_TIMEOUT_MS);
+ break;
+ }
+ }
+ }
+
+ /**
+ * Waits until a transfer instance is available for use, bounded by the given deadline.
+ *
+ * @param deadline absolute deadline (ms since epoch)
+ * @return transfer instance ready for use, or {@code null} if the deadline expired
+ */
+ private Transfer pollAvailableTransfer(long deadline) {
+ var wasInterrupted = false;
+ try {
+ while (true) {
+ var remaining = deadline - System.currentTimeMillis();
+ if (remaining <= 0)
+ return null;
+
+ try {
+ var transfer = availableTransferQueue.poll(remaining, TimeUnit.MILLISECONDS);
+ if (transfer == null)
+ return null;
+
+ // surface a transfer error unless we are already in the error path
+ var result = transfer.resultCode();
+ if (result != 0 && !hasError) {
+ transfer.setResultCode(0);
+ device.throwOSException(result, "error occurred while transmitting to endpoint %d", endpointNumber);
+ }
+
+ return transfer;
+
+ } catch (InterruptedException _) {
+ // defer the interrupt: keep polling the remaining time without re-setting
+ // the flag (avoids a busy-spin), then re-assert it once we are done
+ wasInterrupted = true;
+ }
+ }
+ } finally {
+ if (wasInterrupted)
+ Thread.currentThread().interrupt();
+ }
+ }
+
+ @Override
+ public void write(int b) throws IOException {
+ ensureOpen();
+
+ try {
+ currentTransfer.data().set(JAVA_BYTE, writeOffset, (byte) b);
+ writeOffset += 1;
+ if (writeOffset == transferSize)
+ submitTransfer(writeOffset);
+
+ } catch (UsbException e) {
+ throw toIOException(e);
+ }
+ }
+
+ @Override
+ public void write(byte @NotNull [] b, int off, int len) throws IOException {
+ Objects.checkFromIndexSize(off, len, b.length);
+ ensureOpen();
+
+ try {
+ while (len > 0) {
+ var chunkSize = Math.min(len, transferSize - writeOffset);
+ MemorySegment.copy(b, off, currentTransfer.data(), JAVA_BYTE, writeOffset, chunkSize);
+ writeOffset += chunkSize;
+ off += chunkSize;
+ len -= chunkSize;
+
+ if (writeOffset == transferSize)
+ submitTransfer(writeOffset);
+ }
+
+ } catch (UsbException e) {
+ throw toIOException(e);
+ }
+ }
+
+ @Override
+ public void flush() throws IOException {
+ ensureOpen();
+
+ try {
+ if (writeOffset > 0)
+ submitTransfer(writeOffset);
+
+ if (needsZlp)
+ submitTransfer(0);
+
+ waitForOutstandingTransfers();
+
+ } catch (UsbException e) {
+ throw toIOException(e);
+ }
+ }
+
+ /**
+ * Submits a transfer and set a new transfer instance
+ * as the current one, possibly waiting until one is ready.
+ *
+ * Throws an exception if the transfer to be reused has completed with an error on the
+ * previous operation. The exception is suppressed if {@code hasError} flag is set.
+ *
+ * Throws an exception if any of the transfers has completed with an error.
+ * The exception is suppressed if {@code hasError} flag is set.
+ *
+ */
+ private void waitForOutstandingTransfers() {
+ // Wait until all buffers have been transmitted by removing them from the
+ // queue and reinserting them.
+
+ int numTransfers;
+ synchronized (this) {
+ numTransfers = numOutstandingTransfers + availableTransferQueue.size();
+ }
+
+ if (numTransfers == 0)
+ return;
+
+ var transfers = new Transfer[numTransfers];
+ for (var i = 0; i < numTransfers; i++)
+ transfers[i] = waitForAvailableTransfer();
+
+ // reinsert the transfer instances
+ if (!hasError)
+ availableTransferQueue.addAll(Arrays.asList(transfers));
+ }
+
+ /**
+ * Wait until one of the allocated transfer instances is available for use.
+ *
+ * Throws an exception if the transfer to be reused has completed with an error on the
+ * previous operation. The exception is suppressed if {@code hasError} flag is set.
+ *
+ *
+ * @return transfer instance ready for use
+ */
+ private Transfer waitForAvailableTransfer() {
+ // Defer interruption: keep a local flag instead of re-asserting the interrupt
+ // inside the loop (which would make the next take() throw immediately and
+ // busy-spin). Re-assert once a transfer has actually become available.
+ var wasInterrupted = false;
+ try {
+ while (true) {
+ try {
+ var transfer = availableTransferQueue.take();
+
+ // check for error
+ var result = transfer.resultCode();
+ if (result != 0 && !hasError) {
+ transfer.setResultCode(0);
+ device.throwOSException(result, "error occurred while transmitting to endpoint %d", endpointNumber);
+ }
+
+ return transfer;
+
+ } catch (InterruptedException _) {
+ wasInterrupted = true;
+ }
+ }
+ } finally {
+ if (wasInterrupted)
+ Thread.currentThread().interrupt();
+ }
+ }
+
+ /**
+ * Called by the asynchronous IO completion handler.
+ *
+ * @param transfer the completed request
+ */
+ private synchronized void onCompletion(Transfer transfer) {
+ availableTransferQueue.add(transfer);
+ numOutstandingTransfers -= 1;
+ }
+
+ protected abstract void submitTransferOut(Transfer request);
+
+ protected void configureEndpoint() {
+ }
+
+ private void ensureOpen() throws IOException {
+ if (isClosed())
+ throw new IOException("output stream has been closed");
+ }
+}
diff --git a/java-does-usb/src/main/java/net/codecrete/usb/common/EndpointStreams.java b/java-does-usb/src/main/java/net/codecrete/usb/common/EndpointStreams.java
new file mode 100644
index 00000000..5e9fda43
--- /dev/null
+++ b/java-does-usb/src/main/java/net/codecrete/usb/common/EndpointStreams.java
@@ -0,0 +1,44 @@
+//
+// Java Does USB
+// Copyright (c) 2023 Manuel Bleichenbacher
+// Licensed under MIT License
+// https://opensource.org/licenses/MIT
+//
+
+package net.codecrete.usb.common;
+
+import net.codecrete.usb.UsbException;
+import net.codecrete.usb.UsbTimeoutException;
+
+import java.io.IOException;
+import java.io.InterruptedIOException;
+
+/**
+ * Helpers shared by {@link EndpointInputStream} and {@link EndpointOutputStream}.
+ */
+final class EndpointStreams {
+
+ private EndpointStreams() {
+ }
+
+ /**
+ * Wraps a USB error in an {@link IOException} so it is surfaced through the
+ * {@link java.io.InputStream}/{@link java.io.OutputStream} contract.
+ *
+ * A transfer timeout is mapped to {@link InterruptedIOException} (java.io's "I/O timed out").
+ * All other USB errors become a plain {@link IOException} with the {@link UsbException} as cause,
+ * which preserves the USB error code and any stall/timeout subtype for callers that inspect it.
+ *
+ *
+ * @param e the USB error
+ * @return the corresponding I/O exception
+ */
+ static IOException toIOException(UsbException e) {
+ if (e instanceof UsbTimeoutException) {
+ var ioException = new InterruptedIOException(e.getMessage());
+ ioException.initCause(e);
+ return ioException;
+ }
+ return new IOException(e.getMessage(), e);
+ }
+}
diff --git a/java-does-usb/src/main/java/net/codecrete/usb/common/ForeignMemory.java b/java-does-usb/src/main/java/net/codecrete/usb/common/ForeignMemory.java
index 8f3b53ed..1102cba3 100644
--- a/java-does-usb/src/main/java/net/codecrete/usb/common/ForeignMemory.java
+++ b/java-does-usb/src/main/java/net/codecrete/usb/common/ForeignMemory.java
@@ -7,36 +7,38 @@
package net.codecrete.usb.common;
+import java.lang.foreign.MemoryLayout;
import java.lang.foreign.MemorySegment;
-import java.lang.foreign.MemorySession;
-import java.util.function.Consumer;
+
+import static java.lang.foreign.ValueLayout.ADDRESS;
/**
- * Helper functions for the foreign memory API.
+ * Helper functions for accessing native memory.
*/
public class ForeignMemory {
+ private ForeignMemory() {
+ }
+
+ /**
+ * Dereferences the address at the start of given memory segment and returns the result as
+ * a memory segment with the length suitable for the specified layout.
+ *
+ * @param segment the memory segment with the pointer at offset 0
+ * @param layout layout for determining size of memory segment
+ * @return the dereferenced memory segment
+ */
+ public static MemorySegment dereference(MemorySegment segment, MemoryLayout layout) {
+ return segment.get(ADDRESS, 0).reinterpret(layout.byteSize());
+ }
/**
- * Adds a custom cleanup action which will be execution when the session of the
- * specified memory segment ends.
- *
- * Note that the {@code segment}'s session will already be closed. The {@code segment}
- * can no longer be used for function call. For that reason, the cleanup
- * action is passed a copy of the segment (separate Java instance pointing to the
- * same native memory).
- *
- * @param segment the segment to be accessed during the cleanup
- * @param action the cleanup action
+ * Dereferences the address at the start of given memory segment and returns the result as
+ * a memory segment of size 0.
+ *
+ * @param segment the memory segment with the pointer at offset 0
+ * @return the dereferenced memory segment
*/
- public static void addCloseAction(MemorySegment segment, Consumer action) {
- var size = segment.byteSize();
- var address = segment.address();
- Runnable closeAction = () -> {
- try (MemorySession closingSession = MemorySession.openConfined()) {
- var dup = MemorySegment.ofAddress(address, size, closingSession);
- action.accept(dup);
- }
- };
- segment.session().addCloseAction(closeAction);
+ public static MemorySegment dereference(MemorySegment segment) {
+ return segment.get(ADDRESS, 0);
}
}
diff --git a/java-does-usb/src/main/java/net/codecrete/usb/common/ScopeCleanup.java b/java-does-usb/src/main/java/net/codecrete/usb/common/ScopeCleanup.java
new file mode 100644
index 00000000..44f0d8ce
--- /dev/null
+++ b/java-does-usb/src/main/java/net/codecrete/usb/common/ScopeCleanup.java
@@ -0,0 +1,52 @@
+//
+// Java Does USB
+// Copyright (c) 2022 Manuel Bleichenbacher
+// Licensed under MIT License
+// https://opensource.org/licenses/MIT
+//
+
+package net.codecrete.usb.common;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Auto closeable object for clean up actions.
+ *
+ *
+ * Multiple cleanup actions can be registered and will
+ * be executed when this object is closed. They are run
+ * in the reverse order they are registered.
+ *
+ */
+public class ScopeCleanup implements AutoCloseable {
+
+ private final List cleanupActions = new ArrayList<>();
+
+ /**
+ * Registers a cleanup action to be run later.
+ *
+ * @param cleanupAction cleanup action
+ */
+ public void add(Runnable cleanupAction) {
+ cleanupActions.add(cleanupAction);
+ }
+
+ @Override
+ public void close() {
+ var size = cleanupActions.size();
+ for (var i = size - 1; i >= 0; i--)
+ cleanupActions.get(i).run();
+ }
+}
diff --git a/java-does-usb/src/main/java/net/codecrete/usb/common/Transfer.java b/java-does-usb/src/main/java/net/codecrete/usb/common/Transfer.java
new file mode 100644
index 00000000..0be15f99
--- /dev/null
+++ b/java-does-usb/src/main/java/net/codecrete/usb/common/Transfer.java
@@ -0,0 +1,117 @@
+//
+// Java Does USB
+// Copyright (c) 2023 Manuel Bleichenbacher
+// Licensed under MIT License
+// https://opensource.org/licenses/MIT
+//
+
+package net.codecrete.usb.common;
+
+import java.lang.foreign.MemorySegment;
+
+/**
+ * Asynchronous USB endpoint transfer.
+ */
+public class Transfer {
+ private MemorySegment data;
+ private int dataSize;
+ private int resultCode;
+ private int resultSize;
+ private TransferCompletion completion;
+
+ /**
+ * Gets the with data to transfer (in or out).
+ *
+ * @return buffer
+ */
+ public MemorySegment data() {
+ return data;
+ }
+
+ /**
+ * Sets buffer with data to transfer (in or out)
+ *
+ * @param data buffer
+ */
+ public void setData(MemorySegment data) {
+ this.data = data;
+ }
+
+ /**
+ * Gets length of data to transfer.
+ *
+ * @return length (in bytes)
+ */
+ public int dataSize() {
+ return dataSize;
+ }
+
+ /**
+ * Sets length of data to transfer.
+ *
+ * @param dataSize length (in bytes)
+ */
+ public void setDataSize(int dataSize) {
+ this.dataSize = dataSize;
+ }
+
+ /**
+ * Gets result code (operating system specific).
+ *
+ * 0 represents success, all other values represent an error.
+ *
+ *
+ * @return result code
+ */
+ public int resultCode() {
+ return resultCode;
+ }
+
+ /**
+ * Sets result code (operating system specific).
+ *
+ * 0 represents success, all other values represent an error.
+ *
+ *
+ * @param resultCode result code
+ */
+ public void setResultCode(int resultCode) {
+ this.resultCode = resultCode;
+ }
+
+ /**
+ * Gets length of transferred data.
+ *
+ * @return length (in bytes)
+ */
+ public int resultSize() {
+ return resultSize;
+ }
+
+ /**
+ * Sets length of transferred data.
+ *
+ * @param resultSize length (in bytes)
+ */
+ public void setResultSize(int resultSize) {
+ this.resultSize = resultSize;
+ }
+
+ /**
+ * Gets completion handler to call when the transfer is complete.
+ *
+ * @return completion handler
+ */
+ public TransferCompletion completion() {
+ return completion;
+ }
+
+ /**
+ * Sets completion handler to call when the transfer is complete.
+ *
+ * @param completion completion handler
+ */
+ public void setCompletion(TransferCompletion completion) {
+ this.completion = completion;
+ }
+}
diff --git a/java-does-usb/src/main/java/net/codecrete/usb/common/TransferCompletion.java b/java-does-usb/src/main/java/net/codecrete/usb/common/TransferCompletion.java
new file mode 100644
index 00000000..98a555e6
--- /dev/null
+++ b/java-does-usb/src/main/java/net/codecrete/usb/common/TransferCompletion.java
@@ -0,0 +1,30 @@
+//
+// Java Does USB
+// Copyright (c) 2023 Manuel Bleichenbacher
+// Licensed under MIT License
+// https://opensource.org/licenses/MIT
+//
+
+package net.codecrete.usb.common;
+
+/**
+ * Functional interface used to notify when an asynchronous USB transfer operation has completed.
+ */
+@FunctionalInterface
+public interface TransferCompletion {
+
+ /**
+ * Called when the asynchronous transfer has completed.
+ *
+ * When this function has been called, {@link Transfer#resultSize()} and
+ * {@link Transfer#resultSize()} have been filled in.
+ *
+ *
+ * Since there is a single background task calling all completion functions,
+ * this function should only execute little work and return quickly.
+ *
+ *
+ * @param transfer completed transfer request
+ */
+ void completed(Transfer transfer);
+}
diff --git a/java-does-usb/src/main/java/net/codecrete/usb/common/USBAlternateInterfaceImpl.java b/java-does-usb/src/main/java/net/codecrete/usb/common/USBAlternateInterfaceImpl.java
deleted file mode 100644
index 4bc5c9d2..00000000
--- a/java-does-usb/src/main/java/net/codecrete/usb/common/USBAlternateInterfaceImpl.java
+++ /dev/null
@@ -1,61 +0,0 @@
-//
-// Java Does USB
-// Copyright (c) 2022 Manuel Bleichenbacher
-// Licensed under MIT License
-// https://opensource.org/licenses/MIT
-//
-
-package net.codecrete.usb.common;
-
-import net.codecrete.usb.USBAlternateInterface;
-import net.codecrete.usb.USBEndpoint;
-
-import java.util.Collections;
-import java.util.List;
-
-public class USBAlternateInterfaceImpl implements USBAlternateInterface {
-
- private final int number_;
- private final int classCode_;
- private final int subclassCode_;
- private final int protocolCode_;
- private final List endpoints_;
-
- public USBAlternateInterfaceImpl(int number, int classCode, int subclassCode, int protocolCode,
- List endpoints) {
- number_ = number;
- classCode_ = classCode;
- subclassCode_ = subclassCode;
- protocolCode_ = protocolCode;
- endpoints_ = endpoints;
- }
-
- @Override
- public int number() {
- return number_;
- }
-
- @Override
- public int classCode() {
- return classCode_;
- }
-
- @Override
- public int subclassCode() {
- return subclassCode_;
- }
-
- @Override
- public int protocolCode() {
- return protocolCode_;
- }
-
- @Override
- public List endpoints() {
- return Collections.unmodifiableList(endpoints_);
- }
-
- void addEndpoint(USBEndpoint endpoint) {
- endpoints_.add(endpoint);
- }
-}
diff --git a/java-does-usb/src/main/java/net/codecrete/usb/common/USBDescriptors.java b/java-does-usb/src/main/java/net/codecrete/usb/common/USBDescriptors.java
deleted file mode 100644
index 2ead6d77..00000000
--- a/java-does-usb/src/main/java/net/codecrete/usb/common/USBDescriptors.java
+++ /dev/null
@@ -1,182 +0,0 @@
-//
-// Java Does USB
-// Copyright (c) 2022 Manuel Bleichenbacher
-// Licensed under MIT License
-// https://opensource.org/licenses/MIT
-//
-
-package net.codecrete.usb.common;
-
-import java.lang.foreign.GroupLayout;
-import java.lang.invoke.VarHandle;
-
-import static java.lang.foreign.MemoryLayout.PathElement.groupElement;
-import static java.lang.foreign.MemoryLayout.structLayout;
-import static java.lang.foreign.ValueLayout.JAVA_BYTE;
-import static java.lang.foreign.ValueLayout.JAVA_SHORT;
-
-/**
- * Memory layout of USB descriptors.
- */
-public class USBDescriptors {
-
- public static final byte DEVICE_DESCRIPTOR_TYPE = 0x01;
- public static final byte CONFIGURATION_DESCRIPTOR_TYPE = 0x02;
- public static final byte STRING_DESCRIPTOR_TYPE = 0x03;
- public static final byte INTERFACE_DESCRIPTOR_TYPE = 0x04;
- public static final byte ENDPOINT_DESCRIPTOR_TYPE = 0x05;
- public static final byte DEVICE_QUALIFIER_DESCRIPTOR_TYPE = 0x06;
- public static final byte OTHER_SPEED_CONFIGURATION_DESCRIPTOR_TYPE = 0x07;
- public static final byte INTERFACE_POWER_DESCRIPTOR_TYPE = 0x08;
- public static final byte OTG_DESCRIPTOR_TYPE = 0x09;
- public static final byte DEBUG_DESCRIPTOR_TYPE = 0x0a;
- public static final byte INTERFACE_ASSOCIATION_DESCRIPTOR_TYPE = 0x0b;
- public static final byte BOS_DESCRIPTOR_TYPE = 0x0f;
- public static final byte DEVICE_CAPABILITY_DESCRIPTOR_TYPE = 0x10;
- public static final byte HID_DESCRIPTOR_TYPE = 0x21;
- public static final byte CS_INTERFACE_DESCRIPTOR_TYPE = 0x24;
- public static final byte CS_ENDPOINT_DESCRIPTOR_TYPE = 0x25;
- public static final byte USB_20_HUB_DESCRIPTOR_TYPE = 0x29;
- public static final byte USB_30_HUB_DESCRIPTOR_TYPE = 0x2a;
- public static final byte SUPERSPEED_ENDPOINT_COMPANION_DESCRIPTOR_TYPE = 0x30;
- public static final byte SUPERSPEEDPLUS_ISOCH_ENDPOINT_COMPANION_DESCRIPTOR_TYPE = 0x31;
-
- public static final short DEFAULT_LANGUAGE = 0x0409;
-
- // typedef struct {
- // UCHAR bLength;
- // UCHAR bDescriptorType;
- // USHORT bcdUSB;
- // UCHAR bDeviceClass;
- // UCHAR bDeviceSubClass;
- // UCHAR bDeviceProtocol;
- // UCHAR bMaxPacketSize0;
- // USHORT idVendor;
- // USHORT idProduct;
- // USHORT bcdDevice;
- // UCHAR iManufacturer;
- // UCHAR iProduct;
- // UCHAR iSerialNumber;
- // UCHAR bNumConfigurations;
- //} __attribute__((packed));
- public static final GroupLayout Device$Struct = structLayout(
- JAVA_BYTE.withName("bLength"),
- JAVA_BYTE.withName("bDescriptorType"),
- JAVA_SHORT.withName("bcdUSB"),
- JAVA_BYTE.withName("bDeviceClass"),
- JAVA_BYTE.withName("bDeviceSubClass"),
- JAVA_BYTE.withName("bDeviceProtocol"),
- JAVA_BYTE.withName("bMaxPacketSize0"),
- JAVA_SHORT.withName("idVendor"),
- JAVA_SHORT.withName("idProduct"),
- JAVA_SHORT.withName("bcdDevice"),
- JAVA_BYTE.withName("iManufacturer"),
- JAVA_BYTE.withName("iProduct"),
- JAVA_BYTE.withName("iSerialNumber"),
- JAVA_BYTE.withName("bNumConfigurations")
- );
-
- public static final VarHandle Device_bcdUSB = Device$Struct.varHandle(groupElement("bcdUSB"));
- public static final VarHandle Device_bDeviceClass = Device$Struct.varHandle(groupElement("bDeviceClass"));
- public static final VarHandle Device_bDeviceSubClass = Device$Struct.varHandle(groupElement("bDeviceSubClass"));
- public static final VarHandle Device_bDeviceProtocol = Device$Struct.varHandle(groupElement("bDeviceProtocol"));
- public static final VarHandle Device_idVendor = Device$Struct.varHandle(groupElement("idVendor"));
- public static final VarHandle Device_idProduct = Device$Struct.varHandle(groupElement("idProduct"));
- public static final VarHandle Device_bcdDevice = Device$Struct.varHandle(groupElement("bcdDevice"));
- public static final VarHandle Device_iManufacturer = Device$Struct.varHandle(groupElement("iManufacturer"));
- public static final VarHandle Device_iProduct = Device$Struct.varHandle(groupElement("iProduct"));
- public static final VarHandle Device_iSerialNumber = Device$Struct.varHandle(groupElement("iSerialNumber"));
-
- // struct USBConfigurationDescriptor
- //{
- // uint8_t bLength;
- // uint8_t bDescriptorType;
- // uint16_t wTotalLength;
- // uint8_t bNumInterfaces;
- // uint8_t bConfigurationValue;
- // uint8_t iConfiguration;
- // uint8_t bmAttributes;
- // uint8_t MaxPower;
- //} __attribute__((packed));
-
- /**
- * USB configuration descriptor
- */
- public static final GroupLayout Configuration = structLayout(JAVA_BYTE.withName("bLength"), JAVA_BYTE.withName(
- "bDescriptorType"), JAVA_SHORT.withName("wTotalLength"), JAVA_BYTE.withName("bNumInterfaces"),
- JAVA_BYTE.withName("bConfigurationValue"), JAVA_BYTE.withName("iConfiguration"), JAVA_BYTE.withName(
- "bmAttributes"), JAVA_BYTE.withName("bMaxPower"));
-
- public static final VarHandle Configuration_bLength = Configuration.varHandle(groupElement("bLength"));
- public static final VarHandle Configuration_bDescriptorType = Configuration.varHandle(groupElement(
- "bDescriptorType"));
- public static final VarHandle Configuration_wTotalLength = Configuration.varHandle(groupElement("wTotalLength"));
- public static final VarHandle Configuration_bNumInterfaces = Configuration.varHandle(groupElement("bNumInterfaces"
- ));
- public static final VarHandle Configuration_bConfigurationValue = Configuration.varHandle(groupElement(
- "bConfigurationValue"));
- public static final VarHandle Configuration_iConfiguration = Configuration.varHandle(groupElement("iConfiguration"
- ));
- public static final VarHandle Configuration_bmAttributes = Configuration.varHandle(groupElement("bmAttributes"));
- public static final VarHandle Configuration_bMaxPower = Configuration.varHandle(groupElement("bMaxPower"));
-
- // struct USBInterfaceDescriptor
- // {
- // uint8_t bLength;
- // uint8_t bDescriptorType;
- // uint8_t bInterfaceNumber;
- // uint8_t bAlternateSetting;
- // uint8_t bNumEndpoints;
- // uint8_t bInterfaceClass;
- // uint8_t bInterfaceSubClass;
- // uint8_t bInterfaceProtocol;
- // uint8_t iInterface;
- // } __attribute__((packed));
-
- public static final GroupLayout Interface = structLayout(JAVA_BYTE.withName("bLength"), JAVA_BYTE.withName(
- "bDescriptorType"), JAVA_BYTE.withName("bInterfaceNumber"), JAVA_BYTE.withName("bAlternateSetting"),
- JAVA_BYTE.withName("bNumEndpoints"), JAVA_BYTE.withName("bInterfaceClass"), JAVA_BYTE.withName(
- "bInterfaceSubClass"), JAVA_BYTE.withName("bInterfaceProtocol"), JAVA_BYTE.withName("iInterface"));
-
- public static final VarHandle Interface_bLength = Interface.varHandle(groupElement("bLength"));
- public static final VarHandle Interface_bDescriptorType = Interface.varHandle(groupElement("bDescriptorType"));
- public static final VarHandle Interface_bInterfaceNumber = Interface.varHandle(groupElement("bInterfaceNumber"));
- public static final VarHandle Interface_bAlternateSetting = Interface.varHandle(groupElement("bAlternateSetting"));
- public static final VarHandle Interface_bNumEndpoints = Interface.varHandle(groupElement("bNumEndpoints"));
- public static final VarHandle Interface_bInterfaceClass = Interface.varHandle(groupElement("bInterfaceClass"));
- public static final VarHandle Interface_bInterfaceSubClass = Interface.varHandle(groupElement("bInterfaceSubClass"
- ));
- public static final VarHandle Interface_bInterfaceProtocol = Interface.varHandle(groupElement("bInterfaceProtocol"
- ));
- public static final VarHandle Interface_iInterface = Interface.varHandle(groupElement("iInterface"));
-
-
- // struct USBInterfaceDescriptor
- // {
- // uint8_t bLength;
- // uint8_t bDescriptorType;
- // uint8_t bEndpointAddress;
- // uint8_t bmAttributes;
- // uint16_t wMaxPacketSize;
- // uint8_t bInterval;
- // } __attribute__((packed));
-
- public static final GroupLayout Endpoint = structLayout(JAVA_BYTE.withName("bLength"), JAVA_BYTE.withName(
- "bDescriptorType"), JAVA_BYTE.withName("bEndpointAddress"), JAVA_BYTE.withName("bmAttributes"),
- JAVA_SHORT.withName("wMaxPacketSize").withBitAlignment(8), JAVA_BYTE.withName("bInterval"));
-
- public static final VarHandle Endpoint_bLength = Endpoint.varHandle(groupElement("bLength"));
- public static final VarHandle Endpoint_bDescriptorType = Endpoint.varHandle(groupElement("bDescriptorType"));
- public static final VarHandle Endpoint_bEndpointAddress = Endpoint.varHandle(groupElement("bEndpointAddress"));
- public static final VarHandle Endpoint_bmAttributes = Endpoint.varHandle(groupElement("bmAttributes"));
- public static final VarHandle Endpoint_wMaxPacketSize = Endpoint.varHandle(groupElement("wMaxPacketSize"));
- public static final VarHandle Endpoint_bInterval = Endpoint.varHandle(groupElement("bInterval"));
-
-
- static {
- assert Device$Struct.byteSize() == 18;
- assert Configuration.byteSize() == 9;
- assert Interface.byteSize() == 9;
- assert Endpoint.byteSize() == 7;
- }
-}
diff --git a/java-does-usb/src/main/java/net/codecrete/usb/common/USBDeviceImpl.java b/java-does-usb/src/main/java/net/codecrete/usb/common/USBDeviceImpl.java
deleted file mode 100644
index 8eb1446b..00000000
--- a/java-does-usb/src/main/java/net/codecrete/usb/common/USBDeviceImpl.java
+++ /dev/null
@@ -1,288 +0,0 @@
-//
-// Java Does USB
-// Copyright (c) 2022 Manuel Bleichenbacher
-// Licensed under MIT License
-// https://opensource.org/licenses/MIT
-//
-
-package net.codecrete.usb.common;
-
-import net.codecrete.usb.*;
-
-import java.util.Collections;
-import java.util.List;
-
-public abstract class USBDeviceImpl implements USBDevice {
-
- protected final Object id_;
- protected final int vendorId_;
- protected final int productId_;
- protected final String manufacturer_;
- protected final String product_;
- protected final String serialNumber_;
- protected int classCode_;
- protected int subclassCode_;
- protected int protocolCode_;
- protected Version usbVersion_;
- protected Version deviceVersion_;
-
- protected List interfaces_;
-
- /**
- * Creates a new instance.
- *
- * @param id unique device ID
- * @param vendorId USB vendor ID
- * @param productId USB product ID
- * @param manufacturer manufacturer name
- * @param product product name
- * @param serialNumber serial number
- */
- protected USBDeviceImpl(Object id, int vendorId, int productId, String manufacturer, String product,
- String serialNumber) {
-
- assert id != null;
-
- id_ = id;
- vendorId_ = vendorId;
- productId_ = productId;
- manufacturer_ = manufacturer;
- product_ = product;
- serialNumber_ = serialNumber;
- }
-
- @Override
- public abstract void open();
-
- @Override
- public abstract void close();
-
- @Override
- public abstract boolean isOpen();
-
- protected void checkIsOpen() {
- if (!isOpen())
- throw new USBException("The device needs to be open to call this method");
- }
-
- @Override
- public int productId() {
- return productId_;
- }
-
- @Override
- public int vendorId() {
- return vendorId_;
- }
-
- @Override
- public String product() {
- return product_;
- }
-
- @Override
- public String manufacturer() {
- return manufacturer_;
- }
-
- @Override
- public String serialNumber() {
- return serialNumber_;
- }
-
- @Override
- public int classCode() {
- return classCode_;
- }
-
- @Override
- public int subclassCode() {
- return subclassCode_;
- }
-
- @Override
- public int protocolCode() {
- return protocolCode_;
- }
-
- @Override
- public Version usbVersion() { return usbVersion_; }
-
- @Override
- public Version deviceVersion() { return deviceVersion_; }
-
- public Object getUniqueId() {
- return id_;
- }
-
- public void setClassCodes(int classCode, int subclassCode, int protocolCode) {
- classCode_ = classCode;
- subclassCode_ = subclassCode;
- protocolCode_ = protocolCode;
- }
-
- public void setVersions(int usbVersion, int deviceVersion) {
- usbVersion_ = new Version(usbVersion);
- deviceVersion_ = new Version(deviceVersion);
- }
-
- @Override
- public List interfaces() {
- return Collections.unmodifiableList(interfaces_);
- }
-
- public void setInterfaces(List interfaces) {
- interfaces_ = interfaces;
- }
-
- @Override
- public abstract void claimInterface(int interfaceNumber);
-
- @Override
- public abstract void releaseInterface(int interfaceNumber);
-
- public void setClaimed(int interfaceNumber, boolean claimed) {
- for (var intf : interfaces_) {
- if (intf.number() == interfaceNumber) {
- ((USBInterfaceImpl) intf).setClaimed(claimed);
- return;
- }
- }
- throw new USBException("Internal error (interface not found)");
- }
-
- /**
- * Returns the interface with the specified number.
- *
- * @param interfaceNumber the interface number
- * @return the interface
- */
- protected USBInterfaceImpl getInterface(int interfaceNumber) {
- return (USBInterfaceImpl) interfaces_.stream().filter((intf) -> intf.number() == interfaceNumber).findFirst().orElse(null);
- }
-
- /**
- * Checks if the specified endpoint is valid for communication and returns the endpoint address.
- *
- * @param endpointNumber endpoint number (1 to 127)
- * @param direction transfer direction
- * @param transferType1 transfer type 1
- * @param transferType2 transfer type 2 (or {@code null})
- * @return endpoint address
- */
- protected byte getEndpointAddress(int endpointNumber, USBDirection direction,
- USBTransferType transferType1, USBTransferType transferType2) {
-
- checkIsOpen();
-
- if (endpointNumber >= 1 && endpointNumber <= 127) {
- for (var intf : interfaces_) {
- if (intf.isClaimed()) {
- for (var ep : intf.alternate().endpoints()) {
- if (ep.number() == endpointNumber && ep.direction() == direction
- && (ep.transferType() == transferType1 || ep.transferType() == transferType2))
- return (byte) (endpointNumber | (direction == USBDirection.IN ? 0x80 : 0));
- }
- }
- }
- }
-
- throwInvalidEndpointException(endpointNumber, direction, transferType1, transferType2);
- return 0; // will never be reached
- }
-
- /**
- * Checks if the specified endpoint is valid for communication and returns the endpoint.
- *
- * @param endpointNumber endpoint number (1 to 127)
- * @param direction transfer direction
- * @param transferType1 transfer type 1
- * @param transferType2 transfer type 2 (or {@code null})
- * @return endpoint
- */
- protected EndpointInfo getEndpoint(int endpointNumber, USBDirection direction,
- USBTransferType transferType1, USBTransferType transferType2) {
-
- checkIsOpen();
-
- if (endpointNumber >= 1 && endpointNumber <= 127) {
- for (var intf : interfaces_) {
- if (intf.isClaimed()) {
- for (var ep : intf.alternate().endpoints()) {
- if (ep.number() == endpointNumber && ep.direction() == direction
- && (ep.transferType() == transferType1 || ep.transferType() == transferType2))
- return new EndpointInfo(intf.number(), ep.number(),
- (byte) (endpointNumber | (direction == USBDirection.IN ? 0x80 : 0)));
- }
- }
- }
- }
-
- throwInvalidEndpointException(endpointNumber, direction, transferType1, transferType2);
- return null; // will never be reached
- }
-
- protected void throwInvalidEndpointException(int endpointNumber, USBDirection direction,
- USBTransferType transferType1, USBTransferType transferType2) {
- String transferTypeDesc;
- if (transferType2 == null)
- transferTypeDesc = transferType1.name();
- else
- transferTypeDesc = String.format("%s or %s", transferType1.name(), transferType2.name());
- throw new USBException(String.format("Endpoint number %d does not exist, is not part of a claimed interface " +
- "or is not valid for %s transfer in %s direction", endpointNumber, transferTypeDesc,
- direction.name()));
- }
-
- protected int getInterfaceNumber(int endpointNumber) {
- if (endpointNumber < 1 || endpointNumber > 127)
- return -1;
-
- for (var intf : interfaces_) {
- if (intf.isClaimed()) {
- for (var ep : intf.alternate().endpoints()) {
- if (ep.number() == endpointNumber)
- return intf.number();
- }
- }
- }
-
- return -1;
- }
-
- @Override
- public abstract byte[] controlTransferIn(USBControlTransfer setup, int length);
-
- @Override
- public abstract void controlTransferOut(USBControlTransfer setup, byte[] data);
-
-
- @Override
- public abstract void transferOut(int endpointNumber, byte[] data);
-
- @Override
- public abstract byte[] transferIn(int endpointNumber, int maxLength);
-
- @Override
- public boolean equals(Object o) {
- if (this == o)
- return true;
- if (o == null || getClass() != o.getClass())
- return false;
- USBDeviceImpl that = (USBDeviceImpl) o;
- return id_.equals(that.id_);
- }
-
- @Override
- public int hashCode() {
- return id_.hashCode();
- }
-
- @Override
- public String toString() {
- return "VID: 0x" + String.format("%04x", vendorId_) + ", PID: 0x" + String.format("%04x", productId_) + ", " + "manufacturer: " + manufacturer_ + ", product: " + product_ + ", serial: " + serialNumber_ + ", ID: " + id_;
- }
-
- public record EndpointInfo(int interfaceNumber, int endpointNumber, byte endpointAddress) {
- }
-}
diff --git a/java-does-usb/src/main/java/net/codecrete/usb/common/USBEndpointImpl.java b/java-does-usb/src/main/java/net/codecrete/usb/common/USBEndpointImpl.java
deleted file mode 100644
index c245a4f3..00000000
--- a/java-does-usb/src/main/java/net/codecrete/usb/common/USBEndpointImpl.java
+++ /dev/null
@@ -1,50 +0,0 @@
-//
-// Java Does USB
-// Copyright (c) 2022 Manuel Bleichenbacher
-// Licensed under MIT License
-// https://opensource.org/licenses/MIT
-//
-
-package net.codecrete.usb.common;
-
-import net.codecrete.usb.USBDirection;
-import net.codecrete.usb.USBEndpoint;
-import net.codecrete.usb.USBTransferType;
-
-/**
- * Implementation of {@code USBEndpoint} interface.
- */
-public class USBEndpointImpl implements USBEndpoint {
-
- private final int number_;
- private final USBDirection direction_;
- private final USBTransferType type_;
- private final int packetSize_;
-
- public USBEndpointImpl(int number, USBDirection direction, USBTransferType type, int packetSize) {
- number_ = number;
- direction_ = direction;
- type_ = type;
- packetSize_ = packetSize;
- }
-
- @Override
- public int number() {
- return number_;
- }
-
- @Override
- public USBDirection direction() {
- return direction_;
- }
-
- @Override
- public USBTransferType transferType() {
- return type_;
- }
-
- @Override
- public int packetSize() {
- return packetSize_;
- }
-}
diff --git a/java-does-usb/src/main/java/net/codecrete/usb/common/USBInterfaceImpl.java b/java-does-usb/src/main/java/net/codecrete/usb/common/USBInterfaceImpl.java
deleted file mode 100644
index 7ff8c970..00000000
--- a/java-does-usb/src/main/java/net/codecrete/usb/common/USBInterfaceImpl.java
+++ /dev/null
@@ -1,57 +0,0 @@
-//
-// Java Does USB
-// Copyright (c) 2022 Manuel Bleichenbacher
-// Licensed under MIT License
-// https://opensource.org/licenses/MIT
-//
-
-package net.codecrete.usb.common;
-
-import net.codecrete.usb.USBAlternateInterface;
-import net.codecrete.usb.USBInterface;
-
-import java.util.Collections;
-import java.util.List;
-
-public class USBInterfaceImpl implements USBInterface {
-
- private final int number_;
- private final USBAlternateInterface alternate_;
- private final List alternates_;
-
- private boolean isClaimed_;
-
- public USBInterfaceImpl(int number, List alternates) {
- number_ = number;
- alternates_ = alternates;
- alternate_ = alternates.get(0);
- }
-
- @Override
- public int number() {
- return number_;
- }
-
- @Override
- public boolean isClaimed() {
- return isClaimed_;
- }
-
- public void setClaimed(boolean claimed) {
- isClaimed_ = claimed;
- }
-
- @Override
- public USBAlternateInterface alternate() {
- return alternate_;
- }
-
- @Override
- public List alternates() {
- return Collections.unmodifiableList(alternates_);
- }
-
- void addAlternate(USBAlternateInterface alt) {
- alternates_.add(alt);
- }
-}
diff --git a/java-does-usb/src/main/java/net/codecrete/usb/common/USBStructs.java b/java-does-usb/src/main/java/net/codecrete/usb/common/USBStructs.java
deleted file mode 100644
index b98d3187..00000000
--- a/java-does-usb/src/main/java/net/codecrete/usb/common/USBStructs.java
+++ /dev/null
@@ -1,35 +0,0 @@
-package net.codecrete.usb.common;
-
-import java.lang.foreign.GroupLayout;
-import java.lang.invoke.VarHandle;
-
-import static java.lang.foreign.MemoryLayout.PathElement.groupElement;
-import static java.lang.foreign.MemoryLayout.structLayout;
-import static java.lang.foreign.ValueLayout.JAVA_BYTE;
-import static java.lang.foreign.ValueLayout.JAVA_SHORT;
-
-/**
- * Memory layout of USB data structures.
- */
-public class USBStructs {
- // typedef struct {
- // uint8_t RequestType;
- // uint8_t Request;
- // uint8_t Value;
- // uint16_t Index;
- // uint16_t Length;
- //} __attribute__((packed));
- public static final GroupLayout SetupPacket$Struct = structLayout(JAVA_BYTE.withName("bmRequest"),
- JAVA_BYTE.withName("bRequest"), JAVA_SHORT.withName("wValue"), JAVA_SHORT.withName("wIndex"),
- JAVA_SHORT.withName("wLength"));
-
- public static final VarHandle SetupPacket_bmRequest = SetupPacket$Struct.varHandle(groupElement("bmRequest"));
- public static final VarHandle SetupPacket_bRequest = SetupPacket$Struct.varHandle(groupElement("bRequest"));
- public static final VarHandle SetupPacket_wValue = SetupPacket$Struct.varHandle(groupElement("wValue"));
- public static final VarHandle SetupPacket_wIndex = SetupPacket$Struct.varHandle(groupElement("wIndex"));
- public static final VarHandle SetupPacket_wLength = SetupPacket$Struct.varHandle(groupElement("wLength"));
-
- static {
- assert SetupPacket$Struct.byteSize() == 8;
- }
-}
diff --git a/java-does-usb/src/main/java/net/codecrete/usb/common/UsbAlternateInterfaceImpl.java b/java-does-usb/src/main/java/net/codecrete/usb/common/UsbAlternateInterfaceImpl.java
new file mode 100644
index 00000000..9a7d70b1
--- /dev/null
+++ b/java-does-usb/src/main/java/net/codecrete/usb/common/UsbAlternateInterfaceImpl.java
@@ -0,0 +1,74 @@
+//
+// Java Does USB
+// Copyright (c) 2022 Manuel Bleichenbacher
+// Licensed under MIT License
+// https://opensource.org/licenses/MIT
+//
+
+package net.codecrete.usb.common;
+
+import net.codecrete.usb.UsbAlternateInterface;
+import net.codecrete.usb.UsbDirection;
+import net.codecrete.usb.UsbEndpoint;
+import net.codecrete.usb.UsbException;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.Comparator;
+import java.util.List;
+
+import static java.util.Collections.unmodifiableList;
+
+public class UsbAlternateInterfaceImpl implements UsbAlternateInterface {
+
+ private final int alternateInterfaceNumber;
+ private final int alternateInterfaceClass;
+ private final int alternateInterfaceSubclass;
+ private final int alternateInterfaceProtocol;
+ private final List endpointList;
+
+ public UsbAlternateInterfaceImpl(int number, int classCode, int subclassCode, int protocolCode,
+ List endpoints) {
+ alternateInterfaceNumber = number;
+ alternateInterfaceClass = classCode;
+ alternateInterfaceSubclass = subclassCode;
+ alternateInterfaceProtocol = protocolCode;
+ endpointList = endpoints;
+ endpointList.sort(Comparator.comparingInt(UsbEndpoint::getNumber));
+ }
+
+ @Override
+ public int getNumber() {
+ return alternateInterfaceNumber;
+ }
+
+ @Override
+ public int getClassCode() {
+ return alternateInterfaceClass;
+ }
+
+ @Override
+ public int getSubclassCode() {
+ return alternateInterfaceSubclass;
+ }
+
+ @Override
+ public int getProtocolCode() {
+ return alternateInterfaceProtocol;
+ }
+
+ @Override
+ public @NotNull List getEndpoints() {
+ return unmodifiableList(endpointList);
+ }
+
+ void addEndpoint(UsbEndpoint endpoint) {
+ endpointList.add(endpoint);
+ }
+
+ @Override
+ public @NotNull UsbEndpoint getEndpoint(int endpointNumber, UsbDirection direction) {
+ return endpointList.stream()
+ .filter(ep -> ep.getNumber() == endpointNumber && ep.getDirection() == direction).findFirst()
+ .orElseThrow(() -> new UsbException(String.format("Endpoint %d (%s) does not exist", endpointNumber, direction)));
+ }
+}
diff --git a/java-does-usb/src/main/java/net/codecrete/usb/common/UsbDeviceImpl.java b/java-does-usb/src/main/java/net/codecrete/usb/common/UsbDeviceImpl.java
new file mode 100644
index 00000000..41a1fb4f
--- /dev/null
+++ b/java-does-usb/src/main/java/net/codecrete/usb/common/UsbDeviceImpl.java
@@ -0,0 +1,517 @@
+//
+// Java Does USB
+// Copyright (c) 2022 Manuel Bleichenbacher
+// Licensed under MIT License
+// https://opensource.org/licenses/MIT
+//
+
+package net.codecrete.usb.common;
+
+import net.codecrete.usb.UsbDevice;
+import net.codecrete.usb.UsbDirection;
+import net.codecrete.usb.UsbEndpoint;
+import net.codecrete.usb.UsbException;
+import net.codecrete.usb.UsbInterface;
+import net.codecrete.usb.UsbTimeoutException;
+import net.codecrete.usb.UsbTransferType;
+import net.codecrete.usb.Version;
+import net.codecrete.usb.usbstandard.DeviceDescriptor;
+import org.jetbrains.annotations.NotNull;
+
+import java.lang.foreign.MemorySegment;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.function.IntFunction;
+
+import static java.lang.System.Logger.Level.WARNING;
+import static java.lang.foreign.ValueLayout.JAVA_BYTE;
+
+@SuppressWarnings("SynchronizationOnLocalVariableOrMethodParameter")
+public abstract class UsbDeviceImpl implements UsbDevice {
+
+ private static final System.Logger LOG = System.getLogger(UsbDeviceImpl.class.getName());
+
+ // Maximum time (ms) to wait for the completion of a transfer that was aborted due to a
+ // timeout. If the abort's completion is never delivered (e.g. after an unplug), the wait
+ // degrades to a logged warning instead of a permanent hang. The transfer is then abandoned.
+ private static final long ABORT_COMPLETION_TIMEOUT_MS = 1000;
+
+ /**
+ * Operating system-specific device ID used for {@link #equals(Object)} and {@link #hashCode()}.
+ *
+ * Can be a String instance (such as a file path) or a Long instance (such as an internal ID).
+ * It only needs to be valid for the duration the device is connected.
+ *
+ */
+ protected final Object uniqueDeviceId;
+
+ protected List interfaceList;
+
+ protected byte[] rawDeviceDescriptor;
+
+ protected byte[] rawConfigurationDescriptor;
+
+ // Information from the device descriptor
+ protected final int vid;
+ protected final int pid;
+ protected String manufacturerString;
+ protected String productString;
+ protected String serialString;
+ protected int deviceClass;
+ protected int deviceSubclass;
+ protected int deviceProtocol;
+ protected Version versionUsb;
+ protected Version versionDevice;
+ // volatile: written by the device monitor thread in disconnect(), read unlocked
+ // via isConnected() and checkIsClosed()
+ protected volatile boolean connected;
+
+ /**
+ * Creates a new instance.
+ *
+ * @param id unique device ID
+ * @param vendorId USB vendor ID
+ * @param productId USB product ID
+ */
+ protected UsbDeviceImpl(Object id, int vendorId, int productId) {
+
+ assert id != null;
+
+ uniqueDeviceId = id;
+ vid = vendorId;
+ pid = productId;
+ connected = true;
+ }
+
+ @Override
+ public void detachStandardDrivers() {
+ if (isOpened())
+ throw new UsbException("detachStandardDrivers() must not be called while the device is open");
+
+ // default implementation: do nothing
+ }
+
+ @Override
+ public void attachStandardDrivers() {
+ if (isOpened())
+ throw new UsbException("attachStandardDrivers() must not be called while the device is open");
+
+ // default implementation: do nothing
+ }
+
+ protected void checkIsOpen() {
+ if (!isOpened())
+ throw new UsbException("device needs to be opened first for this operation");
+ }
+
+ protected void checkIsClosed(String message) {
+ if (!connected)
+ throw new UsbException("device has been disconnected");
+ if (isOpened())
+ throw new UsbException(message);
+ }
+
+ protected synchronized void disconnect() {
+ connected = false;
+ close();
+ }
+
+ @Override
+ public int getProductId() {
+ return pid;
+ }
+
+ @Override
+ public int getVendorId() {
+ return vid;
+ }
+
+ @Override
+ public String getProduct() {
+ return productString;
+ }
+
+ @Override
+ public String getManufacturer() {
+ return manufacturerString;
+ }
+
+ @Override
+ public String getSerialNumber() {
+ return serialString;
+ }
+
+ @Override
+ public int getClassCode() {
+ return deviceClass;
+ }
+
+ @Override
+ public int getSubclassCode() {
+ return deviceSubclass;
+ }
+
+ @Override
+ public int getProtocolCode() {
+ return deviceProtocol;
+ }
+
+ @Override
+ public @NotNull Version getUsbVersion() {
+ return versionUsb;
+ }
+
+ @Override
+ public @NotNull Version getDeviceVersion() {
+ return versionDevice;
+ }
+
+ @Override
+ public byte @NotNull [] getConfigurationDescriptor() {
+ return rawConfigurationDescriptor;
+ }
+
+ @Override
+ public byte @NotNull [] getDeviceDescriptor() {
+ return rawDeviceDescriptor;
+ }
+
+ public Object getUniqueId() {
+ return uniqueDeviceId;
+ }
+
+ @Override
+ public boolean isConnected() {
+ return connected;
+ }
+
+ /**
+ * Sets the class codes and version for the device descriptor.
+ *
+ * @param descriptor the device descriptor
+ */
+ public void setFromDeviceDescriptor(MemorySegment descriptor) {
+ rawDeviceDescriptor = descriptor.toArray(JAVA_BYTE);
+ var deviceDescriptor = new DeviceDescriptor(descriptor);
+ deviceClass = deviceDescriptor.deviceClass() & 255;
+ deviceSubclass = deviceDescriptor.deviceSubClass() & 255;
+ deviceProtocol = deviceDescriptor.deviceProtocol() & 255;
+ versionUsb = new Version(deviceDescriptor.usbVersion());
+ versionDevice = new Version(deviceDescriptor.deviceVersion());
+ }
+
+ /**
+ * Sets the configuration descriptor and derives the interface and endpoint descriptions.
+ *
+ * @param descriptor configuration descriptor
+ * @return parsed configuration
+ */
+ protected Configuration setConfigurationDescriptor(MemorySegment descriptor) {
+ rawConfigurationDescriptor = descriptor.toArray(JAVA_BYTE);
+ var configuration = ConfigurationParser.parseConfigurationDescriptor(descriptor);
+ interfaceList = configuration.interfaces();
+ interfaceList.sort(Comparator.comparingInt(UsbInterface::getNumber));
+ return configuration;
+ }
+
+ /**
+ * Set the product strings.
+ *
+ * @param manufacturer manufacturer name
+ * @param product product name
+ * @param serialNumber serial number
+ */
+ public void setProductStrings(String manufacturer, String product, String serialNumber) {
+ manufacturerString = manufacturer;
+ productString = product;
+ serialString = serialNumber;
+ }
+
+ /**
+ * Sets the product strings from the device descriptor.
+ *
+ * To look up the string, a lookup function is provided. It takes the
+ * string ID and returns the string from the string descriptor.
+ *
+ *
+ * @param descriptor device descriptor
+ * @param stringLookup string lookup function
+ */
+ public void setProductString(MemorySegment descriptor, IntFunction stringLookup) {
+ var deviceDescriptor = new DeviceDescriptor(descriptor);
+ manufacturerString = stringLookup.apply(deviceDescriptor.iManufacturer());
+ productString = stringLookup.apply(deviceDescriptor.iProduct());
+ serialString = stringLookup.apply(deviceDescriptor.iSerialNumber());
+ }
+
+ public void setClassCodes(int classCode, int subclassCode, int protocolCode) {
+ deviceClass = classCode;
+ deviceSubclass = subclassCode;
+ deviceProtocol = protocolCode;
+ }
+
+ public void setVersions(int usbVersion, int deviceVersion) {
+ versionUsb = new Version(usbVersion);
+ versionDevice = new Version(deviceVersion);
+ }
+
+ @Override
+ public @NotNull List getInterfaces() {
+ return Collections.unmodifiableList(interfaceList);
+ }
+
+ public void setClaimed(int interfaceNumber, boolean claimed) {
+ for (var intf : interfaceList) {
+ if (intf.getNumber() == interfaceNumber) {
+ ((UsbInterfaceImpl) intf).setClaimed(claimed);
+ return;
+ }
+ }
+ throw new UsbException("internal error (interface not found)");
+ }
+
+ @Override
+ public @NotNull UsbInterfaceImpl getInterface(int interfaceNumber) {
+ return (UsbInterfaceImpl) interfaceList.stream()
+ .filter(intf -> intf.getNumber() == interfaceNumber).findFirst()
+ .orElseThrow(() -> new UsbException(String.format("USB device has no interface %d", interfaceNumber)));
+ }
+
+ public UsbInterfaceImpl getInterfaceWithCheck(int interfaceNumber, boolean isClaimed) {
+ var intf = getInterface(interfaceNumber);
+ if (isClaimed && !intf.isClaimed()) {
+ throw new UsbException(String.format("interface %d must be claimed first", interfaceNumber));
+ } else if (!isClaimed && intf.isClaimed()) {
+ throw new UsbException(String.format("interface %d has already been claimed", interfaceNumber));
+ }
+ return intf;
+ }
+
+ @Override
+ public @NotNull UsbEndpoint getEndpoint(UsbDirection direction, int endpointNumber) {
+ for (var intf : interfaceList) {
+ for (var endpoint : intf.getCurrentAlternate().getEndpoints()) {
+ if (endpoint.getDirection() == direction && endpoint.getNumber() == endpointNumber)
+ return endpoint;
+ }
+ }
+ throw new UsbException(String.format("endpoint %d (%s) does not exist", endpointNumber, direction.name()));
+ }
+
+ /**
+ * Checks if the specified endpoint is valid for communication and returns the endpoint.
+ *
+ * @param direction transfer direction
+ * @param endpointNumber endpoint number (1 to 127)
+ * @param transferType1 transfer type 1
+ * @param transferType2 transfer type 2 (or {@code null})
+ * @return endpoint
+ */
+ @SuppressWarnings("java:S3776")
+ protected EndpointInfo getEndpoint(UsbDirection direction, int endpointNumber, UsbTransferType transferType1,
+ UsbTransferType transferType2) {
+
+ checkIsOpen();
+
+ if (endpointNumber >= 1 && endpointNumber <= 127) {
+ for (var intf : interfaceList) {
+ if (intf.isClaimed()) {
+ for (var ep : intf.getCurrentAlternate().getEndpoints()) {
+ if (ep.getNumber() == endpointNumber && ep.getDirection() == direction
+ && (ep.getTransferType() == transferType1 || ep.getTransferType() == transferType2))
+ return new EndpointInfo(intf.getNumber(), ep.getNumber(),
+ (byte) (endpointNumber | (direction == UsbDirection.IN ? 0x80 : 0)),
+ ep.getPacketSize(), ep.getTransferType());
+ }
+ }
+ }
+ }
+
+ throwInvalidEndpointException(direction, endpointNumber, transferType1, transferType2);
+ return null; // will never be reached
+ }
+
+ protected void throwInvalidEndpointException(UsbDirection direction, int endpointNumber,
+ UsbTransferType transferType1, UsbTransferType transferType2) {
+ String transferTypeDesc;
+ if (transferType2 == null)
+ transferTypeDesc = transferType1.name();
+ else
+ transferTypeDesc = String.format("%s or %s", transferType1.name(), transferType2.name());
+ throw new UsbException(String.format(
+ "endpoint number %d does not exist, is not part of a claimed interface or is not valid for %s transfer in %s direction",
+ endpointNumber, transferTypeDesc, direction.name()));
+ }
+
+ protected int getInterfaceNumber(UsbDirection direction, int endpointNumber) {
+ if (endpointNumber < 1 || endpointNumber > 127)
+ return -1;
+
+ for (var intf : interfaceList) {
+ if (intf.isClaimed()) {
+ for (var ep : intf.getCurrentAlternate().getEndpoints()) {
+ if (ep.getNumber() == endpointNumber && ep.getDirection() == direction)
+ return intf.getNumber();
+ }
+ }
+ }
+
+ return -1;
+ }
+
+ @Override
+ public void transferOut(int endpointNumber, byte @NotNull [] data) {
+ transferOut(endpointNumber, data, 0, data.length, 0);
+ }
+
+ @Override
+ public void transferOut(int endpointNumber, byte @NotNull [] data, int timeout) {
+ transferOut(endpointNumber, data, 0, data.length, timeout);
+ }
+
+ @Override
+ public byte @NotNull [] transferIn(int endpointNumber) {
+ return transferIn(endpointNumber, 0);
+ }
+
+ protected void waitForTransfer(Transfer transfer, int timeout, UsbDirection direction, int endpointNumber) {
+ if (timeout <= 0) {
+ waitNoTimeout(transfer);
+
+ } else {
+ var hasTimedOut = waitWithTimeout(transfer, timeout);
+
+ // test for timeout
+ if (hasTimedOut && transfer.resultCode() == 0) {
+ abortTransfers(direction, endpointNumber);
+
+ // Wait for the abort's completion, but bounded: if it never arrives (device
+ // vanished such that neither the transfer nor the abort yields a callback),
+ // abandon the transfer instead of blocking forever. Abandoning is safe because
+ // buffers reaching this path come from an auto arena and survive a late completion.
+ var abortCompleted = !waitWithTimeout(transfer, (int) ABORT_COMPLETION_TIMEOUT_MS);
+ if (!abortCompleted)
+ LOG.log(WARNING, "abort completion for {0} did not arrive within {1} ms - abandoning transfer",
+ getOperationDescription(direction, endpointNumber), ABORT_COMPLETION_TIMEOUT_MS);
+
+ throw new UsbTimeoutException(getOperationDescription(direction, endpointNumber) + " aborted due to timeout");
+ }
+ }
+
+ // test for error
+ if (transfer.resultCode() != 0) {
+ var operation = getOperationDescription(direction, endpointNumber);
+ throwOSException(transfer.resultCode(), operation + " failed");
+ }
+ }
+
+ @SuppressWarnings({"java:S2273", "java:S2142"})
+ private static void waitNoTimeout(Transfer transfer) {
+ // wait for transfer.
+ // Defer interruption: keep a local flag instead of re-asserting the interrupt
+ // inside the loop (which would make the next wait() throw immediately and
+ // busy-spin). Re-assert once the transfer has actually completed.
+ var wasInterrupted = false;
+ while (transfer.resultSize() == -1) {
+ try {
+ transfer.wait();
+ } catch (InterruptedException _) {
+ wasInterrupted = true;
+ }
+ }
+ if (wasInterrupted)
+ Thread.currentThread().interrupt();
+ }
+
+ @SuppressWarnings({"java:S2273", "java:S2142"})
+ private static boolean waitWithTimeout(Transfer transfer, int timeout) {
+ // wait for transfer to complete, or abort when timeout occurs.
+ // Defer interruption: keep a local flag instead of re-asserting the interrupt
+ // inside the loop (which would make the next wait() throw immediately and
+ // busy-spin). The remaining timeout is recomputed on both the normal and the
+ // interrupted path, so the wait stays bounded by the original expiration.
+ var expiration = System.currentTimeMillis() + timeout;
+ long remainingTimeout = timeout;
+ var wasInterrupted = false;
+ while (remainingTimeout > 0 && transfer.resultSize() == -1) {
+ try {
+ transfer.wait(remainingTimeout);
+ } catch (InterruptedException _) {
+ wasInterrupted = true;
+ }
+ remainingTimeout = expiration - System.currentTimeMillis();
+ }
+
+ if (wasInterrupted)
+ Thread.currentThread().interrupt();
+
+ return remainingTimeout <= 0;
+ }
+
+ protected static String getOperationDescription(UsbDirection direction, int endpointNumber) {
+ if (endpointNumber == 0) {
+ return "control transfer";
+ } else {
+ return String.format("transfer %s on endpoint %d", direction.name(), endpointNumber);
+ }
+
+ }
+
+ /**
+ * Create a transfer object suitable for this device.
+ *
+ * @return transfer object
+ */
+ protected abstract Transfer createTransfer();
+
+ /**
+ * Completion handler used for synchronous, blocking transfers.
+ *
+ * Calls {@link Object#notify()} so the caller can use
+ * {@link Object#wait()} to wait for completion.
+ *
+ *
+ * @param transfer the transfer that has completed
+ */
+ @SuppressWarnings({"java:S2445", "java:S2446"})
+ protected static void onSyncTransferCompleted(Transfer transfer) {
+ synchronized (transfer) {
+ transfer.notify();
+ }
+ }
+
+ /**
+ * Throws an exception for the specified operating-specific error code.
+ *
+ * @param errorCode error code, operating specific
+ * @param message exception message format ({@link String#format(String, Object...)} style)
+ * @param args arguments for exception message
+ */
+ protected abstract void throwOSException(int errorCode, String message, Object... args);
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o)
+ return true;
+ if (o == null || getClass() != o.getClass())
+ return false;
+ var that = (UsbDeviceImpl) o;
+ return uniqueDeviceId.equals(that.uniqueDeviceId);
+ }
+
+ @Override
+ public int hashCode() {
+ return uniqueDeviceId.hashCode();
+ }
+
+ @Override
+ public String toString() {
+ return String.format("VID: 0x%04x, PID: 0x%04x, manufacturer: %s, product: %s, serial: %s, ID: %s",
+ vid, pid, manufacturerString, productString, serialString, uniqueDeviceId);
+ }
+
+ public record EndpointInfo(int interfaceNumber, int endpointNumber, byte endpointAddress, int packetSize,
+ UsbTransferType transferType) {
+ }
+}
diff --git a/java-does-usb/src/main/java/net/codecrete/usb/common/USBDeviceRegistry.java b/java-does-usb/src/main/java/net/codecrete/usb/common/UsbDeviceRegistry.java
similarity index 60%
rename from java-does-usb/src/main/java/net/codecrete/usb/common/USBDeviceRegistry.java
rename to java-does-usb/src/main/java/net/codecrete/usb/common/UsbDeviceRegistry.java
index a7818bfe..ed7999af 100644
--- a/java-does-usb/src/main/java/net/codecrete/usb/common/USBDeviceRegistry.java
+++ b/java-does-usb/src/main/java/net/codecrete/usb/common/UsbDeviceRegistry.java
@@ -7,17 +7,19 @@
package net.codecrete.usb.common;
-import net.codecrete.usb.USBDevice;
-import net.codecrete.usb.USBException;
+import net.codecrete.usb.UsbDevice;
+import net.codecrete.usb.UsbException;
import java.util.ArrayList;
-import java.util.Collections;
import java.util.List;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Consumer;
+import static java.lang.System.Logger.Level.INFO;
+import static java.lang.System.Logger.Level.WARNING;
+
/**
* Base class for USB device registry.
*
@@ -30,12 +32,16 @@
* and builds the initial device list.
*
*/
-public abstract class USBDeviceRegistry {
+@SuppressWarnings("java:S3077")
+public abstract class UsbDeviceRegistry {
+
+ private static final System.Logger LOG = System.getLogger(UsbDeviceRegistry.class.getName());
- private volatile List devices;
- private volatile Throwable failureCause;
- protected Consumer onDeviceConnectedHandler;
- protected Consumer onDeviceDisconnectedHandler;
+ private List devices;
+ private Throwable failureCause;
+ // volatile: set by the application thread, read by the device monitor thread
+ protected volatile Consumer onDeviceConnectedHandler;
+ protected volatile Consumer onDeviceDisconnectedHandler;
private final Lock lock = new ReentrantLock();
private final Condition enumerationComplete = lock.newCondition();
@@ -69,26 +75,43 @@ public void start() {
*
* @return list of devices
*/
- public List getAllDevices() {
- return Collections.unmodifiableList(devices);
+ public synchronized List getAllDevices() {
+ return devices;
}
- public void setOnDeviceConnected(Consumer handler) {
+ public void setOnDeviceConnected(Consumer handler) {
onDeviceConnectedHandler = handler;
}
- public void setOnDeviceDisconnected(Consumer handler) {
+ public void setOnDeviceDisconnected(Consumer handler) {
onDeviceDisconnectedHandler = handler;
}
- protected void emitOnDeviceConnected(USBDevice device) {
- if (onDeviceConnectedHandler != null)
- onDeviceConnectedHandler.accept(device);
+ protected void emitOnDeviceConnected(UsbDevice device) {
+ // read once so a concurrent setOnDeviceConnected(null) cannot fail between check and call
+ var handler = onDeviceConnectedHandler;
+ if (handler == null)
+ return;
+
+ try {
+ handler.accept(device);
+
+ } catch (Exception e) {
+ LOG.log(WARNING, "unhandled exception in 'onDeviceConnected' handler - ignoring", e);
+ }
}
- protected void emitOnDeviceDisconnected(USBDevice device) {
- if (onDeviceDisconnectedHandler != null)
- onDeviceDisconnectedHandler.accept(device);
+ protected void emitOnDeviceDisconnected(UsbDevice device) {
+ var handler = onDeviceDisconnectedHandler;
+ if (handler == null)
+ return;
+
+ try {
+ handler.accept(device);
+
+ } catch (Exception e) {
+ LOG.log(WARNING, "unhandled exception in 'onDeviceDisconnected' handler - ignoring", e);
+ }
}
/**
@@ -102,7 +125,7 @@ protected void emitOnDeviceDisconnected(USBDevice device) {
*/
protected void startDeviceMonitor(Runnable monitorTask) {
// start new thread
- Thread t = new Thread(monitorTask, "USB device monitor");
+ var t = new Thread(monitorTask, "USB device monitor");
t.setDaemon(true);
t.start();
@@ -117,7 +140,7 @@ protected void startDeviceMonitor(Runnable monitorTask) {
}
if (failureCause != null)
- throw new USBException("Initial device enumeration has failed", failureCause);
+ throw new UsbException("initial device enumeration has failed", failureCause);
}
/**
@@ -150,8 +173,10 @@ protected void enumerationFailed(Throwable e) {
*
* @param deviceList the device list
*/
- protected void setInitialDeviceList(List deviceList) {
- devices = deviceList;
+ protected void setInitialDeviceList(List deviceList) {
+ synchronized (this) {
+ devices = deviceList;
+ }
signalEnumerationComplete();
}
@@ -160,31 +185,33 @@ protected void setInitialDeviceList(List deviceList) {
*
* @param device device to add
*/
- protected void addDevice(USBDevice device) {
- // check for duplicates
- if (findDeviceIndex(devices, ((USBDeviceImpl) device).getUniqueId()) >= 0)
- return;
+ protected void addDevice(UsbDevice device) {
+ synchronized (this) {
+ // check for duplicates
+ if (findDeviceIndex(devices, ((UsbDeviceImpl) device).getUniqueId()) >= 0)
+ return;
- // copy list
- var newDeviceList = new ArrayList(devices.size() + 1);
- newDeviceList.addAll(devices);
- newDeviceList.add(device);
- devices = newDeviceList;
+ // copy list
+ var newDeviceList = new ArrayList(devices.size() + 1);
+ newDeviceList.addAll(devices);
+ newDeviceList.add(device);
+ devices = newDeviceList;
+ }
// send notification
emitOnDeviceConnected(device);
}
+ @SuppressWarnings("java:S106")
protected void closeAndRemoveDevice(Object deviceId) {
var device = findDevice(deviceId);
if (device == null)
return;
try {
- device.close();
- } catch (Throwable e) {
- System.err.println("Info: [JavaDoesUSB] failed to close USB device - ignoring exception");
- e.printStackTrace(System.err);
+ ((UsbDeviceImpl) device).disconnect();
+ } catch (Exception e) {
+ LOG.log(INFO, "failed to close disconnected USB device - ignoring exception", e);
}
removeDevice(deviceId);
@@ -196,16 +223,19 @@ protected void closeAndRemoveDevice(Object deviceId) {
* @param deviceId the unique ID of the device to remove
*/
protected void removeDevice(Object deviceId) {
- // locate device to be removed
- int index = findDeviceIndex(devices, deviceId);
- if (index < 0)
- return; // strange
+ UsbDevice device;
+ synchronized (this) {
+ // locate device to be removed
+ int index = findDeviceIndex(devices, deviceId);
+ if (index < 0)
+ return; // strange
- // copy list and remove device
- var device = devices.get(index);
- var newDeviceList = new ArrayList<>(devices);
- newDeviceList.remove(index);
- devices = newDeviceList;
+ // copy list and remove device
+ device = devices.get(index);
+ var newDeviceList = new ArrayList<>(devices);
+ newDeviceList.remove(index);
+ devices = newDeviceList;
+ }
// send notification
emitOnDeviceDisconnected(device);
@@ -218,9 +248,9 @@ protected void removeDevice(Object deviceId) {
* @param deviceId the unique device ID
* @return return the index, or -1 if the device is not found
*/
- protected int findDeviceIndex(List deviceList, Object deviceId) {
+ protected int findDeviceIndex(List deviceList, Object deviceId) {
for (int i = 0; i < deviceList.size(); i++) {
- var dev = (USBDeviceImpl) deviceList.get(i);
+ var dev = (UsbDeviceImpl) deviceList.get(i);
if (deviceId.equals(dev.getUniqueId()))
return i;
}
@@ -233,7 +263,7 @@ protected int findDeviceIndex(List deviceList, Object deviceId) {
* @param deviceId the unique device ID
* @return return device, or {@code null} if not found.
*/
- protected USBDevice findDevice(Object deviceId) {
+ protected UsbDevice findDevice(Object deviceId) {
int index = findDeviceIndex(devices, deviceId);
if (index < 0)
return null;
diff --git a/java-does-usb/src/main/java/net/codecrete/usb/common/UsbEndpointImpl.java b/java-does-usb/src/main/java/net/codecrete/usb/common/UsbEndpointImpl.java
new file mode 100644
index 00000000..f40525ea
--- /dev/null
+++ b/java-does-usb/src/main/java/net/codecrete/usb/common/UsbEndpointImpl.java
@@ -0,0 +1,50 @@
+//
+// Java Does USB
+// Copyright (c) 2022 Manuel Bleichenbacher
+// Licensed under MIT License
+// https://opensource.org/licenses/MIT
+//
+
+package net.codecrete.usb.common;
+
+import net.codecrete.usb.UsbDirection;
+import net.codecrete.usb.UsbEndpoint;
+import net.codecrete.usb.UsbTransferType;
+
+/**
+ * Implementation of {@code UsbEndpoint} interface.
+ */
+public class UsbEndpointImpl implements UsbEndpoint {
+
+ private final int endpointNumber;
+ private final UsbDirection transferDirection;
+ private final UsbTransferType type;
+ private final int maxPacketSize;
+
+ public UsbEndpointImpl(int number, UsbDirection direction, UsbTransferType type, int packetSize) {
+ endpointNumber = number;
+ transferDirection = direction;
+ this.type = type;
+ maxPacketSize = packetSize;
+ }
+
+ @Override
+ public int getNumber() {
+ return endpointNumber;
+ }
+
+ @Override
+ public UsbDirection getDirection() {
+ return transferDirection;
+ }
+
+ @Override
+ public UsbTransferType getTransferType() {
+ return type;
+ }
+
+ @Override
+ public int getPacketSize() {
+ return maxPacketSize;
+ }
+}
diff --git a/java-does-usb/src/main/java/net/codecrete/usb/common/UsbInterfaceImpl.java b/java-does-usb/src/main/java/net/codecrete/usb/common/UsbInterfaceImpl.java
new file mode 100644
index 00000000..64697e87
--- /dev/null
+++ b/java-does-usb/src/main/java/net/codecrete/usb/common/UsbInterfaceImpl.java
@@ -0,0 +1,75 @@
+//
+// Java Does USB
+// Copyright (c) 2022 Manuel Bleichenbacher
+// Licensed under MIT License
+// https://opensource.org/licenses/MIT
+//
+
+package net.codecrete.usb.common;
+
+import net.codecrete.usb.UsbAlternateInterface;
+import net.codecrete.usb.UsbException;
+import net.codecrete.usb.UsbInterface;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+
+public class UsbInterfaceImpl implements UsbInterface {
+
+ private final int interfaceNumber;
+ private UsbAlternateInterface currentAlternate;
+ private final List alternateInterfaces;
+
+ private boolean claimed;
+
+ public UsbInterfaceImpl(int number, List alternates) {
+ interfaceNumber = number;
+ alternateInterfaces = alternates;
+ currentAlternate = alternates.getFirst();
+ alternateInterfaces.sort(Comparator.comparingInt(UsbAlternateInterface::getNumber));
+ }
+
+ @Override
+ public int getNumber() {
+ return interfaceNumber;
+ }
+
+ @Override
+ public boolean isClaimed() {
+ return claimed;
+ }
+
+ public void setClaimed(boolean claimed) {
+ this.claimed = claimed;
+ }
+
+ @Override
+ public @NotNull UsbAlternateInterface getCurrentAlternate() {
+ return currentAlternate;
+ }
+
+ @Override
+ public @NotNull UsbAlternateInterface getAlternate(int alternateNumber) {
+ return alternateInterfaces.stream()
+ .filter(alt -> alt.getNumber() == alternateNumber).findFirst()
+ .orElseThrow(() -> new UsbException(String.format(
+ "Interface %d does not have an alternate interface setting %d",
+ interfaceNumber, alternateNumber)
+ ));
+ }
+
+ @Override
+ public @NotNull List getAlternates() {
+ return Collections.unmodifiableList(alternateInterfaces);
+ }
+
+ void addAlternate(UsbAlternateInterface alt) {
+ alternateInterfaces.add(alt);
+ }
+
+ public void setAlternate(UsbAlternateInterface alternate) {
+ currentAlternate = alternate;
+ }
+}
diff --git a/java-does-usb/src/main/java/net/codecrete/usb/linux/EPoll.java b/java-does-usb/src/main/java/net/codecrete/usb/linux/EPoll.java
new file mode 100644
index 00000000..38b9d5bb
--- /dev/null
+++ b/java-does-usb/src/main/java/net/codecrete/usb/linux/EPoll.java
@@ -0,0 +1,140 @@
+//
+// Java Does USB
+// Copyright (c) 2022 Manuel Bleichenbacher
+// Licensed under MIT License
+// https://opensource.org/licenses/MIT
+//
+
+package net.codecrete.usb.linux;
+
+import net.codecrete.usb.linux.gen.errno.errno;
+
+import java.lang.foreign.Arena;
+import java.lang.foreign.FunctionDescriptor;
+import java.lang.foreign.GroupLayout;
+import java.lang.foreign.Linker;
+import java.lang.foreign.MemoryLayout;
+import java.lang.foreign.MemorySegment;
+import java.lang.invoke.MethodHandle;
+import java.lang.invoke.VarHandle;
+
+import static java.lang.foreign.ValueLayout.ADDRESS;
+import static java.lang.foreign.ValueLayout.ADDRESS_UNALIGNED;
+import static java.lang.foreign.ValueLayout.JAVA_INT;
+import static java.lang.foreign.ValueLayout.JAVA_INT_UNALIGNED;
+import static java.lang.foreign.ValueLayout.JAVA_LONG_UNALIGNED;
+import static net.codecrete.usb.linux.Linux.allocateErrorState;
+import static net.codecrete.usb.linux.LinuxUsbException.throwLastError;
+import static net.codecrete.usb.linux.gen.epoll.epoll.EPOLL_CTL_ADD;
+import static net.codecrete.usb.linux.gen.epoll.epoll.EPOLL_CTL_DEL;
+
+@SuppressWarnings({"OptionalGetWithoutIsPresent", "SameParameterValue", "java:S100", "java:S1192"})
+public class EPoll {
+ private EPoll() { }
+
+ private static final boolean IS_AARCH64 = System.getProperty("os.arch").equals("aarch64");
+
+ private static final GroupLayout DATA$LAYOUT = MemoryLayout.unionLayout(
+ ADDRESS_UNALIGNED.withName("ptr"),
+ JAVA_INT_UNALIGNED.withName("fd"),
+ JAVA_INT_UNALIGNED.withName("u32"),
+ JAVA_LONG_UNALIGNED.withName("u64")
+ ).withName("epoll_data");
+
+ static final GroupLayout EVENT$LAYOUT = IS_AARCH64 ?
+ MemoryLayout.structLayout(
+ JAVA_INT.withName("events"),
+ MemoryLayout.paddingLayout(4),
+ DATA$LAYOUT.withName("data")
+ ).withName("epoll_event") :
+ MemoryLayout.structLayout(
+ JAVA_INT_UNALIGNED.withName("events"),
+ DATA$LAYOUT.withName("data")
+ ).withName("epoll_event");
+
+ // varhandle to access the "fd" field in an epoll_event array
+ static final VarHandle EVENT_ARRAY_DATA_FD$VH = EVENT$LAYOUT.arrayElementVarHandle(
+ MemoryLayout.PathElement.groupElement("data"),
+ MemoryLayout.PathElement.groupElement("fd")
+ );
+
+ // varhandle to access the "fd" field in an epoll_event struct
+ private static final VarHandle EVENT_DATA_FD$VH = EVENT$LAYOUT.varHandle(
+ MemoryLayout.PathElement.groupElement("data"),
+ MemoryLayout.PathElement.groupElement("fd")
+ );
+
+ private static final VarHandle EVENTS$VH = EVENT$LAYOUT.varHandle(
+ MemoryLayout.PathElement.groupElement("events")
+ );
+
+ private static final Linker linker = Linker.nativeLinker();
+
+ private static final FunctionDescriptor epoll_create1$FUNC = FunctionDescriptor.of(JAVA_INT, JAVA_INT);
+ private static final MethodHandle epoll_create1$MH = linker.downcallHandle(linker.defaultLookup().find(
+ "epoll_create").get(), epoll_create1$FUNC, Linux.ERRNO_STATE);
+
+ private static final FunctionDescriptor epoll_ctl$FUNC = FunctionDescriptor.of(JAVA_INT, JAVA_INT, JAVA_INT, JAVA_INT, ADDRESS);
+ private static final MethodHandle epoll_ctl$MH = linker.downcallHandle(linker.defaultLookup().find(
+ "epoll_ctl").get(), epoll_ctl$FUNC, Linux.ERRNO_STATE);
+
+ private static final FunctionDescriptor epoll_wait$FUNC = FunctionDescriptor.of(JAVA_INT, JAVA_INT, ADDRESS, JAVA_INT, JAVA_INT);
+ private static final MethodHandle epoll_wait$MH = linker.downcallHandle(linker.defaultLookup().find(
+ "epoll_wait").get(), epoll_wait$FUNC, Linux.ERRNO_STATE);
+
+ static int epoll_create1(int flags, MemorySegment errno) {
+ try {
+ return (int) epoll_create1$MH.invokeExact(errno, flags);
+ } catch (Throwable ex) {
+ throw new AssertionError(ex);
+ }
+ }
+
+ private static int epoll_ctl(int epfd, int op, int fd, MemorySegment event, MemorySegment errno) {
+ try {
+ return (int) epoll_ctl$MH.invokeExact(errno, epfd, op, fd, event);
+ } catch (Throwable ex) {
+ throw new AssertionError(ex);
+ }
+ }
+
+ static int epoll_wait(int epfd, MemorySegment events, int maxevent, int timeout, MemorySegment errno) {
+ try {
+ return (int) epoll_wait$MH.invokeExact(errno, epfd, events, maxevent, timeout);
+ } catch (Throwable ex) {
+ throw new AssertionError(ex);
+ }
+ }
+
+ static void addFileDescriptor(int epfd, int op, int fd) {
+ try (var arena = Arena.ofConfined()) {
+ var errorState = allocateErrorState(arena);
+
+ var event = arena.allocate(EVENT$LAYOUT);
+ EVENTS$VH.set(event, 0, op);
+ EVENT_DATA_FD$VH.set(event, 0, fd);
+ var ret = epoll_ctl(epfd, EPOLL_CTL_ADD(), fd, event, errorState);
+ if (ret < 0)
+ throwLastError(errorState, "internal error (epoll_ctl_add)");
+ }
+ }
+
+ static void removeFileDescriptor(int epfd, int fd) {
+ try (var arena = Arena.ofConfined()) {
+ var errorState = allocateErrorState(arena);
+
+ var event = arena.allocate(EVENT$LAYOUT);
+ EVENTS$VH.set(event, 0, 0);
+ EVENT_DATA_FD$VH.set(event, 0, fd);
+ var ret = epoll_ctl(epfd, EPOLL_CTL_DEL(), fd, event, errorState);
+ if (ret < 0) {
+ var err = Linux.getErrno(errorState);
+ // ignore ENOENT as this method might be called twice when cleaning up,
+ // and EBADF as the file descriptor might have been closed concurrently
+ // (closing deregisters it from epoll anyway)
+ if (err != errno.ENOENT() && err != errno.EBADF())
+ throwLastError(errorState, "internal error (epoll_ctl_del)");
+ }
+ }
+ }
+}
diff --git a/java-does-usb/src/main/java/net/codecrete/usb/linux/IO.java b/java-does-usb/src/main/java/net/codecrete/usb/linux/IO.java
index 68792634..367deb80 100644
--- a/java-does-usb/src/main/java/net/codecrete/usb/linux/IO.java
+++ b/java-does-usb/src/main/java/net/codecrete/usb/linux/IO.java
@@ -7,23 +7,42 @@
package net.codecrete.usb.linux;
-import net.codecrete.usb.linux.gen.errno.errno;
-
-import java.lang.foreign.MemoryAddress;
+import java.lang.foreign.FunctionDescriptor;
+import java.lang.foreign.Linker;
import java.lang.foreign.MemorySegment;
-import java.lang.foreign.MemorySession;
+import java.lang.invoke.MethodHandle;
+import static java.lang.foreign.ValueLayout.ADDRESS;
import static java.lang.foreign.ValueLayout.JAVA_INT;
+import static java.lang.foreign.ValueLayout.JAVA_LONG;
+
+@SuppressWarnings({"OptionalGetWithoutIsPresent", "SameParameterValue", "java:S100"})
+class IO {
+
+ private IO() {
+ }
-public class IO {
+ private static final Linker linker = Linker.nativeLinker();
+ private static final FunctionDescriptor ioctl$FUNC = FunctionDescriptor.of(JAVA_INT, JAVA_INT, JAVA_LONG, ADDRESS);
+ private static final MethodHandle ioctl$MH = linker.downcallHandle(linker.defaultLookup().find("ioctl").get(),
+ ioctl$FUNC, Linux.ERRNO_STATE, Linker.Option.firstVariadicArg(2));
+ private static final FunctionDescriptor open$FUNC = FunctionDescriptor.of(JAVA_INT, ADDRESS, JAVA_INT);
+ private static final MethodHandle open$MH = linker.downcallHandle(linker.defaultLookup().find("open").get(),
+ open$FUNC, Linux.ERRNO_STATE);
+
+ static int ioctl(int fd, long request, MemorySegment segment, MemorySegment errno) {
+ try {
+ return (int) ioctl$MH.invokeExact(errno, fd, request, segment);
+ } catch (Throwable ex) {
+ throw new AssertionError(ex);
+ }
+ }
- public static int getErrno() {
- try (var session = MemorySession.openConfined()) {
- var location = (MemoryAddress) errno.__errno_location();
- var errnoSegment = MemorySegment.ofAddress(location, JAVA_INT.byteSize(), session);
- return errnoSegment.get(JAVA_INT, 0);
- } catch (Throwable e) {
- throw new RuntimeException(e);
+ static int open(MemorySegment file, int oflag, MemorySegment errno) {
+ try {
+ return (int) open$MH.invokeExact(errno, file, oflag);
+ } catch (Throwable ex) {
+ throw new AssertionError(ex);
}
}
}
diff --git a/java-does-usb/src/main/java/net/codecrete/usb/linux/Linux.java b/java-does-usb/src/main/java/net/codecrete/usb/linux/Linux.java
index 8d759c0e..ab494b91 100644
--- a/java-does-usb/src/main/java/net/codecrete/usb/linux/Linux.java
+++ b/java-does-usb/src/main/java/net/codecrete/usb/linux/Linux.java
@@ -7,30 +7,55 @@
package net.codecrete.usb.linux;
-import java.lang.foreign.MemoryAddress;
-import java.lang.foreign.MemorySegment;
-import java.lang.foreign.MemorySession;
+import net.codecrete.usb.linux.gen.string.string;
-import static java.lang.foreign.MemoryAddress.NULL;
+import java.lang.foreign.Arena;
+import java.lang.foreign.Linker;
+import java.lang.foreign.MemoryLayout.PathElement;
+import java.lang.foreign.MemorySegment;
+import java.lang.foreign.StructLayout;
+import java.lang.invoke.VarHandle;
/**
* Helper functions for Linux
*/
-public class Linux {
+class Linux {
+
+ private Linux() {
+ }
+
+ /**
+ * Call state for capturing the {@code errno} value.
+ */
+ static final Linker.Option ERRNO_STATE = Linker.Option.captureCallState("errno");
+ private static final StructLayout ERRNO_STATE_LAYOUT = Linker.Option.captureStateLayout();
+ private static final VarHandle callState_errno$VH =
+ ERRNO_STATE_LAYOUT.varHandle(PathElement.groupElement("errno"));
+
+ static MemorySegment allocateErrorState(Arena arena) {
+ return arena.allocate(ERRNO_STATE_LAYOUT.byteSize());
+ }
+
+ /**
+ * Gets the error message for the specified error code (returned by {@code errno}).
+ *
+ * @param err error code
+ * @return error message
+ */
+ static String getErrorMessage(int err) {
+ return string.strerror(err).getString(0);
+ }
/**
- * Creates a Java string as a copy of the null-terminated UTF-8 string.
+ * Gets the error code from the memory segment.
+ *
+ * The memory segment is assumed to have the layout {@link #ERRNO_STATE}.
+ *
+ * Each USB device must register its file handle with this task.
+ *
+ *
+ * The task keeps track of the submitted transfers by indexing them
+ * by URB address (USB request block).
+ *
+ *
+ * URBs are allocated but never freed. To limit the memory usage,
+ * URBs are reused. So the maximum number of outstanding transfers
+ * determines the number of allocated URBs.
+ *
+ */
+@SuppressWarnings("java:S6548")
+class LinuxAsyncTask {
+
+ private static final System.Logger LOG = System.getLogger(LinuxAsyncTask.class.getName());
+
+ /**
+ * Singleton instance of background task.
+ */
+ static final LinuxAsyncTask INSTANCE = new LinuxAsyncTask();
+
+ private static final int NUM_EVENTS = 5;
+
+ private final Arena urbArena = Arena.ofAuto();
+ /// available URBs
+ private final List availableURBs = new ArrayList<>();
+ /// map of URB addresses to transfer (for outstanding transfers)
+ private final Map transfersByURB = new LinkedHashMap<>();
+ /// file descriptor of epoll
+ private int epollFd = -1;
+ /// indicates that the background task has terminated due to an unrecoverable error
+ private boolean taskTerminated;
+
+ /**
+ * Background task for handling asynchronous IO completions.
+ *
+ * It polls on all registered file descriptors. If a file descriptor is
+ * ready, the URB is "reaped".
+ *
+ */
+ @SuppressWarnings({"java:S2189", "java:S135", "java:S3776"})
+ private void asyncCompletionTask() {
+
+ try (var arena = Arena.ofConfined()) {
+ var errorState = allocateErrorState(arena);
+ var urbPointerHolder = arena.allocate(ADDRESS);
+ var events = arena.allocate(EPoll.EVENT$LAYOUT, NUM_EVENTS);
+
+ while (true) {
+ try {
+ // wait for file descriptor to be ready
+ var res = epoll_wait(epollFd, events, NUM_EVENTS, -1, errorState);
+ if (res < 0) {
+ var err = Linux.getErrno(errorState);
+ if (err == EINTR())
+ continue; // continue on interrupt
+ throwException(err, "internal error (epoll_wait)");
+ }
+
+ // for all ready file descriptors, reap URBs
+ for (int i = 0; i < res; i++) {
+ var fd = (int) EPoll.EVENT_ARRAY_DATA_FD$VH.get(events, 0L, i);
+ reapURBs(fd, urbPointerHolder, errorState);
+ }
+
+ } catch (Exception e) {
+ LOG.log(ERROR, "USB async IO thread failed and is terminating; "
+ + "all outstanding transfers will fail, and no further transfers are possible", e);
+ failAllPendingTransfers();
+ return;
+ }
+ }
+ }
+ }
+
+ /**
+ * Fails all outstanding transfers and marks this task as terminated.
+ *
+ * Called when the background task can no longer dispatch completions. Waiters blocked
+ * on the failed transfers wake up with an error result instead of hanging forever,
+ * and future submissions are rejected.
+ *
+ */
+ private void failAllPendingTransfers() {
+ var failedTransfers = new ArrayList();
+ synchronized (this) {
+ taskTerminated = true;
+ for (var transfer : transfersByURB.values()) {
+ transfer.urb = null;
+ transfer.setResultCode(errno.ECANCELED());
+ transfer.setResultSize(0);
+ failedTransfers.add(transfer);
+ }
+ transfersByURB.clear();
+ availableURBs.clear();
+ }
+ completeTransfers(failedTransfers);
+ }
+
+ /**
+ * Reap all pending URBs and handle the completed transfers.
+ *
+ * @param fd file descriptor
+ * @param urbPointerHolder native memory to receive the URB pointer
+ * @param errorState native memory to receive the errno
+ */
+ private void reapURBs(int fd, MemorySegment urbPointerHolder, MemorySegment errorState) {
+
+ var completedTransfers = new ArrayList();
+ try {
+ synchronized (this) {
+ while (true) {
+ var res = IO.ioctl(fd, REAPURBNDELAY, urbPointerHolder, errorState);
+ if (res < 0) {
+ var err = Linux.getErrno(errorState);
+ if (err == errno.EAGAIN())
+ return; // no more pending URBs
+ if (err == errno.EBADF())
+ return; // file descriptor was closed concurrently (deregisters it from epoll)
+ if (err == errno.ENODEV()) {
+ // device might have been unplugged
+ EPoll.removeFileDescriptor(epollFd, fd);
+ return;
+ }
+ // Unexpected error: stop handling this device's completions but keep
+ // the task alive for all other devices. The file descriptor must be
+ // deregistered, or epoll would report it ready again immediately,
+ // resulting in a hot loop.
+ LOG.log(ERROR, "reaping URBs for file descriptor {0} failed with errno {1}; "
+ + "no further transfers will complete for this device", fd, err);
+ EPoll.removeFileDescriptor(epollFd, fd);
+ return;
+ }
+
+ var urb = dereference(urbPointerHolder);
+ completedTransfers.add(getTransferWithResult(urb));
+ }
+ }
+ } finally {
+ // Even if reaping fails, the already reaped transfers must be completed.
+ completeTransfers(completedTransfers);
+ }
+ }
+
+ /**
+ * Calls the completion handlers of the specified transfers.
+ *
+ * Must be called without holding the lock: handlers acquire other monitors
+ * (transfer, device), and threads submitting transfers acquire this task's lock
+ * while holding those monitors, so calling handlers under the lock can deadlock.
+ *
+ *
+ * @param transfers completed transfers
+ */
+ private void completeTransfers(List transfers) {
+ for (var transfer : transfers) {
+ try {
+ transfer.completion().completed(transfer);
+ } catch (Exception e) {
+ // This method also runs on the process-wide async IO thread. Any exception
+ // escaping would kill that thread and hang all async transfers for the
+ // entire library.
+ LOG.log(ERROR, "Unexpected exception while handling async IO completion", e);
+ }
+ }
+ }
+
+ /**
+ * Register a device for asynchronous IO completion handling
+ *
+ * @param device USB device
+ */
+ synchronized void addForAsyncIOCompletion(LinuxUsbDevice device) {
+ // start background process if needed
+ if (epollFd < 0)
+ startAsyncIOTask();
+
+ EPoll.addFileDescriptor(epollFd, EPOLLOUT() | EPOLLWAKEUP(), device.fileDescriptor());
+ }
+
+ /**
+ * Unregisters a device from asynchronous IO completion handling.
+ *
+ * @param device USB device
+ */
+ void removeFromAsyncIOCompletion(LinuxUsbDevice device) {
+ int fd = device.fileDescriptor();
+
+ // remove file descriptor from epoll
+ synchronized (this) {
+ EPoll.removeFileDescriptor(epollFd, fd);
+ }
+
+ // reap outstanding URBs
+ try (var arena = Arena.ofConfined()) {
+ var errorState = allocateErrorState(arena);
+ var urbPointerHolder = arena.allocate(ADDRESS);
+ reapURBs(fd, urbPointerHolder, errorState);
+ }
+
+ // reclaim stale URBs
+ var staleTransfers = new ArrayList();
+ synchronized (this) {
+ transfersByURB.entrySet().removeIf(e -> {
+ var urb = e.getKey();
+ var isMatch = usbdevfs_urb.usercontext(urb).address() == fd;
+ if (isMatch) {
+ var transfer = e.getValue();
+ transfer.urb = null;
+ transfer.setResultCode(ENODEV());
+ transfer.setResultSize(0);
+ availableURBs.add(urb);
+ staleTransfers.add(transfer);
+ }
+ return isMatch;
+ });
+ }
+ completeTransfers(staleTransfers);
+ }
+
+ synchronized void submitTransfer(LinuxUsbDevice device, int endpointAddress, UsbTransferType transferType, LinuxTransfer transfer) {
+ if (taskTerminated)
+ throw new UsbException("USB async IO background thread has terminated due to an unrecoverable error; "
+ + "USB transfers are no longer possible");
+
+ linkToUrb(transfer);
+ var urb = transfer.urb;
+
+ usbdevfs_urb.type(urb, (byte) urbTransferType(transferType));
+ usbdevfs_urb.endpoint(urb, (byte) endpointAddress);
+ usbdevfs_urb.buffer(urb, transfer.data());
+ usbdevfs_urb.buffer_length(urb, transfer.dataSize());
+ usbdevfs_urb.usercontext(urb, MemorySegment.ofAddress(device.fileDescriptor()));
+
+ try (var arena = Arena.ofConfined()) {
+ var errorState = allocateErrorState(arena);
+ if (IO.ioctl(device.fileDescriptor(), SUBMITURB, urb, errorState) < 0) {
+ submissionFailed(transfer);
+ var action = endpointAddress >= 128 ? "reading from" : "writing to";
+ var endpoint = endpointAddress == 0 ? "control endpoint" : String.format("endpoint %d", endpointAddress);
+ throwLastError(errorState, "error occurred while %s %s", action, endpoint);
+ }
+ }
+ }
+
+ /**
+ * Undoes the registration performed by {@link #linkToUrb(LinuxTransfer)}.
+ *
+ * Must be called if the {@code SUBMITURB} ioctl for a linked transfer fails. In that
+ * case, the kernel has not queued the URB and it will never be reaped, so its map
+ * entry would leak and the URB would never return to the pool unless they are
+ * cleaned up here.
+ *
+ *
+ * @param transfer transfer whose submission failed
+ */
+ private void submissionFailed(LinuxTransfer transfer) {
+ transfersByURB.remove(transfer.urb);
+ availableURBs.add(transfer.urb);
+ transfer.urb = null;
+ }
+
+ private static int urbTransferType(UsbTransferType transferType) {
+ return switch (transferType) {
+ case BULK -> USBDEVFS_URB_TYPE_BULK();
+ case INTERRUPT -> USBDEVFS_URB_TYPE_INTERRUPT();
+ case CONTROL -> USBDEVFS_URB_TYPE_CONTROL();
+ case ISOCHRONOUS -> USBDEVFS_URB_TYPE_ISO();
+ };
+ }
+
+ /**
+ * Links the specified transfer instance to a URB.
+ *
+ * The transfer is assigned a URB instance, and a list
+ * of associations from URB to transfer is maintained.
+ *
+ * @param transfer the transfer to assign a URB.
+ */
+ private void linkToUrb(LinuxTransfer transfer) {
+ MemorySegment urb;
+ var size = availableURBs.size();
+ if (size > 0) {
+ urb = availableURBs.remove(size - 1);
+ } else {
+ urb = usbdevfs_urb.allocate(urbArena);
+ }
+
+ transfer.urb = urb;
+ transfersByURB.put(urb, transfer);
+ }
+
+ /**
+ * Gets the transfer associated with the specified URB and adds the result.
+ *
+ * The URB is returned into the list of URBs available for further transfers.
+ *
+ *
+ * @param urb URB instance
+ * @return transfer associated with the URB
+ */
+ @SuppressWarnings("java:S2259")
+ private synchronized LinuxTransfer getTransferWithResult(MemorySegment urb) {
+ var transfer = transfersByURB.remove(urb);
+ if (transfer == null)
+ throwException("internal error (unknown URB)");
+
+ transfer.setResultCode(-usbdevfs_urb.status(transfer.urb));
+ transfer.setResultSize(usbdevfs_urb.actual_length(transfer.urb));
+
+ availableURBs.add(transfer.urb);
+ transfer.urb = null;
+
+ return transfer;
+ }
+
+ @SuppressWarnings("java:S1066")
+ synchronized void abortTransfers(LinuxUsbDevice device, byte endpointAddress) {
+ var fd = device.fileDescriptor();
+ try (var arena = Arena.ofConfined()) {
+
+ var errorState = allocateErrorState(arena);
+
+ // iterate all URBs and discard the ones for the specified endpoint
+ transfersByURB.keySet().stream()
+ .filter(urb ->
+ usbdevfs_urb.usercontext(urb).address() == fd
+ && usbdevfs_urb.endpoint(urb) == endpointAddress)
+ .forEach(urb -> {
+ if (IO.ioctl(fd, DISCARDURB, urb, errorState) < 0) {
+ // ignore EINVAL; it occurs if the URB has completed at the same time
+ if (Linux.getErrno(errorState) != errno.EINVAL())
+ throwLastError(errorState, "error occurred while aborting transfer");
+ }
+ }
+ );
+ }
+ }
+
+ private void startAsyncIOTask() {
+ try (var arena = Arena.ofConfined()) {
+ var errorState = allocateErrorState(arena);
+ epollFd = epoll_create1(FD_CLOEXEC(), errorState);
+ if (epollFd < 0)
+ throwLastError(errorState, "internal error (epoll_create)");
+ }
+
+ // start background thread for handling IO completion
+ var thread = new Thread(this::asyncCompletionTask, "USB async IO");
+ thread.setDaemon(true);
+ thread.start();
+ }
+}
diff --git a/java-does-usb/src/main/java/net/codecrete/usb/linux/LinuxEndpointInputStream.java b/java-does-usb/src/main/java/net/codecrete/usb/linux/LinuxEndpointInputStream.java
new file mode 100644
index 00000000..2f48a0e6
--- /dev/null
+++ b/java-does-usb/src/main/java/net/codecrete/usb/linux/LinuxEndpointInputStream.java
@@ -0,0 +1,24 @@
+//
+// Java Does USB
+// Copyright (c) 2023 Manuel Bleichenbacher
+// Licensed under MIT License
+// https://opensource.org/licenses/MIT
+//
+
+package net.codecrete.usb.linux;
+
+import net.codecrete.usb.UsbDirection;
+import net.codecrete.usb.common.EndpointInputStream;
+import net.codecrete.usb.common.Transfer;
+
+public class LinuxEndpointInputStream extends EndpointInputStream {
+
+ LinuxEndpointInputStream(LinuxUsbDevice device, int endpointNumber, int bufferSize) {
+ super(device, endpointNumber, bufferSize);
+ }
+
+ @Override
+ protected void submitTransferIn(Transfer transfer) {
+ ((LinuxUsbDevice) device).submitTransfer(UsbDirection.IN, endpointNumber, (LinuxTransfer) transfer);
+ }
+}
diff --git a/java-does-usb/src/main/java/net/codecrete/usb/linux/LinuxEndpointOutputStream.java b/java-does-usb/src/main/java/net/codecrete/usb/linux/LinuxEndpointOutputStream.java
new file mode 100644
index 00000000..024b8da0
--- /dev/null
+++ b/java-does-usb/src/main/java/net/codecrete/usb/linux/LinuxEndpointOutputStream.java
@@ -0,0 +1,24 @@
+//
+// Java Does USB
+// Copyright (c) 2023 Manuel Bleichenbacher
+// Licensed under MIT License
+// https://opensource.org/licenses/MIT
+//
+
+package net.codecrete.usb.linux;
+
+import net.codecrete.usb.UsbDirection;
+import net.codecrete.usb.common.EndpointOutputStream;
+import net.codecrete.usb.common.Transfer;
+
+public class LinuxEndpointOutputStream extends EndpointOutputStream {
+
+ LinuxEndpointOutputStream(LinuxUsbDevice device, int endpointNumber, int bufferSize) {
+ super(device, endpointNumber, bufferSize);
+ }
+
+ @Override
+ protected void submitTransferOut(Transfer transfer) {
+ ((LinuxUsbDevice) device).submitTransfer(UsbDirection.OUT, endpointNumber, (LinuxTransfer) transfer);
+ }
+}
diff --git a/java-does-usb/src/main/java/net/codecrete/usb/linux/LinuxTransfer.java b/java-does-usb/src/main/java/net/codecrete/usb/linux/LinuxTransfer.java
new file mode 100644
index 00000000..4e40a5e6
--- /dev/null
+++ b/java-does-usb/src/main/java/net/codecrete/usb/linux/LinuxTransfer.java
@@ -0,0 +1,22 @@
+//
+// Java Does USB
+// Copyright (c) 2023 Manuel Bleichenbacher
+// Licensed under MIT License
+// https://opensource.org/licenses/MIT
+//
+
+package net.codecrete.usb.linux;
+
+import net.codecrete.usb.common.Transfer;
+import net.codecrete.usb.linux.gen.usbdevice_fs.usbdevfs_urb;
+
+import java.lang.foreign.MemorySegment;
+
+public class LinuxTransfer extends Transfer {
+ /**
+ * USB request block.
+ *
+ * @see usbdevfs_urb
+ */
+ MemorySegment urb;
+}
diff --git a/java-does-usb/src/main/java/net/codecrete/usb/linux/LinuxUSBDevice.java b/java-does-usb/src/main/java/net/codecrete/usb/linux/LinuxUSBDevice.java
deleted file mode 100644
index fcae2330..00000000
--- a/java-does-usb/src/main/java/net/codecrete/usb/linux/LinuxUSBDevice.java
+++ /dev/null
@@ -1,229 +0,0 @@
-//
-// Java Does USB
-// Copyright (c) 2022 Manuel Bleichenbacher
-// Licensed under MIT License
-// https://opensource.org/licenses/MIT
-//
-
-package net.codecrete.usb.linux;
-
-import net.codecrete.usb.USBControlTransfer;
-import net.codecrete.usb.USBDirection;
-import net.codecrete.usb.USBException;
-import net.codecrete.usb.USBTransferType;
-import net.codecrete.usb.common.DescriptorParser;
-import net.codecrete.usb.common.USBDescriptors;
-import net.codecrete.usb.common.USBDeviceImpl;
-import net.codecrete.usb.common.USBInterfaceImpl;
-import net.codecrete.usb.linux.gen.fcntl.fcntl;
-import net.codecrete.usb.linux.gen.ioctl.ioctl;
-import net.codecrete.usb.linux.gen.unistd.unistd;
-import net.codecrete.usb.linux.gen.usbdevice_fs.usbdevfs_bulktransfer;
-import net.codecrete.usb.linux.gen.usbdevice_fs.usbdevfs_ctrltransfer;
-
-import java.io.IOException;
-import java.lang.foreign.MemorySegment;
-import java.lang.foreign.MemorySession;
-import java.nio.file.Files;
-import java.nio.file.Path;
-
-import static java.lang.foreign.ValueLayout.JAVA_BYTE;
-import static java.lang.foreign.ValueLayout.JAVA_INT;
-
-public class LinuxUSBDevice extends USBDeviceImpl {
-
- private int fd = -1;
-
- LinuxUSBDevice(Object id, int vendorId, int productId, String manufacturer, String product, String serial) {
- super(id, vendorId, productId, manufacturer, product, serial);
- loadDescription((String) id);
- }
-
- private void loadDescription(String path) {
- byte[] descriptors;
- try {
- descriptors = Files.readAllBytes(Path.of(path));
- } catch (IOException e) {
- throw new USBException("Cannot read configuration descriptor", e);
- }
-
- // `descriptors` contains the device descriptor followed by the configuration descriptor
- // (including the interface descriptors, endpoint descriptors etc.)
-
- try (var session = MemorySession.openConfined()) {
- var descriptorsSegment = MemorySegment.ofArray(descriptors);
-
- // separate device descriptor (and copy it to fix alignment issues)
- var deviceDesc = session.allocate(USBDescriptors.Device$Struct);
- deviceDesc.copyFrom(descriptorsSegment.asSlice(0, USBDescriptors.Device$Struct.byteSize()));
-
- int classCode = 255 & (byte) USBDescriptors.Device_bDeviceClass.get(deviceDesc);
- int subclassCode = 255 & (byte) USBDescriptors.Device_bDeviceSubClass.get(deviceDesc);
- int protocolCode = 255 & (byte) USBDescriptors.Device_bDeviceProtocol.get(deviceDesc);
- setClassCodes(classCode, subclassCode, protocolCode);
-
- var usbVersion = (short) USBDescriptors.Device_bcdUSB.get(deviceDesc);
- var deviceVersion = (short) USBDescriptors.Device_bcdDevice.get(deviceDesc);
- setVersions(usbVersion, deviceVersion);
-
- // skip device descriptor
- var configDesc = session.allocateArray(JAVA_BYTE, descriptors.length - 18);
- configDesc.copyFrom(descriptorsSegment.asSlice(USBDescriptors.Device$Struct.byteSize()));
- var configuration = DescriptorParser.parseConfigurationDescriptor(configDesc, vendorId(), productId());
- setInterfaces(configuration.interfaces);
- }
- }
-
- @Override
- public boolean isOpen() {
- return fd != -1;
- }
-
- @Override
- public void open() {
- if (isOpen())
- throw new USBException("the device is already open");
-
- try (var session = MemorySession.openConfined()) {
- var pathUtf8 = session.allocateUtf8String(id_.toString());
- fd = fcntl.open(pathUtf8, fcntl.O_RDWR() | fcntl.O_CLOEXEC());
- if (fd == -1)
- throw new USBException("Cannot open USB device", IO.getErrno());
- }
- }
-
- @Override
- public void close() {
- if (!isOpen())
- return;
-
- for (var intf : interfaces_)
- ((USBInterfaceImpl) intf).setClaimed(false);
-
- unistd.close(fd);
- fd = -1;
- }
-
- public void claimInterface(int interfaceNumber) {
- checkIsOpen();
-
- var intf = getInterface(interfaceNumber);
- if (intf == null)
- throw new USBException(String.format("Invalid interface number: %d", interfaceNumber));
- if (intf.isClaimed())
- throw new USBException(String.format("Interface %d has already been claimed", interfaceNumber));
-
- try (var session = MemorySession.openConfined()) {
- var intfNumSegment = session.allocate(JAVA_INT, interfaceNumber);
- int ret = ioctl.ioctl(fd, USBDevFS.CLAIMINTERFACE, intfNumSegment.address());
- if (ret != 0)
- throw new USBException("Cannot claim USB interface", IO.getErrno());
- setClaimed(interfaceNumber, true);
- }
- }
-
- public void releaseInterface(int interfaceNumber) {
- checkIsOpen();
-
- var intf = getInterface(interfaceNumber);
- if (intf == null)
- throw new USBException(String.format("Invalid interface number: %d", interfaceNumber));
- if (!intf.isClaimed())
- throw new USBException(String.format("Interface %d has not been claimed", interfaceNumber));
-
- try (var session = MemorySession.openConfined()) {
- var intfNumSegment = session.allocate(JAVA_INT, interfaceNumber);
- int ret = ioctl.ioctl(fd, USBDevFS.RELEASEINTERFACE, intfNumSegment.address());
- if (ret != 0)
- throw new USBException("Cannot release USB interface", IO.getErrno());
- setClaimed(interfaceNumber, false);
- }
- }
-
- private MemorySegment createCtrlTransfer(MemorySession session, USBDirection direction, USBControlTransfer setup,
- MemorySegment data) {
- var ctrlTransfer = session.allocate(usbdevfs_ctrltransfer.$LAYOUT());
- var bmRequest =
- (direction == USBDirection.IN ? 0x80 : 0) | (setup.requestType().ordinal() << 5) | setup.recipient().ordinal();
- usbdevfs_ctrltransfer.bRequestType$set(ctrlTransfer, (byte) bmRequest);
- usbdevfs_ctrltransfer.bRequest$set(ctrlTransfer, setup.request());
- usbdevfs_ctrltransfer.wValue$set(ctrlTransfer, setup.value());
- usbdevfs_ctrltransfer.wIndex$set(ctrlTransfer, setup.index());
- usbdevfs_ctrltransfer.wLength$set(ctrlTransfer, (short) data.byteSize());
- usbdevfs_ctrltransfer.data$set(ctrlTransfer, data.address());
- return ctrlTransfer;
- }
-
- @Override
- public byte[] controlTransferIn(USBControlTransfer setup, int length) {
- try (var session = MemorySession.openConfined()) {
- var data = session.allocate(length);
- var ctrlTransfer = createCtrlTransfer(session, USBDirection.IN, setup, data);
-
- int res = ioctl.ioctl(fd, USBDevFS.CONTROL, ctrlTransfer.address());
- if (res < 0)
- throw new USBException("Control IN transfer failed", IO.getErrno());
-
- return data.asSlice(0, res).toArray(JAVA_BYTE);
- }
- }
-
- @Override
- public void controlTransferOut(USBControlTransfer setup, byte[] data) {
- try (var session = MemorySession.openConfined()) {
- int dataLength = data != null ? data.length : 0;
- var buffer = session.allocate(dataLength);
- if (dataLength != 0)
- buffer.copyFrom(MemorySegment.ofArray(data));
- var ctrlTransfer = createCtrlTransfer(session, USBDirection.OUT, setup, buffer);
-
- int res = ioctl.ioctl(fd, USBDevFS.CONTROL, ctrlTransfer.address());
- if (res < 0)
- throw new USBException("Control OUT transfer failed", IO.getErrno());
- }
- }
-
- private MemorySegment createBulkTransfer(MemorySession session, byte endpointAddress, MemorySegment data) {
- var transfer = session.allocate(usbdevfs_bulktransfer.$LAYOUT());
- usbdevfs_bulktransfer.ep$set(transfer, 255 & endpointAddress);
- usbdevfs_bulktransfer.len$set(transfer, (int) data.byteSize());
- usbdevfs_bulktransfer.data$set(transfer, data.address());
- return transfer;
- }
-
- @Override
- public void transferOut(int endpointNumber, byte[] data) {
- var endpointAddress = getEndpointAddress(endpointNumber, USBDirection.OUT,
- USBTransferType.BULK, USBTransferType.INTERRUPT);
-
- try (var session = MemorySession.openConfined()) {
- var buffer = session.allocate(data.length);
- buffer.copyFrom(MemorySegment.ofArray(data));
- var transfer = createBulkTransfer(session, endpointAddress, buffer);
-
- int res = ioctl.ioctl(fd, USBDevFS.BULK, transfer.address());
- if (res < 0)
- throw new USBException(String.format("USB OUT transfer on endpoint %d failed", endpointNumber),
- IO.getErrno());
- }
- }
-
- @Override
- public byte[] transferIn(int endpointNumber, int maxLength) {
- var endpointAddress = getEndpointAddress(endpointNumber, USBDirection.IN,
- USBTransferType.BULK, USBTransferType.INTERRUPT);
-
- try (var session = MemorySession.openConfined()) {
- var buffer = session.allocate(maxLength);
-
- var transfer = createBulkTransfer(session, endpointAddress, buffer);
-
- int res = ioctl.ioctl(fd, USBDevFS.BULK, transfer.address());
- if (res < 0)
- throw new USBException(String.format("USB IN transfer on endpoint %d failed", endpointNumber),
- IO.getErrno());
-
- return buffer.asSlice(0, res).toArray(JAVA_BYTE);
- }
- }
-}
diff --git a/java-does-usb/src/main/java/net/codecrete/usb/linux/LinuxUSBDeviceRegistry.java b/java-does-usb/src/main/java/net/codecrete/usb/linux/LinuxUSBDeviceRegistry.java
deleted file mode 100644
index 2a793579..00000000
--- a/java-does-usb/src/main/java/net/codecrete/usb/linux/LinuxUSBDeviceRegistry.java
+++ /dev/null
@@ -1,240 +0,0 @@
-//
-// Java Does USB
-// Copyright (c) 2022 Manuel Bleichenbacher
-// Licensed under MIT License
-// https://opensource.org/licenses/MIT
-//
-
-package net.codecrete.usb.linux;
-
-import net.codecrete.usb.USBDevice;
-import net.codecrete.usb.USBException;
-import net.codecrete.usb.common.USBDeviceRegistry;
-import net.codecrete.usb.linux.gen.select.fd_set;
-import net.codecrete.usb.linux.gen.select.select;
-import net.codecrete.usb.linux.gen.udev.udev;
-
-import java.lang.foreign.Addressable;
-import java.lang.foreign.MemoryAddress;
-import java.lang.foreign.MemorySegment;
-import java.lang.foreign.MemorySession;
-import java.util.ArrayList;
-import java.util.List;
-
-import static java.lang.foreign.MemoryAddress.NULL;
-import static java.lang.foreign.ValueLayout.JAVA_LONG;
-
-/**
- * Linux implementation of USB device registry.
- */
-public class LinuxUSBDeviceRegistry extends USBDeviceRegistry {
-
- private static final MemorySegment SUBSYSTEM_USB = MemorySession.global().allocateUtf8String("usb");
- private static final MemorySegment MONITOR_NAME = MemorySession.global().allocateUtf8String("udev");
- private static final MemorySegment DEVTYPE_USB_DEVICE = MemorySession.global().allocateUtf8String("usb_device");
-
- @Override
- protected void monitorDevices() {
-
- int fd;
- MemoryAddress monitor;
-
- try {
- // setup udev monitor
- var udevInstance = udev.udev_new();
- if (udevInstance == NULL)
- throw new USBException("internal error (udev_new)");
-
- monitor = udev.udev_monitor_new_from_netlink(udevInstance, MONITOR_NAME);
- if (monitor == NULL)
- throw new USBException("internal error (udev_monitor_new_from_netlink)");
-
- if (udev.udev_monitor_filter_add_match_subsystem_devtype(monitor, SUBSYSTEM_USB, DEVTYPE_USB_DEVICE) < 0)
- throw new USBException("internal error (udev_monitor_filter_add_match_subsystem_devtype)");
-
- if (udev.udev_monitor_enable_receiving(monitor) < 0)
- throw new USBException("internal error (udev_monitor_enable_receiving)");
-
- fd = udev.udev_monitor_get_fd(monitor);
- if (fd < 0)
- throw new USBException("internal error (udev_monitor_get_fd)");
-
- // create initial list of devices
- var deviceList = enumeratePresentDevices(udevInstance);
- setInitialDeviceList(deviceList);
-
- } catch (Throwable e) {
- enumerationFailed(e);
- return;
- }
-
- // monitor device changes
- //noinspection InfiniteLoopStatement
- while (true) {
- try (var session = MemorySession.openConfined()) {
-
- // wait for next change
- waitForFileDescriptor(fd, session);
-
- // retrieve change
- var udevDevice = udev.udev_monitor_receive_device(monitor);
- if (udevDevice == null)
- continue; // shouldn't happen
-
- session.addCloseAction(() -> udev.udev_device_unref(udevDevice));
-
- // get details
- var action = getDeviceAction(udevDevice);
-
- if ("add".equals(action)) {
- onDeviceConnected(udevDevice);
- } else if ("remove".equals(action)) {
- onDeviceDisconnected(udevDevice);
- }
- }
- }
- }
-
- private List enumeratePresentDevices(Addressable udevInstance) {
- List result = new ArrayList<>();
- try (var outerSession = MemorySession.openConfined()) {
-
- // create device enumerator
- var enumerate = udev.udev_enumerate_new(udevInstance);
- if (enumerate == NULL)
- throw new USBException("internal error (udev_enumerate_new)");
-
- outerSession.addCloseAction(() -> udev.udev_enumerate_unref(enumerate));
-
- if (udev.udev_enumerate_add_match_subsystem(enumerate, SUBSYSTEM_USB) < 0)
- throw new USBException("internal error (udev_enumerate_add_match_subsystem)");
-
- if (udev.udev_enumerate_scan_devices(enumerate) < 0)
- throw new USBException("internal error (udev_enumerate_scan_devices)");
-
- // enumerate devices
- for (var entry = udev.udev_enumerate_get_list_entry(enumerate); entry != NULL; entry =
- udev.udev_list_entry_get_next(entry)) {
-
- try (var session = MemorySession.openConfined()) {
-
- var path = udev.udev_list_entry_get_name(entry);
- if (path == NULL)
- continue;
-
- // get device handle
- var dev = udev.udev_device_new_from_syspath(udevInstance, path);
- if (dev == NULL)
- continue;
-
- // ensure the device is released
- session.addCloseAction(() -> udev.udev_device_unref(dev));
-
- // get device details
- var device = getDeviceDetails(dev);
- if (device != null)
- result.add(device);
- }
- }
- }
-
- return result;
- }
-
- private void onDeviceConnected(MemoryAddress udevDevice) {
-
- var device = getDeviceDetails(udevDevice);
- if (device != null)
- addDevice(device);
- }
-
- private void onDeviceDisconnected(MemoryAddress udevDevice) {
-
- var devPath = getDeviceName(udevDevice);
- if (devPath == null)
- return;
-
- closeAndRemoveDevice(devPath);
- }
-
- /**
- * Retrieves the device details and returns a {@code USBDevice} instance.
- *
- * If the device is missing one of vendor ID, product ID or device path,
- * {@code null} is returned.
- *
- *
- * @param udevDevice the device (udev_device*)
- * @return the device instance
- */
- private static USBDevice getDeviceDetails(MemoryAddress udevDevice) {
-
- int vendorId = 0;
- int productId = 0;
-
- try {
- // retrieve device attributes
- String idVendor = getDeviceAttribute(udevDevice, "idVendor");
- if (idVendor == null)
- return null;
-
- String idProduct = getDeviceAttribute(udevDevice, "idProduct");
- if (idProduct == null)
- return null;
-
- // get device path
- var devPath = getDeviceName(udevDevice);
- if (devPath == null)
- return null;
-
- vendorId = Integer.parseInt(idVendor, 16);
- productId = Integer.parseInt(idProduct, 16);
-
- // create device instance
- return new LinuxUSBDevice(devPath, vendorId, productId, getDeviceAttribute(udevDevice, "manufacturer"),
- getDeviceAttribute(udevDevice, "product"), getDeviceAttribute(udevDevice, "serial"));
-
- } catch (Throwable e) {
- System.err.printf("Info: [JavaDoesUSB] failed to retrieve information about device 0x%04x/0x%04x - " +
- "ignoring device%n", vendorId, productId);
- e.printStackTrace(System.err);
- return null;
- }
- }
-
- private static String getDeviceAttribute(Addressable udevDevice, String attribute) {
- try (var session = MemorySession.openConfined()) {
- var sysattr = session.allocateUtf8String(attribute);
- var valueAddr = udev.udev_device_get_sysattr_value(udevDevice, sysattr);
- if (valueAddr == NULL)
- return null;
-
- var value = MemorySegment.ofAddress(valueAddr, 2000, session);
- return value.getUtf8String(0);
- }
- }
-
- private static String getDeviceName(Addressable udevDevice) {
- return Linux.createStringFromAddress(udev.udev_device_get_devnode(udevDevice));
- }
-
- private static String getDeviceAction(Addressable udevDevice) {
- return Linux.createStringFromAddress(udev.udev_device_get_action(udevDevice));
- }
-
- /**
- * Waits until the specified file descriptor becomes ready for reading.
- *
- * @param fd the file descriptor
- * @param session a memory session for allocating memory
- */
- private static void waitForFileDescriptor(int fd, MemorySession session) {
- // fd_set is a bit array (constructed from 64-bit integers)
- var fds = session.allocate(fd_set.$LAYOUT());
- fds.set(JAVA_LONG, fd / JAVA_LONG.bitSize(), 1L << (fd % JAVA_LONG.bitSize()));
-
- int res = select.select(fd + 1, fds, NULL, NULL, NULL);
- if (res <= 0)
- throw new USBException("internal error (select)");
- }
-}
diff --git a/java-does-usb/src/main/java/net/codecrete/usb/linux/LinuxUsbDevice.java b/java-does-usb/src/main/java/net/codecrete/usb/linux/LinuxUsbDevice.java
new file mode 100644
index 00000000..d0c87f09
--- /dev/null
+++ b/java-does-usb/src/main/java/net/codecrete/usb/linux/LinuxUsbDevice.java
@@ -0,0 +1,358 @@
+//
+// Java Does USB
+// Copyright (c) 2022 Manuel Bleichenbacher
+// Licensed under MIT License
+// https://opensource.org/licenses/MIT
+//
+
+package net.codecrete.usb.linux;
+
+import net.codecrete.usb.UsbControlTransfer;
+import net.codecrete.usb.UsbDirection;
+import net.codecrete.usb.UsbException;
+import net.codecrete.usb.UsbTransferType;
+import net.codecrete.usb.common.Transfer;
+import net.codecrete.usb.common.UsbDeviceImpl;
+import net.codecrete.usb.common.UsbInterfaceImpl;
+import net.codecrete.usb.linux.gen.fcntl.fcntl;
+import net.codecrete.usb.linux.gen.unistd.unistd;
+import net.codecrete.usb.linux.gen.usbdevice_fs.usbdevfs_disconnect_claim;
+import net.codecrete.usb.linux.gen.usbdevice_fs.usbdevfs_ioctl;
+import net.codecrete.usb.linux.gen.usbdevice_fs.usbdevfs_setinterface;
+import net.codecrete.usb.linux.gen.usbdevice_fs.usbdevice_fs;
+import net.codecrete.usb.usbstandard.DeviceDescriptor;
+import net.codecrete.usb.usbstandard.SetupPacket;
+import org.jetbrains.annotations.NotNull;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.lang.foreign.Arena;
+import java.lang.foreign.MemorySegment;
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+import static java.lang.foreign.ValueLayout.JAVA_BYTE;
+import static java.lang.foreign.ValueLayout.JAVA_INT;
+import static net.codecrete.usb.linux.Linux.allocateErrorState;
+import static net.codecrete.usb.linux.LinuxUsbException.throwException;
+import static net.codecrete.usb.linux.LinuxUsbException.throwLastError;
+
+@SuppressWarnings("java:S2160")
+public class LinuxUsbDevice extends UsbDeviceImpl {
+
+ private static final MemorySegment DRIVER_NAME_USBFS = Arena.global().allocateFrom("usbfs");
+
+ // volatile: written under the device monitor, read unlocked via isOpened()
+ private volatile int fd = -1;
+
+ private final LinuxAsyncTask asyncTask;
+
+ private boolean detachDrivers = false;
+
+ LinuxUsbDevice(Object id, int vendorId, int productId) {
+ super(id, vendorId, productId);
+ asyncTask = LinuxAsyncTask.INSTANCE;
+ loadDescription((String) id);
+ }
+
+ private void loadDescription(String path) {
+ byte[] descriptors;
+ try {
+ descriptors = Files.readAllBytes(Path.of(path));
+ } catch (IOException e) {
+ throw new UsbException("reading configuration descriptor failed", e);
+ }
+
+ // `descriptors` contains the device descriptor followed by the configuration descriptor
+ // (including the interface descriptors, endpoint descriptors etc.)
+ var descriptorsSegment = MemorySegment.ofArray(descriptors);
+ setFromDeviceDescriptor(descriptorsSegment.asSlice(0, DeviceDescriptor.LAYOUT));
+ setConfigurationDescriptor(descriptorsSegment.asSlice(DeviceDescriptor.LAYOUT.byteSize()));
+ }
+
+ @Override
+ public synchronized void detachStandardDrivers() {
+ checkIsClosed("detachStandardDrivers() must not be called while the device is open");
+ detachDrivers = true;
+ }
+
+ @Override
+ public synchronized void attachStandardDrivers() {
+ checkIsClosed("attachStandardDrivers() must not be called while the device is open");
+ detachDrivers = false;
+ }
+
+ @Override
+ public boolean isOpened() {
+ return fd != -1;
+ }
+
+ @Override
+ public synchronized void open() {
+ checkIsClosed("device is already open");
+
+ try (var arena = Arena.ofConfined()) {
+ var pathUtf8 = arena.allocateFrom(uniqueDeviceId.toString());
+ var errorState = allocateErrorState(arena);
+ fd = IO.open(pathUtf8, fcntl.O_RDWR() | fcntl.O_CLOEXEC(), errorState);
+ if (fd == -1)
+ throwLastError(errorState, "opening USB device failed");
+ asyncTask.addForAsyncIOCompletion(this);
+ }
+ }
+
+ @Override
+ public synchronized void close() {
+ if (!isOpened())
+ return;
+
+ asyncTask.removeFromAsyncIOCompletion(this);
+
+ for (var intf : interfaceList)
+ ((UsbInterfaceImpl) intf).setClaimed(false);
+
+ unistd.close(fd);
+ fd = -1;
+ }
+
+ int fileDescriptor() {
+ return fd;
+ }
+
+ public synchronized void claimInterface(int interfaceNumber) {
+ checkIsOpen();
+
+ getInterfaceWithCheck(interfaceNumber, false);
+
+ try (var arena = Arena.ofConfined()) {
+ var errorState = allocateErrorState(arena);
+ int ret;
+
+ if (detachDrivers) {
+ // claim interface (detaching kernel driver)
+ var disconnectClaim = usbdevfs_disconnect_claim.allocate(arena);
+ usbdevfs_disconnect_claim.interface_(disconnectClaim, interfaceNumber);
+ usbdevfs_disconnect_claim.flags(disconnectClaim, usbdevice_fs.USBDEVFS_DISCONNECT_CLAIM_EXCEPT_DRIVER());
+ usbdevfs_disconnect_claim.driver(disconnectClaim).copyFrom(DRIVER_NAME_USBFS);
+ ret = IO.ioctl(fd, UsbDevFS.DISCONNECT_CLAIM, disconnectClaim, errorState);
+
+ } else {
+ // claim interface (without detaching kernel driver)
+ var intfNumSegment = arena.allocate(JAVA_INT);
+ intfNumSegment.setAtIndex(JAVA_INT, 0, interfaceNumber);
+ ret = IO.ioctl(fd, UsbDevFS.CLAIMINTERFACE, intfNumSegment, errorState);
+ }
+
+ if (ret != 0)
+ throwLastError(errorState, "claiming USB interface failed");
+
+ setClaimed(interfaceNumber, true);
+ }
+ }
+
+ @Override
+ public synchronized void selectAlternateSetting(int interfaceNumber, int alternateNumber) {
+ checkIsOpen();
+
+ var intf = getInterfaceWithCheck(interfaceNumber, true);
+
+ // check alternate setting
+ var altSetting = intf.getAlternate(alternateNumber);
+
+ try (var arena = Arena.ofConfined()) {
+ var setIntfSegment = usbdevfs_setinterface.allocate(arena);
+ usbdevfs_setinterface.interface_(setIntfSegment, interfaceNumber);
+ usbdevfs_setinterface.altsetting(setIntfSegment, alternateNumber);
+ var errorState = allocateErrorState(arena);
+ var ret = IO.ioctl(fd, UsbDevFS.SETINTERFACE, setIntfSegment, errorState);
+ if (ret != 0)
+ throwLastError(errorState, "setting alternate interface failed");
+ }
+
+ intf.setAlternate(altSetting);
+ }
+
+ public synchronized void releaseInterface(int interfaceNumber) {
+ checkIsOpen();
+
+ getInterfaceWithCheck(interfaceNumber, true);
+
+ try (var arena = Arena.ofConfined()) {
+ var intfNumSegment = arena.allocate(JAVA_INT);
+ intfNumSegment.setAtIndex(JAVA_INT, 0, interfaceNumber);
+ var errorState = allocateErrorState(arena);
+ var ret = IO.ioctl(fd, UsbDevFS.RELEASEINTERFACE, intfNumSegment, errorState);
+ if (ret != 0)
+ throwLastError(errorState, "releasing USB interface failed");
+
+ setClaimed(interfaceNumber, false);
+
+ if (detachDrivers) {
+ // reattach kernel driver
+ var request = usbdevfs_ioctl.allocate(arena);
+ usbdevfs_ioctl.ifno(request, interfaceNumber);
+ usbdevfs_ioctl.ioctl_code(request, UsbDevFS.CONNECT);
+ usbdevfs_ioctl.data(request, MemorySegment.NULL);
+ IO.ioctl(fd, UsbDevFS.IOCTL, request, errorState);
+ }
+ }
+ }
+
+ @Override
+ public void controlTransferOut(@NotNull UsbControlTransfer setup, byte[] data) {
+ try (var arena = Arena.ofConfined()) {
+ var dataLength = data != null ? data.length : 0;
+ var transfer = createSyncCtrlTransfer(arena, UsbDirection.OUT, setup, dataLength);
+ if (dataLength != 0)
+ transfer.data().asSlice(8).copyFrom(MemorySegment.ofArray(data));
+
+ synchronized (transfer) {
+ submitTransfer(UsbDirection.OUT, 0, transfer);
+ waitForTransfer(transfer, 0, UsbDirection.OUT, 0);
+ }
+ }
+ }
+
+ @Override
+ public byte @NotNull [] controlTransferIn(@NotNull UsbControlTransfer setup, int length) {
+ try (var arena = Arena.ofConfined()) {
+ var transfer = createSyncCtrlTransfer(arena, UsbDirection.IN, setup, length);
+
+ synchronized (transfer) {
+ submitTransfer(UsbDirection.IN, 0, transfer);
+ waitForTransfer(transfer, 0, UsbDirection.IN, 0);
+ }
+
+ return transfer.data().asSlice(8, transfer.resultSize()).toArray(JAVA_BYTE);
+ }
+ }
+
+ /**
+ * Create transfer object for synchronous control request.
+ *
+ * @param arena arena for allocating memory
+ * @param direction direction
+ * @param setup setup data
+ * @param dataLength data length (in addition to setup data)
+ * @return transfer object
+ */
+ private LinuxTransfer createSyncCtrlTransfer(Arena arena, UsbDirection direction, UsbControlTransfer setup,
+ int dataLength) {
+ var bmRequest =
+ (direction == UsbDirection.IN ? 0x80 : 0) | (setup.requestType().ordinal() << 5) | setup.recipient().ordinal();
+ var buffer = arena.allocate(8L + dataLength, 8);
+ var setupPacket = new SetupPacket(buffer);
+ setupPacket.setRequestType(bmRequest);
+ setupPacket.setRequest(setup.request());
+ setupPacket.setValue(setup.value());
+ setupPacket.setIndex(setup.index());
+ setupPacket.setLength(dataLength);
+
+ var transfer = new LinuxTransfer();
+ transfer.setData(buffer);
+ transfer.setDataSize((int) buffer.byteSize());
+ transfer.setResultSize(-1);
+ transfer.setCompletion(UsbDeviceImpl::onSyncTransferCompleted);
+
+ return transfer;
+ }
+
+ @Override
+ public void transferOut(int endpointNumber, byte @NotNull [] data, int offset, int length, int timeout) {
+ // Auto arena: a transfer that times out may be abandoned (see UsbDeviceImpl.waitForTransfer),
+ // so the buffer must outlive a possible late completion instead of being freed deterministically.
+ var arena = Arena.ofAuto();
+ var buffer = arena.allocate(length);
+ buffer.copyFrom(MemorySegment.ofArray(data).asSlice(offset, length));
+ var transfer = createSyncTransfer(buffer);
+
+ synchronized (transfer) {
+ submitTransfer(UsbDirection.OUT, endpointNumber, transfer);
+ waitForTransfer(transfer, timeout, UsbDirection.OUT, endpointNumber);
+ }
+ }
+
+ @Override
+ public byte @NotNull [] transferIn(int endpointNumber, int timeout) {
+ var endpoint = getEndpoint(UsbDirection.IN, endpointNumber, UsbTransferType.BULK, UsbTransferType.INTERRUPT);
+
+ // Auto arena: a transfer that times out may be abandoned (see UsbDeviceImpl.waitForTransfer),
+ // so the buffer must outlive a possible late completion instead of being freed deterministically.
+ var arena = Arena.ofAuto();
+ var buffer = arena.allocate(endpoint.packetSize());
+ var transfer = createSyncTransfer(buffer);
+
+ synchronized (transfer) {
+ submitTransfer(UsbDirection.IN, endpointNumber, transfer);
+ waitForTransfer(transfer, timeout, UsbDirection.IN, endpointNumber);
+ }
+
+ return buffer.asSlice(0, transfer.resultSize()).toArray(JAVA_BYTE);
+ }
+
+ private LinuxTransfer createSyncTransfer(MemorySegment data) {
+ var transfer = new LinuxTransfer();
+ transfer.setData(data);
+ transfer.setDataSize((int) data.byteSize());
+ transfer.setResultSize(-1);
+ transfer.setCompletion(UsbDeviceImpl::onSyncTransferCompleted);
+ return transfer;
+ }
+
+ synchronized void submitTransfer(UsbDirection direction, int endpointNumber, LinuxTransfer transfer) {
+ if (endpointNumber != 0) {
+ var endpoint = getEndpoint(direction, endpointNumber, UsbTransferType.BULK, UsbTransferType.INTERRUPT);
+ asyncTask.submitTransfer(this, endpoint.endpointAddress(), endpoint.transferType(), transfer);
+ } else {
+ asyncTask.submitTransfer(this, 0, UsbTransferType.CONTROL, transfer);
+ }
+ }
+
+ @Override
+ protected Transfer createTransfer() {
+ return new LinuxTransfer();
+ }
+
+ @Override
+ protected void throwOSException(int errorCode, String message, Object... args) {
+ throwException(errorCode, message, args);
+ }
+
+ @Override
+ public void clearHalt(UsbDirection direction, int endpointNumber) {
+ var endpoint = getEndpoint(direction, endpointNumber, UsbTransferType.BULK, UsbTransferType.INTERRUPT);
+
+ try (var arena = Arena.ofConfined()) {
+ var endpointAddrSegment = arena.allocate(JAVA_INT);
+ endpointAddrSegment.setAtIndex(JAVA_INT, 0, endpoint.endpointAddress() & 0xff);
+ var errorState = allocateErrorState(arena);
+ var res = IO.ioctl(fd, UsbDevFS.CLEAR_HALT, endpointAddrSegment, errorState);
+ if (res < 0)
+ throwLastError(errorState, "clearing halt failed");
+ }
+ }
+
+ @Override
+ public synchronized void abortTransfers(UsbDirection direction, int endpointNumber) {
+ var endpoint = getEndpoint(direction, endpointNumber, UsbTransferType.BULK, UsbTransferType.INTERRUPT);
+
+ asyncTask.abortTransfers(this, endpoint.endpointAddress());
+ }
+
+ @Override
+ public synchronized @NotNull InputStream openInputStream(int endpointNumber, int bufferSize) {
+ // check that endpoint number is valid
+ getEndpoint(UsbDirection.IN, endpointNumber, UsbTransferType.BULK, null);
+
+ return new LinuxEndpointInputStream(this, endpointNumber, bufferSize);
+ }
+
+ @Override
+ public synchronized @NotNull OutputStream openOutputStream(int endpointNumber, int bufferSize) {
+ // check that endpoint number is valid
+ getEndpoint(UsbDirection.OUT, endpointNumber, UsbTransferType.BULK, null);
+
+ return new LinuxEndpointOutputStream(this, endpointNumber, bufferSize);
+ }
+}
diff --git a/java-does-usb/src/main/java/net/codecrete/usb/linux/LinuxUsbDeviceRegistry.java b/java-does-usb/src/main/java/net/codecrete/usb/linux/LinuxUsbDeviceRegistry.java
new file mode 100644
index 00000000..6c39abe9
--- /dev/null
+++ b/java-does-usb/src/main/java/net/codecrete/usb/linux/LinuxUsbDeviceRegistry.java
@@ -0,0 +1,274 @@
+//
+// Java Does USB
+// Copyright (c) 2022 Manuel Bleichenbacher
+// Licensed under MIT License
+// https://opensource.org/licenses/MIT
+//
+
+package net.codecrete.usb.linux;
+
+import net.codecrete.usb.UsbDevice;
+import net.codecrete.usb.common.ScopeCleanup;
+import net.codecrete.usb.common.UsbDeviceRegistry;
+import net.codecrete.usb.linux.gen.udev.udev;
+
+import java.lang.foreign.Arena;
+import java.lang.foreign.MemorySegment;
+import java.util.ArrayList;
+import java.util.List;
+
+import static java.lang.System.Logger.Level.INFO;
+import static java.lang.foreign.MemorySegment.NULL;
+import static net.codecrete.usb.linux.EPoll.epoll_create1;
+import static net.codecrete.usb.linux.EPoll.epoll_wait;
+import static net.codecrete.usb.linux.Linux.allocateErrorState;
+import static net.codecrete.usb.linux.LinuxUsbException.throwException;
+import static net.codecrete.usb.linux.LinuxUsbException.throwLastError;
+import static net.codecrete.usb.linux.gen.epoll.epoll.EPOLLIN;
+import static net.codecrete.usb.linux.gen.errno.errno.EINTR;
+import static net.codecrete.usb.linux.gen.fcntl.fcntl.FD_CLOEXEC;
+
+/**
+ * Linux implementation of USB device registry.
+ */
+public class LinuxUsbDeviceRegistry extends UsbDeviceRegistry {
+
+ private static final System.Logger LOG = System.getLogger(LinuxUsbDeviceRegistry.class.getName());
+
+ private static final MemorySegment SUBSYSTEM_USB;
+ private static final MemorySegment MONITOR_NAME;
+ private static final MemorySegment DEVTYPE_USB_DEVICE;
+
+ private static final MemorySegment ATTR_ID_VENDOR;
+ private static final MemorySegment ATTR_ID_PRODUCT;
+ private static final MemorySegment ATTR_MANUFACTURER;
+ private static final MemorySegment ATTR_PRODUCT;
+ private static final MemorySegment ATTR_SERIAL;
+
+ private MemorySegment monitor;
+ private int monitorFd;
+
+ static {
+ var global = Arena.global();
+
+ SUBSYSTEM_USB = global.allocateFrom("usb");
+ MONITOR_NAME = global.allocateFrom("udev");
+ DEVTYPE_USB_DEVICE = global.allocateFrom("usb_device");
+
+ ATTR_ID_VENDOR = global.allocateFrom("idVendor");
+ ATTR_ID_PRODUCT = global.allocateFrom("idProduct");
+ ATTR_MANUFACTURER = global.allocateFrom("manufacturer");
+ ATTR_PRODUCT = global.allocateFrom("product");
+ ATTR_SERIAL = global.allocateFrom("serial");
+ }
+
+ @SuppressWarnings("java:S1181")
+ private boolean setupMonitor() {
+ try {
+ // setup udev monitor
+ var udevInstance = udev.udev_new();
+ if (udevInstance.address() == 0)
+ throwException("internal error (udev_new)");
+
+ monitor = udev.udev_monitor_new_from_netlink(udevInstance, MONITOR_NAME);
+ if (monitor.address() == 0)
+ throwException("internal error (udev_monitor_new_from_netlink)");
+
+ if (udev.udev_monitor_filter_add_match_subsystem_devtype(monitor, SUBSYSTEM_USB, DEVTYPE_USB_DEVICE) < 0)
+ throwException("internal error (udev_monitor_filter_add_match_subsystem_devtype)");
+
+ if (udev.udev_monitor_enable_receiving(monitor) < 0)
+ throwException("internal error (udev_monitor_enable_receiving)");
+
+ monitorFd = udev.udev_monitor_get_fd(monitor);
+ if (monitorFd < 0)
+ throwException("internal error (udev_monitor_get_fd)");
+
+ // create initial list of devices
+ var deviceList = enumeratePresentDevices(udevInstance);
+ setInitialDeviceList(deviceList);
+ return true;
+
+ } catch (Throwable e) {
+ enumerationFailed(e);
+ return false;
+ }
+ }
+
+ @SuppressWarnings("java:S2189")
+ @Override
+ protected void monitorDevices() {
+ if (!setupMonitor())
+ return;
+
+ try (var arena = Arena.ofConfined()) {
+ // create epoll
+ var errorState = allocateErrorState(arena);
+ var epfd = epoll_create1(FD_CLOEXEC(), errorState);
+ if (epfd < 0)
+ throwLastError(errorState, "internal error (epoll_create)");
+ EPoll.addFileDescriptor(epfd, EPOLLIN(), monitorFd);
+
+ // allocate event (as output for epoll_wait)
+ var event = arena.allocate(EPoll.EVENT$LAYOUT);
+
+ // monitor device changes
+ //noinspection InfiniteLoopStatement
+ while (true) {
+ try (var cleanup = new ScopeCleanup()) {
+
+ // wait for next change
+ int res = epoll_wait(epfd, event, 1, -1, errorState);
+ if (res < 0) {
+ var err = Linux.getErrno(errorState);
+ if (err == EINTR())
+ continue; // continue on interrupt
+ throwException(err, "internal error (epoll_wait)");
+ }
+
+ // retrieve change
+ var udevDevice = udev.udev_monitor_receive_device(monitor);
+ if (udevDevice != NULL) {
+ cleanup.add(() -> udev.udev_device_unref(udevDevice));
+
+ // get details
+ var action = getDeviceAction(udevDevice);
+
+ if ("add".equals(action)) {
+ onDeviceConnected(udevDevice);
+ } else if ("remove".equals(action)) {
+ onDeviceDisconnected(udevDevice);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ @SuppressWarnings("java:S135")
+ private List enumeratePresentDevices(MemorySegment udevInstance) {
+ List result = new ArrayList<>();
+ try (var outerCleanup = new ScopeCleanup()) {
+
+ // create device enumerator
+ var enumerate = udev.udev_enumerate_new(udevInstance);
+ if (enumerate.address() == 0)
+ throwException("internal error (udev_enumerate_new)");
+
+ outerCleanup.add(() -> udev.udev_enumerate_unref(enumerate));
+
+ if (udev.udev_enumerate_add_match_subsystem(enumerate, SUBSYSTEM_USB) < 0)
+ throwException("internal error (udev_enumerate_add_match_subsystem)");
+
+ if (udev.udev_enumerate_scan_devices(enumerate) < 0)
+ throwException("internal error (udev_enumerate_scan_devices)");
+
+ // enumerate devices
+ for (var entry = udev.udev_enumerate_get_list_entry(enumerate); entry.address() != 0; entry =
+ udev.udev_list_entry_get_next(entry)) {
+
+ try (var cleanup = new ScopeCleanup()) {
+
+ var path = udev.udev_list_entry_get_name(entry);
+ if (path.address() == 0)
+ continue;
+
+ // get device handle
+ var dev = udev.udev_device_new_from_syspath(udevInstance, path);
+ if (dev.address() == 0)
+ continue;
+
+ // ensure the device is released
+ cleanup.add(() -> udev.udev_device_unref(dev));
+
+ // get device details
+ var device = getDeviceDetails(dev);
+ if (device != null)
+ result.add(device);
+ }
+ }
+ }
+
+ return result;
+ }
+
+ private void onDeviceConnected(MemorySegment udevDevice) {
+
+ var device = getDeviceDetails(udevDevice);
+ if (device != null)
+ addDevice(device);
+ }
+
+ private void onDeviceDisconnected(MemorySegment udevDevice) {
+
+ var devPath = getDeviceName(udevDevice);
+ if (devPath == null)
+ return;
+
+ closeAndRemoveDevice(devPath);
+ }
+
+ /**
+ * Retrieves the device details and returns a {@code UsbDevice} instance.
+ *
+ * If the device is missing one of vendor ID, product ID or device path,
+ * {@code null} is returned.
+ *
+ *
+ * @param udevDevice the device (udev_device*)
+ * @return the device instance
+ */
+ @SuppressWarnings("java:S106")
+ private UsbDevice getDeviceDetails(MemorySegment udevDevice) {
+
+ int vendorId = 0;
+ int productId = 0;
+
+ try {
+ // retrieve device attributes
+ String idVendor = getDeviceAttribute(udevDevice, ATTR_ID_VENDOR);
+ if (idVendor == null)
+ return null;
+
+ String idProduct = getDeviceAttribute(udevDevice, ATTR_ID_PRODUCT);
+ if (idProduct == null)
+ return null;
+
+ // get device path
+ var devPath = getDeviceName(udevDevice);
+ if (devPath == null)
+ return null;
+
+ vendorId = Integer.parseInt(idVendor, 16);
+ productId = Integer.parseInt(idProduct, 16);
+
+ // create device instance
+ var device = new LinuxUsbDevice(devPath, vendorId, productId);
+
+ device.setProductStrings(getDeviceAttribute(udevDevice, ATTR_MANUFACTURER), getDeviceAttribute(udevDevice
+ , ATTR_PRODUCT), getDeviceAttribute(udevDevice, ATTR_SERIAL));
+
+ return device;
+
+ } catch (Exception e) {
+ LOG.log(INFO, String.format("failed to retrieve information about device 0x%04x/0x%04x - ignoring device", vendorId, productId), e);
+ return null;
+ }
+ }
+
+ private static String getDeviceAttribute(MemorySegment udevDevice, MemorySegment attribute) {
+ var value = udev.udev_device_get_sysattr_value(udevDevice, attribute);
+ if (value.address() == 0)
+ return null;
+
+ return value.getString(0);
+ }
+
+ private static String getDeviceName(MemorySegment udevDevice) {
+ return udev.udev_device_get_devnode(udevDevice).getString(0);
+ }
+
+ private static String getDeviceAction(MemorySegment udevDevice) {
+ return udev.udev_device_get_action(udevDevice).getString(0);
+ }
+}
diff --git a/java-does-usb/src/main/java/net/codecrete/usb/linux/LinuxUsbException.java b/java-does-usb/src/main/java/net/codecrete/usb/linux/LinuxUsbException.java
new file mode 100644
index 00000000..4f0d95c5
--- /dev/null
+++ b/java-does-usb/src/main/java/net/codecrete/usb/linux/LinuxUsbException.java
@@ -0,0 +1,77 @@
+//
+// Java Does USB
+// Copyright (c) 2022 Manuel Bleichenbacher
+// Licensed under MIT License
+// https://opensource.org/licenses/MIT
+//
+package net.codecrete.usb.linux;
+
+import net.codecrete.usb.UsbException;
+import net.codecrete.usb.UsbStallException;
+import net.codecrete.usb.linux.gen.errno.errno;
+
+import java.lang.foreign.MemorySegment;
+
+/**
+ * Exception thrown if a Linux specific error occurs.
+ */
+public class LinuxUsbException extends UsbException {
+
+ /**
+ * Creates a new instance.
+ *
+ * The message for the Linux error code is looked up and appended to the message.
+ *
+ *
+ * @param message exception message
+ * @param errorCode Linux error code (returned by {@code errno})
+ */
+ public LinuxUsbException(String message, int errorCode) {
+ super(String.format("%s: %s", message, Linux.getErrorMessage(errorCode)), errorCode);
+ }
+
+ /**
+ * Throws an exception for the specified Linux error code.
+ *
+ * The message for the Linux error code is looked up and appended to the message.
+ *
+ *
+ * @param errorCode Linux error code (returned by {@code errno})
+ * @param message exception message format ({@link String#format(String, Object...)} style)
+ * @param args arguments for exception message
+ */
+ static void throwException(int errorCode, String message, Object... args) {
+ var formattedMessage = String.format(message, args);
+ if (errorCode == errno.EPIPE()) {
+ throw new UsbStallException(formattedMessage);
+ } else {
+ throw new LinuxUsbException(formattedMessage, errorCode);
+ }
+ }
+
+ /**
+ * Throws a USB exception.
+ *
+ * @param message exception message format ({@link String#format(String, Object...)} style)
+ * @param args arguments for exception message
+ */
+ static void throwException(String message, Object... args) {
+ throw new UsbException(String.format(message, args));
+ }
+
+ /**
+ * Throws an exception for the last error.
+ *
+ * The message of the last Linux error code is provided in a memory segment with the layout
+ * {@link Linux#ERRNO_STATE}.
+ *
+ *
+ * @param errorState segment with error state
+ * @param message exception message format ({@link String#format(String, Object...)} style)
+ * @param args arguments for exception message
+ */
+ static void throwLastError(MemorySegment errorState, String message, Object... args) {
+ throwException(Linux.getErrno(errorState), message, args);
+ }
+
+}
diff --git a/java-does-usb/src/main/java/net/codecrete/usb/linux/USBDevFS.java b/java-does-usb/src/main/java/net/codecrete/usb/linux/USBDevFS.java
deleted file mode 100644
index 96b6c011..00000000
--- a/java-does-usb/src/main/java/net/codecrete/usb/linux/USBDevFS.java
+++ /dev/null
@@ -1,19 +0,0 @@
-//
-// Java Does USB
-// Copyright (c) 2022 Manuel Bleichenbacher
-// Licensed under MIT License
-// https://opensource.org/licenses/MIT
-//
-
-package net.codecrete.usb.linux;
-
-/**
- * Data structures and constants related to the USB device file system.
- */
-public class USBDevFS {
-
- public static final long CONTROL = 0xc0185500;
- public static final long BULK = 0xc0185502;
- public static final long CLAIMINTERFACE = 0x8004550F;
- public static final long RELEASEINTERFACE = 0x80045510;
-}
diff --git a/java-does-usb/src/main/java/net/codecrete/usb/linux/UsbDevFS.java b/java-does-usb/src/main/java/net/codecrete/usb/linux/UsbDevFS.java
new file mode 100644
index 00000000..15f1d270
--- /dev/null
+++ b/java-does-usb/src/main/java/net/codecrete/usb/linux/UsbDevFS.java
@@ -0,0 +1,33 @@
+//
+// Java Does USB
+// Copyright (c) 2022 Manuel Bleichenbacher
+// Licensed under MIT License
+// https://opensource.org/licenses/MIT
+//
+
+package net.codecrete.usb.linux;
+
+/**
+ * Data structures and constants related to the USB device file system.
+ *
+ * The usbdev_fs header files compute these constants using function like macros.
+ * Thus, they cannot be generated using jextract.
+ *
+ */
+class UsbDevFS {
+
+ private UsbDevFS() {
+ }
+
+ // constants that jextract cannot generate as they are built from function-like macros
+ static final long CLAIMINTERFACE = 0x8004550FL;
+ static final long RELEASEINTERFACE = 0x80045510L;
+ static final long SETINTERFACE = 0x80085504L;
+ static final long CLEAR_HALT = 0x80045515L;
+ static final long SUBMITURB = 0x8038550AL;
+ static final long DISCARDURB = 0x550BL;
+ static final long REAPURBNDELAY = 0x4008550DL;
+ static final long DISCONNECT_CLAIM = 0x8108551BL;
+ static final int CONNECT = 0x5517;
+ static final long IOCTL = 0xC0105512L;
+}
diff --git a/java-does-usb/src/main/java/net/codecrete/usb/linux/gen/epoll/epoll$shared.java b/java-does-usb/src/main/java/net/codecrete/usb/linux/gen/epoll/epoll$shared.java
new file mode 100644
index 00000000..5cb27567
--- /dev/null
+++ b/java-does-usb/src/main/java/net/codecrete/usb/linux/gen/epoll/epoll$shared.java
@@ -0,0 +1,63 @@
+// Generated by jextract
+
+package net.codecrete.usb.linux.gen.epoll;
+
+import java.lang.invoke.*;
+import java.lang.foreign.*;
+import java.nio.ByteOrder;
+import java.util.*;
+import java.util.function.*;
+import java.util.stream.*;
+
+import static java.lang.foreign.ValueLayout.*;
+import static java.lang.foreign.MemoryLayout.PathElement.*;
+
+public class epoll$shared {
+
+ epoll$shared() {
+ // Should not be called directly
+ }
+
+ public static final ValueLayout.OfBoolean C_BOOL = (ValueLayout.OfBoolean) Linker.nativeLinker().canonicalLayouts().get("bool");
+ public static final ValueLayout.OfByte C_CHAR =(ValueLayout.OfByte)Linker.nativeLinker().canonicalLayouts().get("char");
+ public static final ValueLayout.OfShort C_SHORT = (ValueLayout.OfShort) Linker.nativeLinker().canonicalLayouts().get("short");
+ public static final ValueLayout.OfInt C_INT = (ValueLayout.OfInt) Linker.nativeLinker().canonicalLayouts().get("int");
+ public static final ValueLayout.OfLong C_LONG_LONG = (ValueLayout.OfLong) Linker.nativeLinker().canonicalLayouts().get("long long");
+ public static final ValueLayout.OfFloat C_FLOAT = (ValueLayout.OfFloat) Linker.nativeLinker().canonicalLayouts().get("float");
+ public static final ValueLayout.OfDouble C_DOUBLE = (ValueLayout.OfDouble) Linker.nativeLinker().canonicalLayouts().get("double");
+ public static final AddressLayout C_POINTER = ((AddressLayout) Linker.nativeLinker().canonicalLayouts().get("void*"))
+ .withTargetLayout(MemoryLayout.sequenceLayout(java.lang.Long.MAX_VALUE, C_CHAR));
+ public static final ValueLayout.OfLong C_LONG = (ValueLayout.OfLong) Linker.nativeLinker().canonicalLayouts().get("long");
+
+ static final boolean TRACE_DOWNCALLS = Boolean.getBoolean("jextract.trace.downcalls");
+
+ static void traceDowncall(String name, Object... args) {
+ String traceArgs = Arrays.stream(args)
+ .map(Object::toString)
+ .collect(Collectors.joining(", "));
+ System.out.printf("%s(%s)\n", name, traceArgs);
+ }
+
+ static MethodHandle upcallHandle(Class> fi, String name, FunctionDescriptor fdesc) {
+ try {
+ return MethodHandles.lookup().findVirtual(fi, name, fdesc.toMethodType());
+ } catch (ReflectiveOperationException ex) {
+ throw new AssertionError(ex);
+ }
+ }
+
+ static MemoryLayout align(MemoryLayout layout, long align) {
+ return switch (layout) {
+ case PaddingLayout p -> p;
+ case ValueLayout v -> v.withByteAlignment(align);
+ case GroupLayout g -> {
+ MemoryLayout[] alignedMembers = g.memberLayouts().stream()
+ .map(m -> align(m, align)).toArray(MemoryLayout[]::new);
+ yield g instanceof StructLayout ?
+ MemoryLayout.structLayout(alignedMembers) : MemoryLayout.unionLayout(alignedMembers);
+ }
+ case SequenceLayout s -> MemoryLayout.sequenceLayout(s.elementCount(), align(s.elementLayout(), align));
+ };
+ }
+}
+
diff --git a/java-does-usb/src/main/java/net/codecrete/usb/linux/gen/epoll/epoll.java b/java-does-usb/src/main/java/net/codecrete/usb/linux/gen/epoll/epoll.java
new file mode 100644
index 00000000..22e0cb02
--- /dev/null
+++ b/java-does-usb/src/main/java/net/codecrete/usb/linux/gen/epoll/epoll.java
@@ -0,0 +1,72 @@
+// Generated by jextract
+
+package net.codecrete.usb.linux.gen.epoll;
+
+import java.lang.invoke.*;
+import java.lang.foreign.*;
+import java.nio.ByteOrder;
+import java.util.*;
+import java.util.function.*;
+import java.util.stream.*;
+
+import static java.lang.foreign.ValueLayout.*;
+import static java.lang.foreign.MemoryLayout.PathElement.*;
+
+public class epoll extends epoll$shared {
+
+ epoll() {
+ // Should not be called directly
+ }
+
+ static final Arena LIBRARY_ARENA = Arena.ofAuto();
+
+ static final SymbolLookup SYMBOL_LOOKUP = SymbolLookup.loaderLookup()
+ .or(Linker.nativeLinker().defaultLookup());
+
+ private static final int EPOLL_CTL_ADD = (int)1L;
+ /**
+ * {@snippet lang=c :
+ * #define EPOLL_CTL_ADD 1
+ * }
+ */
+ public static int EPOLL_CTL_ADD() {
+ return EPOLL_CTL_ADD;
+ }
+ private static final int EPOLL_CTL_DEL = (int)2L;
+ /**
+ * {@snippet lang=c :
+ * #define EPOLL_CTL_DEL 2
+ * }
+ */
+ public static int EPOLL_CTL_DEL() {
+ return EPOLL_CTL_DEL;
+ }
+ private static final int EPOLLIN = (int)1L;
+ /**
+ * {@snippet lang=c :
+ * enum EPOLL_EVENTS.EPOLLIN = 1
+ * }
+ */
+ public static int EPOLLIN() {
+ return EPOLLIN;
+ }
+ private static final int EPOLLOUT = (int)4L;
+ /**
+ * {@snippet lang=c :
+ * enum EPOLL_EVENTS.EPOLLOUT = 4
+ * }
+ */
+ public static int EPOLLOUT() {
+ return EPOLLOUT;
+ }
+ private static final int EPOLLWAKEUP = (int)536870912L;
+ /**
+ * {@snippet lang=c :
+ * enum EPOLL_EVENTS.EPOLLWAKEUP = 536870912
+ * }
+ */
+ public static int EPOLLWAKEUP() {
+ return EPOLLWAKEUP;
+ }
+}
+
diff --git a/java-does-usb/src/main/java/net/codecrete/usb/linux/gen/errno/Constants$root.java b/java-does-usb/src/main/java/net/codecrete/usb/linux/gen/errno/Constants$root.java
deleted file mode 100644
index 73adc572..00000000
--- a/java-does-usb/src/main/java/net/codecrete/usb/linux/gen/errno/Constants$root.java
+++ /dev/null
@@ -1,19 +0,0 @@
-// Generated by jextract
-
-package net.codecrete.usb.linux.gen.errno;
-
-import static java.lang.foreign.ValueLayout.*;
-public class Constants$root {
-
- static final OfBoolean C_BOOL$LAYOUT = JAVA_BOOLEAN;
- static final OfByte C_CHAR$LAYOUT = JAVA_BYTE;
- static final OfShort C_SHORT$LAYOUT = JAVA_SHORT.withBitAlignment(16);
- static final OfInt C_INT$LAYOUT = JAVA_INT.withBitAlignment(32);
- static final OfLong C_LONG$LAYOUT = JAVA_LONG.withBitAlignment(64);
- static final OfLong C_LONG_LONG$LAYOUT = JAVA_LONG.withBitAlignment(64);
- static final OfFloat C_FLOAT$LAYOUT = JAVA_FLOAT.withBitAlignment(32);
- static final OfDouble C_DOUBLE$LAYOUT = JAVA_DOUBLE.withBitAlignment(64);
- static final OfAddress C_POINTER$LAYOUT = ADDRESS.withBitAlignment(64);
-}
-
-
diff --git a/java-does-usb/src/main/java/net/codecrete/usb/linux/gen/errno/RuntimeHelper.java b/java-does-usb/src/main/java/net/codecrete/usb/linux/gen/errno/RuntimeHelper.java
deleted file mode 100644
index 282b5dfb..00000000
--- a/java-does-usb/src/main/java/net/codecrete/usb/linux/gen/errno/RuntimeHelper.java
+++ /dev/null
@@ -1,216 +0,0 @@
-package net.codecrete.usb.linux.gen.errno;
-// Generated by jextract
-
-import java.lang.foreign.*;
-import java.lang.invoke.MethodHandle;
-import java.lang.invoke.MethodHandles;
-import java.lang.invoke.MethodType;
-
-import static java.lang.foreign.ValueLayout.*;
-
-final class RuntimeHelper {
-
- private RuntimeHelper() {}
- private final static Linker LINKER = Linker.nativeLinker();
- private final static ClassLoader LOADER = RuntimeHelper.class.getClassLoader();
- private final static MethodHandles.Lookup MH_LOOKUP = MethodHandles.lookup();
- private final static SymbolLookup SYMBOL_LOOKUP;
-
- final static SegmentAllocator CONSTANT_ALLOCATOR =
- (size, align) -> MemorySegment.allocateNative(size, align, MemorySession.openImplicit());
-
- static {
-
- SymbolLookup loaderLookup = SymbolLookup.loaderLookup();
- SYMBOL_LOOKUP = name -> loaderLookup.lookup(name).or(() -> LINKER.defaultLookup().lookup(name));
- }
-
- static T requireNonNull(T obj, String symbolName) {
- if (obj == null) {
- throw new UnsatisfiedLinkError("unresolved symbol: " + symbolName);
- }
- return obj;
- }
-
- private final static SegmentAllocator THROWING_ALLOCATOR = (x, y) -> { throw new AssertionError("should not reach here"); };
-
- static final MemorySegment lookupGlobalVariable(String name, MemoryLayout layout) {
- return SYMBOL_LOOKUP.lookup(name).map(symbol -> MemorySegment.ofAddress(symbol.address(), layout.byteSize(), MemorySession.openShared())).orElse(null);
- }
-
- static final MethodHandle downcallHandle(String name, FunctionDescriptor fdesc) {
- return SYMBOL_LOOKUP.lookup(name).
- map(addr -> LINKER.downcallHandle(addr, fdesc)).
- orElse(null);
- }
-
- static final MethodHandle downcallHandle(FunctionDescriptor fdesc) {
- return LINKER.downcallHandle(fdesc);
- }
-
- static final MethodHandle downcallHandleVariadic(String name, FunctionDescriptor fdesc) {
- return SYMBOL_LOOKUP.lookup(name).
- map(addr -> VarargsInvoker.make(addr, fdesc)).
- orElse(null);
- }
-
- static final MemorySegment upcallStub(Class fi, Z z, FunctionDescriptor fdesc, MemorySession session) {
- try {
- MethodHandle handle = MH_LOOKUP.findVirtual(fi, "apply", Linker.upcallType(fdesc));
- handle = handle.bindTo(z);
- return LINKER.upcallStub(handle, fdesc, session);
- } catch (Throwable ex) {
- throw new AssertionError(ex);
- }
- }
-
- static MemorySegment asArray(MemoryAddress addr, MemoryLayout layout, int numElements, MemorySession session) {
- return MemorySegment.ofAddress(addr, numElements * layout.byteSize(), session);
- }
-
- // Internals only below this point
-
- private static class VarargsInvoker {
- private static final MethodHandle INVOKE_MH;
- private final MemorySegment symbol;
- private final FunctionDescriptor function;
-
- private VarargsInvoker(MemorySegment symbol, FunctionDescriptor function) {
- this.symbol = symbol;
- this.function = function;
- }
-
- static {
- try {
- INVOKE_MH = MethodHandles.lookup().findVirtual(VarargsInvoker.class, "invoke", MethodType.methodType(Object.class, SegmentAllocator.class, Object[].class));
- } catch (ReflectiveOperationException e) {
- throw new RuntimeException(e);
- }
- }
-
- static MethodHandle make(MemorySegment symbol, FunctionDescriptor function) {
- VarargsInvoker invoker = new VarargsInvoker(symbol, function);
- MethodHandle handle = INVOKE_MH.bindTo(invoker).asCollector(Object[].class, function.argumentLayouts().size() + 1);
- MethodType mtype = MethodType.methodType(function.returnLayout().isPresent() ? carrier(function.returnLayout().get(), true) : void.class);
- for (MemoryLayout layout : function.argumentLayouts()) {
- mtype = mtype.appendParameterTypes(carrier(layout, false));
- }
- mtype = mtype.appendParameterTypes(Object[].class);
- if (mtype.returnType().equals(MemorySegment.class)) {
- mtype = mtype.insertParameterTypes(0, SegmentAllocator.class);
- } else {
- handle = MethodHandles.insertArguments(handle, 0, THROWING_ALLOCATOR);
- }
- return handle.asType(mtype);
- }
-
- static Class> carrier(MemoryLayout layout, boolean ret) {
- if (layout instanceof ValueLayout valueLayout) {
- return (ret || valueLayout.carrier() != MemoryAddress.class) ?
- valueLayout.carrier() : Addressable.class;
- } else if (layout instanceof GroupLayout) {
- return MemorySegment.class;
- } else {
- throw new AssertionError("Cannot get here!");
- }
- }
-
- private Object invoke(SegmentAllocator allocator, Object[] args) throws Throwable {
- // one trailing Object[]
- int nNamedArgs = function.argumentLayouts().size();
- assert(args.length == nNamedArgs + 1);
- // The last argument is the array of vararg collector
- Object[] unnamedArgs = (Object[]) args[args.length - 1];
-
- int argsCount = nNamedArgs + unnamedArgs.length;
- Class>[] argTypes = new Class>[argsCount];
- MemoryLayout[] argLayouts = new MemoryLayout[nNamedArgs + unnamedArgs.length];
-
- int pos = 0;
- for (pos = 0; pos < nNamedArgs; pos++) {
- argLayouts[pos] = function.argumentLayouts().get(pos);
- }
-
- assert pos == nNamedArgs;
- for (Object o: unnamedArgs) {
- argLayouts[pos] = variadicLayout(normalize(o.getClass()));
- pos++;
- }
- assert pos == argsCount;
-
- FunctionDescriptor f = (function.returnLayout().isEmpty()) ?
- FunctionDescriptor.ofVoid(argLayouts) :
- FunctionDescriptor.of(function.returnLayout().get(), argLayouts);
- MethodHandle mh = LINKER.downcallHandle(symbol, f);
- if (mh.type().returnType() == MemorySegment.class) {
- mh = mh.bindTo(allocator);
- }
- // flatten argument list so that it can be passed to an asSpreader MH
- Object[] allArgs = new Object[nNamedArgs + unnamedArgs.length];
- System.arraycopy(args, 0, allArgs, 0, nNamedArgs);
- System.arraycopy(unnamedArgs, 0, allArgs, nNamedArgs, unnamedArgs.length);
-
- return mh.asSpreader(Object[].class, argsCount).invoke(allArgs);
- }
-
- private static Class> unboxIfNeeded(Class> clazz) {
- if (clazz == Boolean.class) {
- return boolean.class;
- } else if (clazz == Void.class) {
- return void.class;
- } else if (clazz == Byte.class) {
- return byte.class;
- } else if (clazz == Character.class) {
- return char.class;
- } else if (clazz == Short.class) {
- return short.class;
- } else if (clazz == Integer.class) {
- return int.class;
- } else if (clazz == Long.class) {
- return long.class;
- } else if (clazz == Float.class) {
- return float.class;
- } else if (clazz == Double.class) {
- return double.class;
- } else {
- return clazz;
- }
- }
-
- private Class> promote(Class> c) {
- if (c == byte.class || c == char.class || c == short.class || c == int.class) {
- return long.class;
- } else if (c == float.class) {
- return double.class;
- } else {
- return c;
- }
- }
-
- private Class> normalize(Class> c) {
- c = unboxIfNeeded(c);
- if (c.isPrimitive()) {
- return promote(c);
- }
- if (MemoryAddress.class.isAssignableFrom(c)) {
- return MemoryAddress.class;
- }
- if (MemorySegment.class.isAssignableFrom(c)) {
- return MemorySegment.class;
- }
- throw new IllegalArgumentException("Invalid type for ABI: " + c.getTypeName());
- }
-
- private MemoryLayout variadicLayout(Class> c) {
- if (c == long.class) {
- return JAVA_LONG;
- } else if (c == double.class) {
- return JAVA_DOUBLE;
- } else if (MemoryAddress.class.isAssignableFrom(c)) {
- return ADDRESS;
- } else {
- throw new IllegalArgumentException("Unhandled variadic argument class: " + c);
- }
- }
- }
-}
diff --git a/java-does-usb/src/main/java/net/codecrete/usb/linux/gen/errno/constants$0.java b/java-does-usb/src/main/java/net/codecrete/usb/linux/gen/errno/constants$0.java
deleted file mode 100644
index cb3211ae..00000000
--- a/java-does-usb/src/main/java/net/codecrete/usb/linux/gen/errno/constants$0.java
+++ /dev/null
@@ -1,16 +0,0 @@
-// Generated by jextract
-
-package net.codecrete.usb.linux.gen.errno;
-
-import java.lang.foreign.FunctionDescriptor;
-import java.lang.invoke.MethodHandle;
-class constants$0 {
-
- static final FunctionDescriptor __errno_location$FUNC = FunctionDescriptor.of(Constants$root.C_POINTER$LAYOUT);
- static final MethodHandle __errno_location$MH = RuntimeHelper.downcallHandle(
- "__errno_location",
- constants$0.__errno_location$FUNC
- );
-}
-
-
diff --git a/java-does-usb/src/main/java/net/codecrete/usb/linux/gen/errno/errno$shared.java b/java-does-usb/src/main/java/net/codecrete/usb/linux/gen/errno/errno$shared.java
new file mode 100644
index 00000000..499be5f7
--- /dev/null
+++ b/java-does-usb/src/main/java/net/codecrete/usb/linux/gen/errno/errno$shared.java
@@ -0,0 +1,63 @@
+// Generated by jextract
+
+package net.codecrete.usb.linux.gen.errno;
+
+import java.lang.invoke.*;
+import java.lang.foreign.*;
+import java.nio.ByteOrder;
+import java.util.*;
+import java.util.function.*;
+import java.util.stream.*;
+
+import static java.lang.foreign.ValueLayout.*;
+import static java.lang.foreign.MemoryLayout.PathElement.*;
+
+public class errno$shared {
+
+ errno$shared() {
+ // Should not be called directly
+ }
+
+ public static final ValueLayout.OfBoolean C_BOOL = (ValueLayout.OfBoolean) Linker.nativeLinker().canonicalLayouts().get("bool");
+ public static final ValueLayout.OfByte C_CHAR =(ValueLayout.OfByte)Linker.nativeLinker().canonicalLayouts().get("char");
+ public static final ValueLayout.OfShort C_SHORT = (ValueLayout.OfShort) Linker.nativeLinker().canonicalLayouts().get("short");
+ public static final ValueLayout.OfInt C_INT = (ValueLayout.OfInt) Linker.nativeLinker().canonicalLayouts().get("int");
+ public static final ValueLayout.OfLong C_LONG_LONG = (ValueLayout.OfLong) Linker.nativeLinker().canonicalLayouts().get("long long");
+ public static final ValueLayout.OfFloat C_FLOAT = (ValueLayout.OfFloat) Linker.nativeLinker().canonicalLayouts().get("float");
+ public static final ValueLayout.OfDouble C_DOUBLE = (ValueLayout.OfDouble) Linker.nativeLinker().canonicalLayouts().get("double");
+ public static final AddressLayout C_POINTER = ((AddressLayout) Linker.nativeLinker().canonicalLayouts().get("void*"))
+ .withTargetLayout(MemoryLayout.sequenceLayout(java.lang.Long.MAX_VALUE, C_CHAR));
+ public static final ValueLayout.OfLong C_LONG = (ValueLayout.OfLong) Linker.nativeLinker().canonicalLayouts().get("long");
+
+ static final boolean TRACE_DOWNCALLS = Boolean.getBoolean("jextract.trace.downcalls");
+
+ static void traceDowncall(String name, Object... args) {
+ String traceArgs = Arrays.stream(args)
+ .map(Object::toString)
+ .collect(Collectors.joining(", "));
+ System.out.printf("%s(%s)\n", name, traceArgs);
+ }
+
+ static MethodHandle upcallHandle(Class> fi, String name, FunctionDescriptor fdesc) {
+ try {
+ return MethodHandles.lookup().findVirtual(fi, name, fdesc.toMethodType());
+ } catch (ReflectiveOperationException ex) {
+ throw new AssertionError(ex);
+ }
+ }
+
+ static MemoryLayout align(MemoryLayout layout, long align) {
+ return switch (layout) {
+ case PaddingLayout p -> p;
+ case ValueLayout v -> v.withByteAlignment(align);
+ case GroupLayout g -> {
+ MemoryLayout[] alignedMembers = g.memberLayouts().stream()
+ .map(m -> align(m, align)).toArray(MemoryLayout[]::new);
+ yield g instanceof StructLayout ?
+ MemoryLayout.structLayout(alignedMembers) : MemoryLayout.unionLayout(alignedMembers);
+ }
+ case SequenceLayout s -> MemoryLayout.sequenceLayout(s.elementCount(), align(s.elementLayout(), align));
+ };
+ }
+}
+
diff --git a/java-does-usb/src/main/java/net/codecrete/usb/linux/gen/errno/errno.java b/java-does-usb/src/main/java/net/codecrete/usb/linux/gen/errno/errno.java
index ddc5d6e8..d4397f45 100644
--- a/java-does-usb/src/main/java/net/codecrete/usb/linux/gen/errno/errno.java
+++ b/java-does-usb/src/main/java/net/codecrete/usb/linux/gen/errno/errno.java
@@ -2,32 +2,98 @@
package net.codecrete.usb.linux.gen.errno;
-import java.lang.foreign.MemoryAddress;
-import java.lang.invoke.MethodHandle;
+import java.lang.invoke.*;
+import java.lang.foreign.*;
+import java.nio.ByteOrder;
+import java.util.*;
+import java.util.function.*;
+import java.util.stream.*;
import static java.lang.foreign.ValueLayout.*;
-public class errno {
-
- /* package-private */ errno() {}
- public static OfByte C_CHAR = Constants$root.C_CHAR$LAYOUT;
- public static OfShort C_SHORT = Constants$root.C_SHORT$LAYOUT;
- public static OfInt C_INT = Constants$root.C_INT$LAYOUT;
- public static OfLong C_LONG = Constants$root.C_LONG_LONG$LAYOUT;
- public static OfLong C_LONG_LONG = Constants$root.C_LONG_LONG$LAYOUT;
- public static OfFloat C_FLOAT = Constants$root.C_FLOAT$LAYOUT;
- public static OfDouble C_DOUBLE = Constants$root.C_DOUBLE$LAYOUT;
- public static OfAddress C_POINTER = Constants$root.C_POINTER$LAYOUT;
- public static MethodHandle __errno_location$MH() {
- return RuntimeHelper.requireNonNull(constants$0.__errno_location$MH,"__errno_location");
- }
- public static MemoryAddress __errno_location () {
- var mh$ = __errno_location$MH();
- try {
- return (java.lang.foreign.MemoryAddress)mh$.invokeExact();
- } catch (Throwable ex$) {
- throw new AssertionError("should not reach here", ex$);
- }
+import static java.lang.foreign.MemoryLayout.PathElement.*;
+
+public class errno extends errno$shared {
+
+ errno() {
+ // Should not be called directly
}
-}
+ static final Arena LIBRARY_ARENA = Arena.ofAuto();
+
+ static final SymbolLookup SYMBOL_LOOKUP = SymbolLookup.loaderLookup()
+ .or(Linker.nativeLinker().defaultLookup());
+
+ private static final int ENOENT = (int)2L;
+ /**
+ * {@snippet lang=c :
+ * #define ENOENT 2
+ * }
+ */
+ public static int ENOENT() {
+ return ENOENT;
+ }
+ private static final int EINTR = (int)4L;
+ /**
+ * {@snippet lang=c :
+ * #define EINTR 4
+ * }
+ */
+ public static int EINTR() {
+ return EINTR;
+ }
+ private static final int EBADF = (int)9L;
+ /**
+ * {@snippet lang=c :
+ * #define EBADF 9
+ * }
+ */
+ public static int EBADF() {
+ return EBADF;
+ }
+ private static final int EAGAIN = (int)11L;
+ /**
+ * {@snippet lang=c :
+ * #define EAGAIN 11
+ * }
+ */
+ public static int EAGAIN() {
+ return EAGAIN;
+ }
+ private static final int ENODEV = (int)19L;
+ /**
+ * {@snippet lang=c :
+ * #define ENODEV 19
+ * }
+ */
+ public static int ENODEV() {
+ return ENODEV;
+ }
+ private static final int EINVAL = (int)22L;
+ /**
+ * {@snippet lang=c :
+ * #define EINVAL 22
+ * }
+ */
+ public static int EINVAL() {
+ return EINVAL;
+ }
+ private static final int EPIPE = (int)32L;
+ /**
+ * {@snippet lang=c :
+ * #define EPIPE 32
+ * }
+ */
+ public static int EPIPE() {
+ return EPIPE;
+ }
+ private static final int ECANCELED = (int)125L;
+ /**
+ * {@snippet lang=c :
+ * #define ECANCELED 125
+ * }
+ */
+ public static int ECANCELED() {
+ return ECANCELED;
+ }
+}
diff --git a/java-does-usb/src/main/java/net/codecrete/usb/linux/gen/fcntl/Constants$root.java b/java-does-usb/src/main/java/net/codecrete/usb/linux/gen/fcntl/Constants$root.java
deleted file mode 100644
index c938e854..00000000
--- a/java-does-usb/src/main/java/net/codecrete/usb/linux/gen/fcntl/Constants$root.java
+++ /dev/null
@@ -1,19 +0,0 @@
-// Generated by jextract
-
-package net.codecrete.usb.linux.gen.fcntl;
-
-import static java.lang.foreign.ValueLayout.*;
-public class Constants$root {
-
- static final OfBoolean C_BOOL$LAYOUT = JAVA_BOOLEAN;
- static final OfByte C_CHAR$LAYOUT = JAVA_BYTE;
- static final OfShort C_SHORT$LAYOUT = JAVA_SHORT.withBitAlignment(16);
- static final OfInt C_INT$LAYOUT = JAVA_INT.withBitAlignment(32);
- static final OfLong C_LONG$LAYOUT = JAVA_LONG.withBitAlignment(64);
- static final OfLong C_LONG_LONG$LAYOUT = JAVA_LONG.withBitAlignment(64);
- static final OfFloat C_FLOAT$LAYOUT = JAVA_FLOAT.withBitAlignment(32);
- static final OfDouble C_DOUBLE$LAYOUT = JAVA_DOUBLE.withBitAlignment(64);
- static final OfAddress C_POINTER$LAYOUT = ADDRESS.withBitAlignment(64);
-}
-
-
diff --git a/java-does-usb/src/main/java/net/codecrete/usb/linux/gen/fcntl/RuntimeHelper.java b/java-does-usb/src/main/java/net/codecrete/usb/linux/gen/fcntl/RuntimeHelper.java
deleted file mode 100644
index 48c09b33..00000000
--- a/java-does-usb/src/main/java/net/codecrete/usb/linux/gen/fcntl/RuntimeHelper.java
+++ /dev/null
@@ -1,216 +0,0 @@
-package net.codecrete.usb.linux.gen.fcntl;
-// Generated by jextract
-
-import java.lang.foreign.*;
-import java.lang.invoke.MethodHandle;
-import java.lang.invoke.MethodHandles;
-import java.lang.invoke.MethodType;
-
-import static java.lang.foreign.ValueLayout.*;
-
-final class RuntimeHelper {
-
- private RuntimeHelper() {}
- private final static Linker LINKER = Linker.nativeLinker();
- private final static ClassLoader LOADER = RuntimeHelper.class.getClassLoader();
- private final static MethodHandles.Lookup MH_LOOKUP = MethodHandles.lookup();
- private final static SymbolLookup SYMBOL_LOOKUP;
-
- final static SegmentAllocator CONSTANT_ALLOCATOR =
- (size, align) -> MemorySegment.allocateNative(size, align, MemorySession.openImplicit());
-
- static {
-
- SymbolLookup loaderLookup = SymbolLookup.loaderLookup();
- SYMBOL_LOOKUP = name -> loaderLookup.lookup(name).or(() -> LINKER.defaultLookup().lookup(name));
- }
-
- static T requireNonNull(T obj, String symbolName) {
- if (obj == null) {
- throw new UnsatisfiedLinkError("unresolved symbol: " + symbolName);
- }
- return obj;
- }
-
- private final static SegmentAllocator THROWING_ALLOCATOR = (x, y) -> { throw new AssertionError("should not reach here"); };
-
- static final MemorySegment lookupGlobalVariable(String name, MemoryLayout layout) {
- return SYMBOL_LOOKUP.lookup(name).map(symbol -> MemorySegment.ofAddress(symbol.address(), layout.byteSize(), MemorySession.openShared())).orElse(null);
- }
-
- static final MethodHandle downcallHandle(String name, FunctionDescriptor fdesc) {
- return SYMBOL_LOOKUP.lookup(name).
- map(addr -> LINKER.downcallHandle(addr, fdesc)).
- orElse(null);
- }
-
- static final MethodHandle downcallHandle(FunctionDescriptor fdesc) {
- return LINKER.downcallHandle(fdesc);
- }
-
- static final MethodHandle downcallHandleVariadic(String name, FunctionDescriptor fdesc) {
- return SYMBOL_LOOKUP.lookup(name).
- map(addr -> VarargsInvoker.make(addr, fdesc)).
- orElse(null);
- }
-
- static final MemorySegment upcallStub(Class fi, Z z, FunctionDescriptor fdesc, MemorySession session) {
- try {
- MethodHandle handle = MH_LOOKUP.findVirtual(fi, "apply", Linker.upcallType(fdesc));
- handle = handle.bindTo(z);
- return LINKER.upcallStub(handle, fdesc, session);
- } catch (Throwable ex) {
- throw new AssertionError(ex);
- }
- }
-
- static MemorySegment asArray(MemoryAddress addr, MemoryLayout layout, int numElements, MemorySession session) {
- return MemorySegment.ofAddress(addr, numElements * layout.byteSize(), session);
- }
-
- // Internals only below this point
-
- private static class VarargsInvoker {
- private static final MethodHandle INVOKE_MH;
- private final MemorySegment symbol;
- private final FunctionDescriptor function;
-
- private VarargsInvoker(MemorySegment symbol, FunctionDescriptor function) {
- this.symbol = symbol;
- this.function = function;
- }
-
- static {
- try {
- INVOKE_MH = MethodHandles.lookup().findVirtual(VarargsInvoker.class, "invoke", MethodType.methodType(Object.class, SegmentAllocator.class, Object[].class));
- } catch (ReflectiveOperationException e) {
- throw new RuntimeException(e);
- }
- }
-
- static MethodHandle make(MemorySegment symbol, FunctionDescriptor function) {
- VarargsInvoker invoker = new VarargsInvoker(symbol, function);
- MethodHandle handle = INVOKE_MH.bindTo(invoker).asCollector(Object[].class, function.argumentLayouts().size() + 1);
- MethodType mtype = MethodType.methodType(function.returnLayout().isPresent() ? carrier(function.returnLayout().get(), true) : void.class);
- for (MemoryLayout layout : function.argumentLayouts()) {
- mtype = mtype.appendParameterTypes(carrier(layout, false));
- }
- mtype = mtype.appendParameterTypes(Object[].class);
- if (mtype.returnType().equals(MemorySegment.class)) {
- mtype = mtype.insertParameterTypes(0, SegmentAllocator.class);
- } else {
- handle = MethodHandles.insertArguments(handle, 0, THROWING_ALLOCATOR);
- }
- return handle.asType(mtype);
- }
-
- static Class> carrier(MemoryLayout layout, boolean ret) {
- if (layout instanceof ValueLayout valueLayout) {
- return (ret || valueLayout.carrier() != MemoryAddress.class) ?
- valueLayout.carrier() : Addressable.class;
- } else if (layout instanceof GroupLayout) {
- return MemorySegment.class;
- } else {
- throw new AssertionError("Cannot get here!");
- }
- }
-
- private Object invoke(SegmentAllocator allocator, Object[] args) throws Throwable {
- // one trailing Object[]
- int nNamedArgs = function.argumentLayouts().size();
- assert(args.length == nNamedArgs + 1);
- // The last argument is the array of vararg collector
- Object[] unnamedArgs = (Object[]) args[args.length - 1];
-
- int argsCount = nNamedArgs + unnamedArgs.length;
- Class>[] argTypes = new Class>[argsCount];
- MemoryLayout[] argLayouts = new MemoryLayout[nNamedArgs + unnamedArgs.length];
-
- int pos = 0;
- for (pos = 0; pos < nNamedArgs; pos++) {
- argLayouts[pos] = function.argumentLayouts().get(pos);
- }
-
- assert pos == nNamedArgs;
- for (Object o: unnamedArgs) {
- argLayouts[pos] = variadicLayout(normalize(o.getClass()));
- pos++;
- }
- assert pos == argsCount;
-
- FunctionDescriptor f = (function.returnLayout().isEmpty()) ?
- FunctionDescriptor.ofVoid(argLayouts) :
- FunctionDescriptor.of(function.returnLayout().get(), argLayouts);
- MethodHandle mh = LINKER.downcallHandle(symbol, f);
- if (mh.type().returnType() == MemorySegment.class) {
- mh = mh.bindTo(allocator);
- }
- // flatten argument list so that it can be passed to an asSpreader MH
- Object[] allArgs = new Object[nNamedArgs + unnamedArgs.length];
- System.arraycopy(args, 0, allArgs, 0, nNamedArgs);
- System.arraycopy(unnamedArgs, 0, allArgs, nNamedArgs, unnamedArgs.length);
-
- return mh.asSpreader(Object[].class, argsCount).invoke(allArgs);
- }
-
- private static Class> unboxIfNeeded(Class> clazz) {
- if (clazz == Boolean.class) {
- return boolean.class;
- } else if (clazz == Void.class) {
- return void.class;
- } else if (clazz == Byte.class) {
- return byte.class;
- } else if (clazz == Character.class) {
- return char.class;
- } else if (clazz == Short.class) {
- return short.class;
- } else if (clazz == Integer.class) {
- return int.class;
- } else if (clazz == Long.class) {
- return long.class;
- } else if (clazz == Float.class) {
- return float.class;
- } else if (clazz == Double.class) {
- return double.class;
- } else {
- return clazz;
- }
- }
-
- private Class> promote(Class> c) {
- if (c == byte.class || c == char.class || c == short.class || c == int.class) {
- return long.class;
- } else if (c == float.class) {
- return double.class;
- } else {
- return c;
- }
- }
-
- private Class> normalize(Class> c) {
- c = unboxIfNeeded(c);
- if (c.isPrimitive()) {
- return promote(c);
- }
- if (MemoryAddress.class.isAssignableFrom(c)) {
- return MemoryAddress.class;
- }
- if (MemorySegment.class.isAssignableFrom(c)) {
- return MemorySegment.class;
- }
- throw new IllegalArgumentException("Invalid type for ABI: " + c.getTypeName());
- }
-
- private MemoryLayout variadicLayout(Class> c) {
- if (c == long.class) {
- return JAVA_LONG;
- } else if (c == double.class) {
- return JAVA_DOUBLE;
- } else if (MemoryAddress.class.isAssignableFrom(c)) {
- return ADDRESS;
- } else {
- throw new IllegalArgumentException("Unhandled variadic argument class: " + c);
- }
- }
- }
-}
diff --git a/java-does-usb/src/main/java/net/codecrete/usb/linux/gen/fcntl/constants$0.java b/java-does-usb/src/main/java/net/codecrete/usb/linux/gen/fcntl/constants$0.java
deleted file mode 100644
index 517dcb8f..00000000
--- a/java-does-usb/src/main/java/net/codecrete/usb/linux/gen/fcntl/constants$0.java
+++ /dev/null
@@ -1,19 +0,0 @@
-// Generated by jextract
-
-package net.codecrete.usb.linux.gen.fcntl;
-
-import java.lang.foreign.FunctionDescriptor;
-import java.lang.invoke.MethodHandle;
-class constants$0 {
-
- static final FunctionDescriptor open$FUNC = FunctionDescriptor.of(Constants$root.C_INT$LAYOUT,
- Constants$root.C_POINTER$LAYOUT,
- Constants$root.C_INT$LAYOUT
- );
- static final MethodHandle open$MH = RuntimeHelper.downcallHandleVariadic(
- "open",
- constants$0.open$FUNC
- );
-}
-
-
diff --git a/java-does-usb/src/main/java/net/codecrete/usb/linux/gen/fcntl/fcntl$shared.java b/java-does-usb/src/main/java/net/codecrete/usb/linux/gen/fcntl/fcntl$shared.java
new file mode 100644
index 00000000..0a6ca8e4
--- /dev/null
+++ b/java-does-usb/src/main/java/net/codecrete/usb/linux/gen/fcntl/fcntl$shared.java
@@ -0,0 +1,63 @@
+// Generated by jextract
+
+package net.codecrete.usb.linux.gen.fcntl;
+
+import java.lang.invoke.*;
+import java.lang.foreign.*;
+import java.nio.ByteOrder;
+import java.util.*;
+import java.util.function.*;
+import java.util.stream.*;
+
+import static java.lang.foreign.ValueLayout.*;
+import static java.lang.foreign.MemoryLayout.PathElement.*;
+
+public class fcntl$shared {
+
+ fcntl$shared() {
+ // Should not be called directly
+ }
+
+ public static final ValueLayout.OfBoolean C_BOOL = (ValueLayout.OfBoolean) Linker.nativeLinker().canonicalLayouts().get("bool");
+ public static final ValueLayout.OfByte C_CHAR =(ValueLayout.OfByte)Linker.nativeLinker().canonicalLayouts().get("char");
+ public static final ValueLayout.OfShort C_SHORT = (ValueLayout.OfShort) Linker.nativeLinker().canonicalLayouts().get("short");
+ public static final ValueLayout.OfInt C_INT = (ValueLayout.OfInt) Linker.nativeLinker().canonicalLayouts().get("int");
+ public static final ValueLayout.OfLong C_LONG_LONG = (ValueLayout.OfLong) Linker.nativeLinker().canonicalLayouts().get("long long");
+ public static final ValueLayout.OfFloat C_FLOAT = (ValueLayout.OfFloat) Linker.nativeLinker().canonicalLayouts().get("float");
+ public static final ValueLayout.OfDouble C_DOUBLE = (ValueLayout.OfDouble) Linker.nativeLinker().canonicalLayouts().get("double");
+ public static final AddressLayout C_POINTER = ((AddressLayout) Linker.nativeLinker().canonicalLayouts().get("void*"))
+ .withTargetLayout(MemoryLayout.sequenceLayout(java.lang.Long.MAX_VALUE, C_CHAR));
+ public static final ValueLayout.OfLong C_LONG = (ValueLayout.OfLong) Linker.nativeLinker().canonicalLayouts().get("long");
+
+ static final boolean TRACE_DOWNCALLS = Boolean.getBoolean("jextract.trace.downcalls");
+
+ static void traceDowncall(String name, Object... args) {
+ String traceArgs = Arrays.stream(args)
+ .map(Object::toString)
+ .collect(Collectors.joining(", "));
+ System.out.printf("%s(%s)\n", name, traceArgs);
+ }
+
+ static MethodHandle upcallHandle(Class> fi, String name, FunctionDescriptor fdesc) {
+ try {
+ return MethodHandles.lookup().findVirtual(fi, name, fdesc.toMethodType());
+ } catch (ReflectiveOperationException ex) {
+ throw new AssertionError(ex);
+ }
+ }
+
+ static MemoryLayout align(MemoryLayout layout, long align) {
+ return switch (layout) {
+ case PaddingLayout p -> p;
+ case ValueLayout v -> v.withByteAlignment(align);
+ case GroupLayout g -> {
+ MemoryLayout[] alignedMembers = g.memberLayouts().stream()
+ .map(m -> align(m, align)).toArray(MemoryLayout[]::new);
+ yield g instanceof StructLayout ?
+ MemoryLayout.structLayout(alignedMembers) : MemoryLayout.unionLayout(alignedMembers);
+ }
+ case SequenceLayout s -> MemoryLayout.sequenceLayout(s.elementCount(), align(s.elementLayout(), align));
+ };
+ }
+}
+
diff --git a/java-does-usb/src/main/java/net/codecrete/usb/linux/gen/fcntl/fcntl.java b/java-does-usb/src/main/java/net/codecrete/usb/linux/gen/fcntl/fcntl.java
index 41eb1e0c..72cbfb53 100644
--- a/java-does-usb/src/main/java/net/codecrete/usb/linux/gen/fcntl/fcntl.java
+++ b/java-does-usb/src/main/java/net/codecrete/usb/linux/gen/fcntl/fcntl.java
@@ -2,38 +2,53 @@
package net.codecrete.usb.linux.gen.fcntl;
-import java.lang.foreign.Addressable;
-import java.lang.invoke.MethodHandle;
+import java.lang.invoke.*;
+import java.lang.foreign.*;
+import java.nio.ByteOrder;
+import java.util.*;
+import java.util.function.*;
+import java.util.stream.*;
import static java.lang.foreign.ValueLayout.*;
-public class fcntl {
-
- /* package-private */ fcntl() {}
- public static OfByte C_CHAR = Constants$root.C_CHAR$LAYOUT;
- public static OfShort C_SHORT = Constants$root.C_SHORT$LAYOUT;
- public static OfInt C_INT = Constants$root.C_INT$LAYOUT;
- public static OfLong C_LONG = Constants$root.C_LONG_LONG$LAYOUT;
- public static OfLong C_LONG_LONG = Constants$root.C_LONG_LONG$LAYOUT;
- public static OfFloat C_FLOAT = Constants$root.C_FLOAT$LAYOUT;
- public static OfDouble C_DOUBLE = Constants$root.C_DOUBLE$LAYOUT;
- public static OfAddress C_POINTER = Constants$root.C_POINTER$LAYOUT;
- public static int O_RDWR() {
- return (int)2L;
+import static java.lang.foreign.MemoryLayout.PathElement.*;
+
+public class fcntl extends fcntl$shared {
+
+ fcntl() {
+ // Should not be called directly
}
- public static MethodHandle open$MH() {
- return RuntimeHelper.requireNonNull(constants$0.open$MH,"open");
+
+ static final Arena LIBRARY_ARENA = Arena.ofAuto();
+
+ static final SymbolLookup SYMBOL_LOOKUP = SymbolLookup.loaderLookup()
+ .or(Linker.nativeLinker().defaultLookup());
+
+ private static final int O_RDWR = (int)2L;
+ /**
+ * {@snippet lang=c :
+ * #define O_RDWR 2
+ * }
+ */
+ public static int O_RDWR() {
+ return O_RDWR;
}
- public static int open ( Addressable __file, int __oflag, Object... x2) {
- var mh$ = open$MH();
- try {
- return (int)mh$.invokeExact(__file, __oflag, x2);
- } catch (Throwable ex$) {
- throw new AssertionError("should not reach here", ex$);
- }
+ private static final int FD_CLOEXEC = (int)1L;
+ /**
+ * {@snippet lang=c :
+ * #define FD_CLOEXEC 1
+ * }
+ */
+ public static int FD_CLOEXEC() {
+ return FD_CLOEXEC;
}
+ private static final int O_CLOEXEC = (int)524288L;
+ /**
+ * {@snippet lang=c :
+ * #define O_CLOEXEC 524288
+ * }
+ */
public static int O_CLOEXEC() {
- return (int)524288L;
+ return O_CLOEXEC;
}
}
-
diff --git a/java-does-usb/src/main/java/net/codecrete/usb/linux/gen/ioctl/Constants$root.java b/java-does-usb/src/main/java/net/codecrete/usb/linux/gen/ioctl/Constants$root.java
deleted file mode 100644
index c9e48345..00000000
--- a/java-does-usb/src/main/java/net/codecrete/usb/linux/gen/ioctl/Constants$root.java
+++ /dev/null
@@ -1,19 +0,0 @@
-// Generated by jextract
-
-package net.codecrete.usb.linux.gen.ioctl;
-
-import static java.lang.foreign.ValueLayout.*;
-public class Constants$root {
-
- static final OfBoolean C_BOOL$LAYOUT = JAVA_BOOLEAN;
- static final OfByte C_CHAR$LAYOUT = JAVA_BYTE;
- static final OfShort C_SHORT$LAYOUT = JAVA_SHORT.withBitAlignment(16);
- static final OfInt C_INT$LAYOUT = JAVA_INT.withBitAlignment(32);
- static final OfLong C_LONG$LAYOUT = JAVA_LONG.withBitAlignment(64);
- static final OfLong C_LONG_LONG$LAYOUT = JAVA_LONG.withBitAlignment(64);
- static final OfFloat C_FLOAT$LAYOUT = JAVA_FLOAT.withBitAlignment(32);
- static final OfDouble C_DOUBLE$LAYOUT = JAVA_DOUBLE.withBitAlignment(64);
- static final OfAddress C_POINTER$LAYOUT = ADDRESS.withBitAlignment(64);
-}
-
-
diff --git a/java-does-usb/src/main/java/net/codecrete/usb/linux/gen/ioctl/RuntimeHelper.java b/java-does-usb/src/main/java/net/codecrete/usb/linux/gen/ioctl/RuntimeHelper.java
deleted file mode 100644
index e7740016..00000000
--- a/java-does-usb/src/main/java/net/codecrete/usb/linux/gen/ioctl/RuntimeHelper.java
+++ /dev/null
@@ -1,216 +0,0 @@
-package net.codecrete.usb.linux.gen.ioctl;
-// Generated by jextract
-
-import java.lang.foreign.*;
-import java.lang.invoke.MethodHandle;
-import java.lang.invoke.MethodHandles;
-import java.lang.invoke.MethodType;
-
-import static java.lang.foreign.ValueLayout.*;
-
-final class RuntimeHelper {
-
- private RuntimeHelper() {}
- private final static Linker LINKER = Linker.nativeLinker();
- private final static ClassLoader LOADER = RuntimeHelper.class.getClassLoader();
- private final static MethodHandles.Lookup MH_LOOKUP = MethodHandles.lookup();
- private final static SymbolLookup SYMBOL_LOOKUP;
-
- final static SegmentAllocator CONSTANT_ALLOCATOR =
- (size, align) -> MemorySegment.allocateNative(size, align, MemorySession.openImplicit());
-
- static {
-
- SymbolLookup loaderLookup = SymbolLookup.loaderLookup();
- SYMBOL_LOOKUP = name -> loaderLookup.lookup(name).or(() -> LINKER.defaultLookup().lookup(name));
- }
-
- static T requireNonNull(T obj, String symbolName) {
- if (obj == null) {
- throw new UnsatisfiedLinkError("unresolved symbol: " + symbolName);
- }
- return obj;
- }
-
- private final static SegmentAllocator THROWING_ALLOCATOR = (x, y) -> { throw new AssertionError("should not reach here"); };
-
- static final MemorySegment lookupGlobalVariable(String name, MemoryLayout layout) {
- return SYMBOL_LOOKUP.lookup(name).map(symbol -> MemorySegment.ofAddress(symbol.address(), layout.byteSize(), MemorySession.openShared())).orElse(null);
- }
-
- static final MethodHandle downcallHandle(String name, FunctionDescriptor fdesc) {
- return SYMBOL_LOOKUP.lookup(name).
- map(addr -> LINKER.downcallHandle(addr, fdesc)).
- orElse(null);
- }
-
- static final MethodHandle downcallHandle(FunctionDescriptor fdesc) {
- return LINKER.downcallHandle(fdesc);
- }
-
- static final MethodHandle downcallHandleVariadic(String name, FunctionDescriptor fdesc) {
- return SYMBOL_LOOKUP.lookup(name).
- map(addr -> VarargsInvoker.make(addr, fdesc)).
- orElse(null);
- }
-
- static final MemorySegment upcallStub(Class fi, Z z, FunctionDescriptor fdesc, MemorySession session) {
- try {
- MethodHandle handle = MH_LOOKUP.findVirtual(fi, "apply", Linker.upcallType(fdesc));
- handle = handle.bindTo(z);
- return LINKER.upcallStub(handle, fdesc, session);
- } catch (Throwable ex) {
- throw new AssertionError(ex);
- }
- }
-
- static MemorySegment asArray(MemoryAddress addr, MemoryLayout layout, int numElements, MemorySession session) {
- return MemorySegment.ofAddress(addr, numElements * layout.byteSize(), session);
- }
-
- // Internals only below this point
-
- private static class VarargsInvoker {
- private static final MethodHandle INVOKE_MH;
- private final MemorySegment symbol;
- private final FunctionDescriptor function;
-
- private VarargsInvoker(MemorySegment symbol, FunctionDescriptor function) {
- this.symbol = symbol;
- this.function = function;
- }
-
- static {
- try {
- INVOKE_MH = MethodHandles.lookup().findVirtual(VarargsInvoker.class, "invoke", MethodType.methodType(Object.class, SegmentAllocator.class, Object[].class));
- } catch (ReflectiveOperationException e) {
- throw new RuntimeException(e);
- }
- }
-
- static MethodHandle make(MemorySegment symbol, FunctionDescriptor function) {
- VarargsInvoker invoker = new VarargsInvoker(symbol, function);
- MethodHandle handle = INVOKE_MH.bindTo(invoker).asCollector(Object[].class, function.argumentLayouts().size() + 1);
- MethodType mtype = MethodType.methodType(function.returnLayout().isPresent() ? carrier(function.returnLayout().get(), true) : void.class);
- for (MemoryLayout layout : function.argumentLayouts()) {
- mtype = mtype.appendParameterTypes(carrier(layout, false));
- }
- mtype = mtype.appendParameterTypes(Object[].class);
- if (mtype.returnType().equals(MemorySegment.class)) {
- mtype = mtype.insertParameterTypes(0, SegmentAllocator.class);
- } else {
- handle = MethodHandles.insertArguments(handle, 0, THROWING_ALLOCATOR);
- }
- return handle.asType(mtype);
- }
-
- static Class> carrier(MemoryLayout layout, boolean ret) {
- if (layout instanceof ValueLayout valueLayout) {
- return (ret || valueLayout.carrier() != MemoryAddress.class) ?
- valueLayout.carrier() : Addressable.class;
- } else if (layout instanceof GroupLayout) {
- return MemorySegment.class;
- } else {
- throw new AssertionError("Cannot get here!");
- }
- }
-
- private Object invoke(SegmentAllocator allocator, Object[] args) throws Throwable {
- // one trailing Object[]
- int nNamedArgs = function.argumentLayouts().size();
- assert(args.length == nNamedArgs + 1);
- // The last argument is the array of vararg collector
- Object[] unnamedArgs = (Object[]) args[args.length - 1];
-
- int argsCount = nNamedArgs + unnamedArgs.length;
- Class>[] argTypes = new Class>[argsCount];
- MemoryLayout[] argLayouts = new MemoryLayout[nNamedArgs + unnamedArgs.length];
-
- int pos = 0;
- for (pos = 0; pos < nNamedArgs; pos++) {
- argLayouts[pos] = function.argumentLayouts().get(pos);
- }
-
- assert pos == nNamedArgs;
- for (Object o: unnamedArgs) {
- argLayouts[pos] = variadicLayout(normalize(o.getClass()));
- pos++;
- }
- assert pos == argsCount;
-
- FunctionDescriptor f = (function.returnLayout().isEmpty()) ?
- FunctionDescriptor.ofVoid(argLayouts) :
- FunctionDescriptor.of(function.returnLayout().get(), argLayouts);
- MethodHandle mh = LINKER.downcallHandle(symbol, f);
- if (mh.type().returnType() == MemorySegment.class) {
- mh = mh.bindTo(allocator);
- }
- // flatten argument list so that it can be passed to an asSpreader MH
- Object[] allArgs = new Object[nNamedArgs + unnamedArgs.length];
- System.arraycopy(args, 0, allArgs, 0, nNamedArgs);
- System.arraycopy(unnamedArgs, 0, allArgs, nNamedArgs, unnamedArgs.length);
-
- return mh.asSpreader(Object[].class, argsCount).invoke(allArgs);
- }
-
- private static Class> unboxIfNeeded(Class> clazz) {
- if (clazz == Boolean.class) {
- return boolean.class;
- } else if (clazz == Void.class) {
- return void.class;
- } else if (clazz == Byte.class) {
- return byte.class;
- } else if (clazz == Character.class) {
- return char.class;
- } else if (clazz == Short.class) {
- return short.class;
- } else if (clazz == Integer.class) {
- return int.class;
- } else if (clazz == Long.class) {
- return long.class;
- } else if (clazz == Float.class) {
- return float.class;
- } else if (clazz == Double.class) {
- return double.class;
- } else {
- return clazz;
- }
- }
-
- private Class> promote(Class> c) {
- if (c == byte.class || c == char.class || c == short.class || c == int.class) {
- return long.class;
- } else if (c == float.class) {
- return double.class;
- } else {
- return c;
- }
- }
-
- private Class> normalize(Class> c) {
- c = unboxIfNeeded(c);
- if (c.isPrimitive()) {
- return promote(c);
- }
- if (MemoryAddress.class.isAssignableFrom(c)) {
- return MemoryAddress.class;
- }
- if (MemorySegment.class.isAssignableFrom(c)) {
- return MemorySegment.class;
- }
- throw new IllegalArgumentException("Invalid type for ABI: " + c.getTypeName());
- }
-
- private MemoryLayout variadicLayout(Class> c) {
- if (c == long.class) {
- return JAVA_LONG;
- } else if (c == double.class) {
- return JAVA_DOUBLE;
- } else if (MemoryAddress.class.isAssignableFrom(c)) {
- return ADDRESS;
- } else {
- throw new IllegalArgumentException("Unhandled variadic argument class: " + c);
- }
- }
- }
-}
diff --git a/java-does-usb/src/main/java/net/codecrete/usb/linux/gen/ioctl/constants$0.java b/java-does-usb/src/main/java/net/codecrete/usb/linux/gen/ioctl/constants$0.java
deleted file mode 100644
index e68a7892..00000000
--- a/java-does-usb/src/main/java/net/codecrete/usb/linux/gen/ioctl/constants$0.java
+++ /dev/null
@@ -1,19 +0,0 @@
-// Generated by jextract
-
-package net.codecrete.usb.linux.gen.ioctl;
-
-import java.lang.foreign.FunctionDescriptor;
-import java.lang.invoke.MethodHandle;
-class constants$0 {
-
- static final FunctionDescriptor ioctl$FUNC = FunctionDescriptor.of(Constants$root.C_INT$LAYOUT,
- Constants$root.C_INT$LAYOUT,
- Constants$root.C_LONG_LONG$LAYOUT
- );
- static final MethodHandle ioctl$MH = RuntimeHelper.downcallHandleVariadic(
- "ioctl",
- constants$0.ioctl$FUNC
- );
-}
-
-
diff --git a/java-does-usb/src/main/java/net/codecrete/usb/linux/gen/ioctl/ioctl.java b/java-does-usb/src/main/java/net/codecrete/usb/linux/gen/ioctl/ioctl.java
deleted file mode 100644
index cbe51a0b..00000000
--- a/java-does-usb/src/main/java/net/codecrete/usb/linux/gen/ioctl/ioctl.java
+++ /dev/null
@@ -1,32 +0,0 @@
-// Generated by jextract
-
-package net.codecrete.usb.linux.gen.ioctl;
-
-import java.lang.invoke.MethodHandle;
-
-import static java.lang.foreign.ValueLayout.*;
-public class ioctl {
-
- /* package-private */ ioctl() {}
- public static OfByte C_CHAR = Constants$root.C_CHAR$LAYOUT;
- public static OfShort C_SHORT = Constants$root.C_SHORT$LAYOUT;
- public static OfInt C_INT = Constants$root.C_INT$LAYOUT;
- public static OfLong C_LONG = Constants$root.C_LONG_LONG$LAYOUT;
- public static OfLong C_LONG_LONG = Constants$root.C_LONG_LONG$LAYOUT;
- public static OfFloat C_FLOAT = Constants$root.C_FLOAT$LAYOUT;
- public static OfDouble C_DOUBLE = Constants$root.C_DOUBLE$LAYOUT;
- public static OfAddress C_POINTER = Constants$root.C_POINTER$LAYOUT;
- public static MethodHandle ioctl$MH() {
- return RuntimeHelper.requireNonNull(constants$0.ioctl$MH,"ioctl");
- }
- public static int ioctl ( int __fd, long __request, Object... x2) {
- var mh$ = ioctl$MH();
- try {
- return (int)mh$.invokeExact(__fd, __request, x2);
- } catch (Throwable ex$) {
- throw new AssertionError("should not reach here", ex$);
- }
- }
-}
-
-
diff --git a/java-does-usb/src/main/java/net/codecrete/usb/linux/gen/select/Constants$root.java b/java-does-usb/src/main/java/net/codecrete/usb/linux/gen/select/Constants$root.java
deleted file mode 100644
index bd53659c..00000000
--- a/java-does-usb/src/main/java/net/codecrete/usb/linux/gen/select/Constants$root.java
+++ /dev/null
@@ -1,19 +0,0 @@
-// Generated by jextract
-
-package net.codecrete.usb.linux.gen.select;
-
-import static java.lang.foreign.ValueLayout.*;
-public class Constants$root {
-
- static final OfBoolean C_BOOL$LAYOUT = JAVA_BOOLEAN;
- static final OfByte C_CHAR$LAYOUT = JAVA_BYTE;
- static final OfShort C_SHORT$LAYOUT = JAVA_SHORT.withBitAlignment(16);
- static final OfInt C_INT$LAYOUT = JAVA_INT.withBitAlignment(32);
- static final OfLong C_LONG$LAYOUT = JAVA_LONG.withBitAlignment(64);
- static final OfLong C_LONG_LONG$LAYOUT = JAVA_LONG.withBitAlignment(64);
- static final OfFloat C_FLOAT$LAYOUT = JAVA_FLOAT.withBitAlignment(32);
- static final OfDouble C_DOUBLE$LAYOUT = JAVA_DOUBLE.withBitAlignment(64);
- static final OfAddress C_POINTER$LAYOUT = ADDRESS.withBitAlignment(64);
-}
-
-
diff --git a/java-does-usb/src/main/java/net/codecrete/usb/linux/gen/select/RuntimeHelper.java b/java-does-usb/src/main/java/net/codecrete/usb/linux/gen/select/RuntimeHelper.java
deleted file mode 100644
index a634735c..00000000
--- a/java-does-usb/src/main/java/net/codecrete/usb/linux/gen/select/RuntimeHelper.java
+++ /dev/null
@@ -1,216 +0,0 @@
-package net.codecrete.usb.linux.gen.select;
-// Generated by jextract
-
-import java.lang.foreign.*;
-import java.lang.invoke.MethodHandle;
-import java.lang.invoke.MethodHandles;
-import java.lang.invoke.MethodType;
-
-import static java.lang.foreign.ValueLayout.*;
-
-final class RuntimeHelper {
-
- private RuntimeHelper() {}
- private final static Linker LINKER = Linker.nativeLinker();
- private final static ClassLoader LOADER = RuntimeHelper.class.getClassLoader();
- private final static MethodHandles.Lookup MH_LOOKUP = MethodHandles.lookup();
- private final static SymbolLookup SYMBOL_LOOKUP;
-
- final static SegmentAllocator CONSTANT_ALLOCATOR =
- (size, align) -> MemorySegment.allocateNative(size, align, MemorySession.openImplicit());
-
- static {
-
- SymbolLookup loaderLookup = SymbolLookup.loaderLookup();
- SYMBOL_LOOKUP = name -> loaderLookup.lookup(name).or(() -> LINKER.defaultLookup().lookup(name));
- }
-
- static T requireNonNull(T obj, String symbolName) {
- if (obj == null) {
- throw new UnsatisfiedLinkError("unresolved symbol: " + symbolName);
- }
- return obj;
- }
-
- private final static SegmentAllocator THROWING_ALLOCATOR = (x, y) -> { throw new AssertionError("should not reach here"); };
-
- static final MemorySegment lookupGlobalVariable(String name, MemoryLayout layout) {
- return SYMBOL_LOOKUP.lookup(name).map(symbol -> MemorySegment.ofAddress(symbol.address(), layout.byteSize(), MemorySession.openShared())).orElse(null);
- }
-
- static final MethodHandle downcallHandle(String name, FunctionDescriptor fdesc) {
- return SYMBOL_LOOKUP.lookup(name).
- map(addr -> LINKER.downcallHandle(addr, fdesc)).
- orElse(null);
- }
-
- static final MethodHandle downcallHandle(FunctionDescriptor fdesc) {
- return LINKER.downcallHandle(fdesc);
- }
-
- static final MethodHandle downcallHandleVariadic(String name, FunctionDescriptor fdesc) {
- return SYMBOL_LOOKUP.lookup(name).
- map(addr -> VarargsInvoker.make(addr, fdesc)).
- orElse(null);
- }
-
- static final MemorySegment upcallStub(Class fi, Z z, FunctionDescriptor fdesc, MemorySession session) {
- try {
- MethodHandle handle = MH_LOOKUP.findVirtual(fi, "apply", Linker.upcallType(fdesc));
- handle = handle.bindTo(z);
- return LINKER.upcallStub(handle, fdesc, session);
- } catch (Throwable ex) {
- throw new AssertionError(ex);
- }
- }
-
- static MemorySegment asArray(MemoryAddress addr, MemoryLayout layout, int numElements, MemorySession session) {
- return MemorySegment.ofAddress(addr, numElements * layout.byteSize(), session);
- }
-
- // Internals only below this point
-
- private static class VarargsInvoker {
- private static final MethodHandle INVOKE_MH;
- private final MemorySegment symbol;
- private final FunctionDescriptor function;
-
- private VarargsInvoker(MemorySegment symbol, FunctionDescriptor function) {
- this.symbol = symbol;
- this.function = function;
- }
-
- static {
- try {
- INVOKE_MH = MethodHandles.lookup().findVirtual(VarargsInvoker.class, "invoke", MethodType.methodType(Object.class, SegmentAllocator.class, Object[].class));
- } catch (ReflectiveOperationException e) {
- throw new RuntimeException(e);
- }
- }
-
- static MethodHandle make(MemorySegment symbol, FunctionDescriptor function) {
- VarargsInvoker invoker = new VarargsInvoker(symbol, function);
- MethodHandle handle = INVOKE_MH.bindTo(invoker).asCollector(Object[].class, function.argumentLayouts().size() + 1);
- MethodType mtype = MethodType.methodType(function.returnLayout().isPresent() ? carrier(function.returnLayout().get(), true) : void.class);
- for (MemoryLayout layout : function.argumentLayouts()) {
- mtype = mtype.appendParameterTypes(carrier(layout, false));
- }
- mtype = mtype.appendParameterTypes(Object[].class);
- if (mtype.returnType().equals(MemorySegment.class)) {
- mtype = mtype.insertParameterTypes(0, SegmentAllocator.class);
- } else {
- handle = MethodHandles.insertArguments(handle, 0, THROWING_ALLOCATOR);
- }
- return handle.asType(mtype);
- }
-
- static Class> carrier(MemoryLayout layout, boolean ret) {
- if (layout instanceof ValueLayout valueLayout) {
- return (ret || valueLayout.carrier() != MemoryAddress.class) ?
- valueLayout.carrier() : Addressable.class;
- } else if (layout instanceof GroupLayout) {
- return MemorySegment.class;
- } else {
- throw new AssertionError("Cannot get here!");
- }
- }
-
- private Object invoke(SegmentAllocator allocator, Object[] args) throws Throwable {
- // one trailing Object[]
- int nNamedArgs = function.argumentLayouts().size();
- assert(args.length == nNamedArgs + 1);
- // The last argument is the array of vararg collector
- Object[] unnamedArgs = (Object[]) args[args.length - 1];
-
- int argsCount = nNamedArgs + unnamedArgs.length;
- Class>[] argTypes = new Class>[argsCount];
- MemoryLayout[] argLayouts = new MemoryLayout[nNamedArgs + unnamedArgs.length];
-
- int pos = 0;
- for (pos = 0; pos < nNamedArgs; pos++) {
- argLayouts[pos] = function.argumentLayouts().get(pos);
- }
-
- assert pos == nNamedArgs;
- for (Object o: unnamedArgs) {
- argLayouts[pos] = variadicLayout(normalize(o.getClass()));
- pos++;
- }
- assert pos == argsCount;
-
- FunctionDescriptor f = (function.returnLayout().isEmpty()) ?
- FunctionDescriptor.ofVoid(argLayouts) :
- FunctionDescriptor.of(function.returnLayout().get(), argLayouts);
- MethodHandle mh = LINKER.downcallHandle(symbol, f);
- if (mh.type().returnType() == MemorySegment.class) {
- mh = mh.bindTo(allocator);
- }
- // flatten argument list so that it can be passed to an asSpreader MH
- Object[] allArgs = new Object[nNamedArgs + unnamedArgs.length];
- System.arraycopy(args, 0, allArgs, 0, nNamedArgs);
- System.arraycopy(unnamedArgs, 0, allArgs, nNamedArgs, unnamedArgs.length);
-
- return mh.asSpreader(Object[].class, argsCount).invoke(allArgs);
- }
-
- private static Class> unboxIfNeeded(Class> clazz) {
- if (clazz == Boolean.class) {
- return boolean.class;
- } else if (clazz == Void.class) {
- return void.class;
- } else if (clazz == Byte.class) {
- return byte.class;
- } else if (clazz == Character.class) {
- return char.class;
- } else if (clazz == Short.class) {
- return short.class;
- } else if (clazz == Integer.class) {
- return int.class;
- } else if (clazz == Long.class) {
- return long.class;
- } else if (clazz == Float.class) {
- return float.class;
- } else if (clazz == Double.class) {
- return double.class;
- } else {
- return clazz;
- }
- }
-
- private Class> promote(Class> c) {
- if (c == byte.class || c == char.class || c == short.class || c == int.class) {
- return long.class;
- } else if (c == float.class) {
- return double.class;
- } else {
- return c;
- }
- }
-
- private Class> normalize(Class> c) {
- c = unboxIfNeeded(c);
- if (c.isPrimitive()) {
- return promote(c);
- }
- if (MemoryAddress.class.isAssignableFrom(c)) {
- return MemoryAddress.class;
- }
- if (MemorySegment.class.isAssignableFrom(c)) {
- return MemorySegment.class;
- }
- throw new IllegalArgumentException("Invalid type for ABI: " + c.getTypeName());
- }
-
- private MemoryLayout variadicLayout(Class> c) {
- if (c == long.class) {
- return JAVA_LONG;
- } else if (c == double.class) {
- return JAVA_DOUBLE;
- } else if (MemoryAddress.class.isAssignableFrom(c)) {
- return ADDRESS;
- } else {
- throw new IllegalArgumentException("Unhandled variadic argument class: " + c);
- }
- }
- }
-}
diff --git a/java-does-usb/src/main/java/net/codecrete/usb/linux/gen/select/constants$0.java b/java-does-usb/src/main/java/net/codecrete/usb/linux/gen/select/constants$0.java
deleted file mode 100644
index 1e095f2b..00000000
--- a/java-does-usb/src/main/java/net/codecrete/usb/linux/gen/select/constants$0.java
+++ /dev/null
@@ -1,22 +0,0 @@
-// Generated by jextract
-
-package net.codecrete.usb.linux.gen.select;
-
-import java.lang.foreign.FunctionDescriptor;
-import java.lang.invoke.MethodHandle;
-class constants$0 {
-
- static final FunctionDescriptor select$FUNC = FunctionDescriptor.of(Constants$root.C_INT$LAYOUT,
- Constants$root.C_INT$LAYOUT,
- Constants$root.C_POINTER$LAYOUT,
- Constants$root.C_POINTER$LAYOUT,
- Constants$root.C_POINTER$LAYOUT,
- Constants$root.C_POINTER$LAYOUT
- );
- static final MethodHandle select$MH = RuntimeHelper.downcallHandle(
- "select",
- constants$0.select$FUNC
- );
-}
-
-
diff --git a/java-does-usb/src/main/java/net/codecrete/usb/linux/gen/select/fd_set.java b/java-does-usb/src/main/java/net/codecrete/usb/linux/gen/select/fd_set.java
deleted file mode 100644
index 2a321b89..00000000
--- a/java-does-usb/src/main/java/net/codecrete/usb/linux/gen/select/fd_set.java
+++ /dev/null
@@ -1,25 +0,0 @@
-// Generated by jextract
-
-package net.codecrete.usb.linux.gen.select;
-
-import java.lang.foreign.*;
-public class fd_set {
-
- static final GroupLayout $struct$LAYOUT = MemoryLayout.structLayout(
- MemoryLayout.sequenceLayout(16, Constants$root.C_LONG_LONG$LAYOUT).withName("__fds_bits")
- );
- public static MemoryLayout $LAYOUT() {
- return fd_set.$struct$LAYOUT;
- }
- public static MemorySegment __fds_bits$slice(MemorySegment seg) {
- return seg.asSlice(0, 128);
- }
- public static long sizeof() { return $LAYOUT().byteSize(); }
- public static MemorySegment allocate(SegmentAllocator allocator) { return allocator.allocate($LAYOUT()); }
- public static MemorySegment allocateArray(int len, SegmentAllocator allocator) {
- return allocator.allocate(MemoryLayout.sequenceLayout(len, $LAYOUT()));
- }
- public static MemorySegment ofAddress(MemoryAddress addr, MemorySession session) { return RuntimeHelper.asArray(addr, $LAYOUT(), 1, session); }
-}
-
-
diff --git a/java-does-usb/src/main/java/net/codecrete/usb/linux/gen/select/select.java b/java-does-usb/src/main/java/net/codecrete/usb/linux/gen/select/select.java
deleted file mode 100644
index 85ed94b2..00000000
--- a/java-does-usb/src/main/java/net/codecrete/usb/linux/gen/select/select.java
+++ /dev/null
@@ -1,33 +0,0 @@
-// Generated by jextract
-
-package net.codecrete.usb.linux.gen.select;
-
-import java.lang.foreign.Addressable;
-import java.lang.invoke.MethodHandle;
-
-import static java.lang.foreign.ValueLayout.*;
-public class select {
-
- /* package-private */ select() {}
- public static OfByte C_CHAR = Constants$root.C_CHAR$LAYOUT;
- public static OfShort C_SHORT = Constants$root.C_SHORT$LAYOUT;
- public static OfInt C_INT = Constants$root.C_INT$LAYOUT;
- public static OfLong C_LONG = Constants$root.C_LONG_LONG$LAYOUT;
- public static OfLong C_LONG_LONG = Constants$root.C_LONG_LONG$LAYOUT;
- public static OfFloat C_FLOAT = Constants$root.C_FLOAT$LAYOUT;
- public static OfDouble C_DOUBLE = Constants$root.C_DOUBLE$LAYOUT;
- public static OfAddress C_POINTER = Constants$root.C_POINTER$LAYOUT;
- public static MethodHandle select$MH() {
- return RuntimeHelper.requireNonNull(constants$0.select$MH,"select");
- }
- public static int select ( int __nfds, Addressable __readfds, Addressable __writefds, Addressable __exceptfds, Addressable __timeout) {
- var mh$ = select$MH();
- try {
- return (int)mh$.invokeExact(__nfds, __readfds, __writefds, __exceptfds, __timeout);
- } catch (Throwable ex$) {
- throw new AssertionError("should not reach here", ex$);
- }
- }
-}
-
-
diff --git a/java-does-usb/src/main/java/net/codecrete/usb/linux/gen/string/string$shared.java b/java-does-usb/src/main/java/net/codecrete/usb/linux/gen/string/string$shared.java
new file mode 100644
index 00000000..824bb277
--- /dev/null
+++ b/java-does-usb/src/main/java/net/codecrete/usb/linux/gen/string/string$shared.java
@@ -0,0 +1,63 @@
+// Generated by jextract
+
+package net.codecrete.usb.linux.gen.string;
+
+import java.lang.invoke.*;
+import java.lang.foreign.*;
+import java.nio.ByteOrder;
+import java.util.*;
+import java.util.function.*;
+import java.util.stream.*;
+
+import static java.lang.foreign.ValueLayout.*;
+import static java.lang.foreign.MemoryLayout.PathElement.*;
+
+public class string$shared {
+
+ string$shared() {
+ // Should not be called directly
+ }
+
+ public static final ValueLayout.OfBoolean C_BOOL = (ValueLayout.OfBoolean) Linker.nativeLinker().canonicalLayouts().get("bool");
+ public static final ValueLayout.OfByte C_CHAR =(ValueLayout.OfByte)Linker.nativeLinker().canonicalLayouts().get("char");
+ public static final ValueLayout.OfShort C_SHORT = (ValueLayout.OfShort) Linker.nativeLinker().canonicalLayouts().get("short");
+ public static final ValueLayout.OfInt C_INT = (ValueLayout.OfInt) Linker.nativeLinker().canonicalLayouts().get("int");
+ public static final ValueLayout.OfLong C_LONG_LONG = (ValueLayout.OfLong) Linker.nativeLinker().canonicalLayouts().get("long long");
+ public static final ValueLayout.OfFloat C_FLOAT = (ValueLayout.OfFloat) Linker.nativeLinker().canonicalLayouts().get("float");
+ public static final ValueLayout.OfDouble C_DOUBLE = (ValueLayout.OfDouble) Linker.nativeLinker().canonicalLayouts().get("double");
+ public static final AddressLayout C_POINTER = ((AddressLayout) Linker.nativeLinker().canonicalLayouts().get("void*"))
+ .withTargetLayout(MemoryLayout.sequenceLayout(java.lang.Long.MAX_VALUE, C_CHAR));
+ public static final ValueLayout.OfLong C_LONG = (ValueLayout.OfLong) Linker.nativeLinker().canonicalLayouts().get("long");
+
+ static final boolean TRACE_DOWNCALLS = Boolean.getBoolean("jextract.trace.downcalls");
+
+ static void traceDowncall(String name, Object... args) {
+ String traceArgs = Arrays.stream(args)
+ .map(Object::toString)
+ .collect(Collectors.joining(", "));
+ System.out.printf("%s(%s)\n", name, traceArgs);
+ }
+
+ static MethodHandle upcallHandle(Class> fi, String name, FunctionDescriptor fdesc) {
+ try {
+ return MethodHandles.lookup().findVirtual(fi, name, fdesc.toMethodType());
+ } catch (ReflectiveOperationException ex) {
+ throw new AssertionError(ex);
+ }
+ }
+
+ static MemoryLayout align(MemoryLayout layout, long align) {
+ return switch (layout) {
+ case PaddingLayout p -> p;
+ case ValueLayout v -> v.withByteAlignment(align);
+ case GroupLayout g -> {
+ MemoryLayout[] alignedMembers = g.memberLayouts().stream()
+ .map(m -> align(m, align)).toArray(MemoryLayout[]::new);
+ yield g instanceof StructLayout ?
+ MemoryLayout.structLayout(alignedMembers) : MemoryLayout.unionLayout(alignedMembers);
+ }
+ case SequenceLayout s -> MemoryLayout.sequenceLayout(s.elementCount(), align(s.elementLayout(), align));
+ };
+ }
+}
+
diff --git a/java-does-usb/src/main/java/net/codecrete/usb/linux/gen/string/string.java b/java-does-usb/src/main/java/net/codecrete/usb/linux/gen/string/string.java
new file mode 100644
index 00000000..de0e4452
--- /dev/null
+++ b/java-does-usb/src/main/java/net/codecrete/usb/linux/gen/string/string.java
@@ -0,0 +1,87 @@
+// Generated by jextract
+
+package net.codecrete.usb.linux.gen.string;
+
+import java.lang.invoke.*;
+import java.lang.foreign.*;
+import java.nio.ByteOrder;
+import java.util.*;
+import java.util.function.*;
+import java.util.stream.*;
+
+import static java.lang.foreign.ValueLayout.*;
+import static java.lang.foreign.MemoryLayout.PathElement.*;
+
+public class string extends string$shared {
+
+ string() {
+ // Should not be called directly
+ }
+
+ static final Arena LIBRARY_ARENA = Arena.ofAuto();
+
+ static final SymbolLookup SYMBOL_LOOKUP = SymbolLookup.loaderLookup()
+ .or(Linker.nativeLinker().defaultLookup());
+
+
+ private static class strerror {
+ public static final FunctionDescriptor DESC = FunctionDescriptor.of(
+ string.C_POINTER,
+ string.C_INT
+ );
+
+ public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("strerror");
+
+ public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC);
+ }
+
+ /**
+ * Function descriptor for:
+ * {@snippet lang=c :
+ * extern char *strerror(int __errnum)
+ * }
+ */
+ public static FunctionDescriptor strerror$descriptor() {
+ return strerror.DESC;
+ }
+
+ /**
+ * Downcall method handle for:
+ * {@snippet lang=c :
+ * extern char *strerror(int __errnum)
+ * }
+ */
+ public static MethodHandle strerror$handle() {
+ return strerror.HANDLE;
+ }
+
+ /**
+ * Address for:
+ * {@snippet lang=c :
+ * extern char *strerror(int __errnum)
+ * }
+ */
+ public static MemorySegment strerror$address() {
+ return strerror.ADDR;
+ }
+
+ /**
+ * {@snippet lang=c :
+ * extern char *strerror(int __errnum)
+ * }
+ */
+ public static MemorySegment strerror(int __errnum) {
+ var mh$ = strerror.HANDLE;
+ try {
+ if (TRACE_DOWNCALLS) {
+ traceDowncall("strerror", __errnum);
+ }
+ return (MemorySegment)mh$.invokeExact(__errnum);
+ } catch (Error | RuntimeException ex) {
+ throw ex;
+ } catch (Throwable ex$) {
+ throw new AssertionError("should not reach here", ex$);
+ }
+ }
+}
+
diff --git a/java-does-usb/src/main/java/net/codecrete/usb/linux/gen/udev/Constants$root.java b/java-does-usb/src/main/java/net/codecrete/usb/linux/gen/udev/Constants$root.java
deleted file mode 100644
index 2a06330e..00000000
--- a/java-does-usb/src/main/java/net/codecrete/usb/linux/gen/udev/Constants$root.java
+++ /dev/null
@@ -1,19 +0,0 @@
-// Generated by jextract
-
-package net.codecrete.usb.linux.gen.udev;
-
-import static java.lang.foreign.ValueLayout.*;
-public class Constants$root {
-
- static final OfBoolean C_BOOL$LAYOUT = JAVA_BOOLEAN;
- static final OfByte C_CHAR$LAYOUT = JAVA_BYTE;
- static final OfShort C_SHORT$LAYOUT = JAVA_SHORT.withBitAlignment(16);
- static final OfInt C_INT$LAYOUT = JAVA_INT.withBitAlignment(32);
- static final OfLong C_LONG$LAYOUT = JAVA_LONG.withBitAlignment(64);
- static final OfLong C_LONG_LONG$LAYOUT = JAVA_LONG.withBitAlignment(64);
- static final OfFloat C_FLOAT$LAYOUT = JAVA_FLOAT.withBitAlignment(32);
- static final OfDouble C_DOUBLE$LAYOUT = JAVA_DOUBLE.withBitAlignment(64);
- static final OfAddress C_POINTER$LAYOUT = ADDRESS.withBitAlignment(64);
-}
-
-
diff --git a/java-does-usb/src/main/java/net/codecrete/usb/linux/gen/udev/RuntimeHelper.java b/java-does-usb/src/main/java/net/codecrete/usb/linux/gen/udev/RuntimeHelper.java
deleted file mode 100644
index 775c0b6c..00000000
--- a/java-does-usb/src/main/java/net/codecrete/usb/linux/gen/udev/RuntimeHelper.java
+++ /dev/null
@@ -1,217 +0,0 @@
-package net.codecrete.usb.linux.gen.udev;
-// Generated by jextract
-
-import java.lang.foreign.*;
-import java.lang.invoke.MethodHandle;
-import java.lang.invoke.MethodHandles;
-import java.lang.invoke.MethodType;
-
-import static java.lang.foreign.ValueLayout.*;
-
-final class RuntimeHelper {
-
- private RuntimeHelper() {}
- private final static Linker LINKER = Linker.nativeLinker();
- private final static ClassLoader LOADER = RuntimeHelper.class.getClassLoader();
- private final static MethodHandles.Lookup MH_LOOKUP = MethodHandles.lookup();
- private final static SymbolLookup SYMBOL_LOOKUP;
-
- final static SegmentAllocator CONSTANT_ALLOCATOR =
- (size, align) -> MemorySegment.allocateNative(size, align, MemorySession.openImplicit());
-
- static {
-// System.loadLibrary("udev");
-// SymbolLookup loaderLookup = SymbolLookup.loaderLookup();
- SymbolLookup loaderLookup = SymbolLookup.libraryLookup("libudev.so", MemorySession.openImplicit());
- SYMBOL_LOOKUP = name -> loaderLookup.lookup(name).or(() -> LINKER.defaultLookup().lookup(name));
- }
-
- static T requireNonNull(T obj, String symbolName) {
- if (obj == null) {
- throw new UnsatisfiedLinkError("unresolved symbol: " + symbolName);
- }
- return obj;
- }
-
- private final static SegmentAllocator THROWING_ALLOCATOR = (x, y) -> { throw new AssertionError("should not reach here"); };
-
- static final MemorySegment lookupGlobalVariable(String name, MemoryLayout layout) {
- return SYMBOL_LOOKUP.lookup(name).map(symbol -> MemorySegment.ofAddress(symbol.address(), layout.byteSize(), MemorySession.openShared())).orElse(null);
- }
-
- static final MethodHandle downcallHandle(String name, FunctionDescriptor fdesc) {
- return SYMBOL_LOOKUP.lookup(name).
- map(addr -> LINKER.downcallHandle(addr, fdesc)).
- orElse(null);
- }
-
- static final MethodHandle downcallHandle(FunctionDescriptor fdesc) {
- return LINKER.downcallHandle(fdesc);
- }
-
- static final MethodHandle downcallHandleVariadic(String name, FunctionDescriptor fdesc) {
- return SYMBOL_LOOKUP.lookup(name).
- map(addr -> VarargsInvoker.make(addr, fdesc)).
- orElse(null);
- }
-
- static final MemorySegment upcallStub(Class fi, Z z, FunctionDescriptor fdesc, MemorySession session) {
- try {
- MethodHandle handle = MH_LOOKUP.findVirtual(fi, "apply", Linker.upcallType(fdesc));
- handle = handle.bindTo(z);
- return LINKER.upcallStub(handle, fdesc, session);
- } catch (Throwable ex) {
- throw new AssertionError(ex);
- }
- }
-
- static MemorySegment asArray(MemoryAddress addr, MemoryLayout layout, int numElements, MemorySession session) {
- return MemorySegment.ofAddress(addr, numElements * layout.byteSize(), session);
- }
-
- // Internals only below this point
-
- private static class VarargsInvoker {
- private static final MethodHandle INVOKE_MH;
- private final MemorySegment symbol;
- private final FunctionDescriptor function;
-
- private VarargsInvoker(MemorySegment symbol, FunctionDescriptor function) {
- this.symbol = symbol;
- this.function = function;
- }
-
- static {
- try {
- INVOKE_MH = MethodHandles.lookup().findVirtual(VarargsInvoker.class, "invoke", MethodType.methodType(Object.class, SegmentAllocator.class, Object[].class));
- } catch (ReflectiveOperationException e) {
- throw new RuntimeException(e);
- }
- }
-
- static MethodHandle make(MemorySegment symbol, FunctionDescriptor function) {
- VarargsInvoker invoker = new VarargsInvoker(symbol, function);
- MethodHandle handle = INVOKE_MH.bindTo(invoker).asCollector(Object[].class, function.argumentLayouts().size() + 1);
- MethodType mtype = MethodType.methodType(function.returnLayout().isPresent() ? carrier(function.returnLayout().get(), true) : void.class);
- for (MemoryLayout layout : function.argumentLayouts()) {
- mtype = mtype.appendParameterTypes(carrier(layout, false));
- }
- mtype = mtype.appendParameterTypes(Object[].class);
- if (mtype.returnType().equals(MemorySegment.class)) {
- mtype = mtype.insertParameterTypes(0, SegmentAllocator.class);
- } else {
- handle = MethodHandles.insertArguments(handle, 0, THROWING_ALLOCATOR);
- }
- return handle.asType(mtype);
- }
-
- static Class> carrier(MemoryLayout layout, boolean ret) {
- if (layout instanceof ValueLayout valueLayout) {
- return (ret || valueLayout.carrier() != MemoryAddress.class) ?
- valueLayout.carrier() : Addressable.class;
- } else if (layout instanceof GroupLayout) {
- return MemorySegment.class;
- } else {
- throw new AssertionError("Cannot get here!");
- }
- }
-
- private Object invoke(SegmentAllocator allocator, Object[] args) throws Throwable {
- // one trailing Object[]
- int nNamedArgs = function.argumentLayouts().size();
- assert(args.length == nNamedArgs + 1);
- // The last argument is the array of vararg collector
- Object[] unnamedArgs = (Object[]) args[args.length - 1];
-
- int argsCount = nNamedArgs + unnamedArgs.length;
- Class>[] argTypes = new Class>[argsCount];
- MemoryLayout[] argLayouts = new MemoryLayout[nNamedArgs + unnamedArgs.length];
-
- int pos = 0;
- for (pos = 0; pos < nNamedArgs; pos++) {
- argLayouts[pos] = function.argumentLayouts().get(pos);
- }
-
- assert pos == nNamedArgs;
- for (Object o: unnamedArgs) {
- argLayouts[pos] = variadicLayout(normalize(o.getClass()));
- pos++;
- }
- assert pos == argsCount;
-
- FunctionDescriptor f = (function.returnLayout().isEmpty()) ?
- FunctionDescriptor.ofVoid(argLayouts) :
- FunctionDescriptor.of(function.returnLayout().get(), argLayouts);
- MethodHandle mh = LINKER.downcallHandle(symbol, f);
- if (mh.type().returnType() == MemorySegment.class) {
- mh = mh.bindTo(allocator);
- }
- // flatten argument list so that it can be passed to an asSpreader MH
- Object[] allArgs = new Object[nNamedArgs + unnamedArgs.length];
- System.arraycopy(args, 0, allArgs, 0, nNamedArgs);
- System.arraycopy(unnamedArgs, 0, allArgs, nNamedArgs, unnamedArgs.length);
-
- return mh.asSpreader(Object[].class, argsCount).invoke(allArgs);
- }
-
- private static Class> unboxIfNeeded(Class> clazz) {
- if (clazz == Boolean.class) {
- return boolean.class;
- } else if (clazz == Void.class) {
- return void.class;
- } else if (clazz == Byte.class) {
- return byte.class;
- } else if (clazz == Character.class) {
- return char.class;
- } else if (clazz == Short.class) {
- return short.class;
- } else if (clazz == Integer.class) {
- return int.class;
- } else if (clazz == Long.class) {
- return long.class;
- } else if (clazz == Float.class) {
- return float.class;
- } else if (clazz == Double.class) {
- return double.class;
- } else {
- return clazz;
- }
- }
-
- private Class> promote(Class> c) {
- if (c == byte.class || c == char.class || c == short.class || c == int.class) {
- return long.class;
- } else if (c == float.class) {
- return double.class;
- } else {
- return c;
- }
- }
-
- private Class> normalize(Class> c) {
- c = unboxIfNeeded(c);
- if (c.isPrimitive()) {
- return promote(c);
- }
- if (MemoryAddress.class.isAssignableFrom(c)) {
- return MemoryAddress.class;
- }
- if (MemorySegment.class.isAssignableFrom(c)) {
- return MemorySegment.class;
- }
- throw new IllegalArgumentException("Invalid type for ABI: " + c.getTypeName());
- }
-
- private MemoryLayout variadicLayout(Class> c) {
- if (c == long.class) {
- return JAVA_LONG;
- } else if (c == double.class) {
- return JAVA_DOUBLE;
- } else if (MemoryAddress.class.isAssignableFrom(c)) {
- return ADDRESS;
- } else {
- throw new IllegalArgumentException("Unhandled variadic argument class: " + c);
- }
- }
- }
-}
diff --git a/java-does-usb/src/main/java/net/codecrete/usb/linux/gen/udev/constants$0.java b/java-does-usb/src/main/java/net/codecrete/usb/linux/gen/udev/constants$0.java
deleted file mode 100644
index 962ea187..00000000
--- a/java-does-usb/src/main/java/net/codecrete/usb/linux/gen/udev/constants$0.java
+++ /dev/null
@@ -1,52 +0,0 @@
-// Generated by jextract
-
-package net.codecrete.usb.linux.gen.udev;
-
-import java.lang.foreign.FunctionDescriptor;
-import java.lang.invoke.MethodHandle;
-class constants$0 {
-
- static final FunctionDescriptor udev_new$FUNC = FunctionDescriptor.of(Constants$root.C_POINTER$LAYOUT);
- static final MethodHandle udev_new$MH = RuntimeHelper.downcallHandle(
- "udev_new",
- constants$0.udev_new$FUNC
- );
- static final FunctionDescriptor udev_list_entry_get_next$FUNC = FunctionDescriptor.of(Constants$root.C_POINTER$LAYOUT,
- Constants$root.C_POINTER$LAYOUT
- );
- static final MethodHandle udev_list_entry_get_next$MH = RuntimeHelper.downcallHandle(
- "udev_list_entry_get_next",
- constants$0.udev_list_entry_get_next$FUNC
- );
- static final FunctionDescriptor udev_list_entry_get_name$FUNC = FunctionDescriptor.of(Constants$root.C_POINTER$LAYOUT,
- Constants$root.C_POINTER$LAYOUT
- );
- static final MethodHandle udev_list_entry_get_name$MH = RuntimeHelper.downcallHandle(
- "udev_list_entry_get_name",
- constants$0.udev_list_entry_get_name$FUNC
- );
- static final FunctionDescriptor udev_device_unref$FUNC = FunctionDescriptor.of(Constants$root.C_POINTER$LAYOUT,
- Constants$root.C_POINTER$LAYOUT
- );
- static final MethodHandle udev_device_unref$MH = RuntimeHelper.downcallHandle(
- "udev_device_unref",
- constants$0.udev_device_unref$FUNC
- );
- static final FunctionDescriptor udev_device_new_from_syspath$FUNC = FunctionDescriptor.of(Constants$root.C_POINTER$LAYOUT,
- Constants$root.C_POINTER$LAYOUT,
- Constants$root.C_POINTER$LAYOUT
- );
- static final MethodHandle udev_device_new_from_syspath$MH = RuntimeHelper.downcallHandle(
- "udev_device_new_from_syspath",
- constants$0.udev_device_new_from_syspath$FUNC
- );
- static final FunctionDescriptor udev_device_get_devtype$FUNC = FunctionDescriptor.of(Constants$root.C_POINTER$LAYOUT,
- Constants$root.C_POINTER$LAYOUT
- );
- static final MethodHandle udev_device_get_devtype$MH = RuntimeHelper.downcallHandle(
- "udev_device_get_devtype",
- constants$0.udev_device_get_devtype$FUNC
- );
-}
-
-
diff --git a/java-does-usb/src/main/java/net/codecrete/usb/linux/gen/udev/constants$1.java b/java-does-usb/src/main/java/net/codecrete/usb/linux/gen/udev/constants$1.java
deleted file mode 100644
index 986a90d3..00000000
--- a/java-does-usb/src/main/java/net/codecrete/usb/linux/gen/udev/constants$1.java
+++ /dev/null
@@ -1,55 +0,0 @@
-// Generated by jextract
-
-package net.codecrete.usb.linux.gen.udev;
-
-import java.lang.foreign.FunctionDescriptor;
-import java.lang.invoke.MethodHandle;
-class constants$1 {
-
- static final FunctionDescriptor udev_device_get_devnode$FUNC = FunctionDescriptor.of(Constants$root.C_POINTER$LAYOUT,
- Constants$root.C_POINTER$LAYOUT
- );
- static final MethodHandle udev_device_get_devnode$MH = RuntimeHelper.downcallHandle(
- "udev_device_get_devnode",
- constants$1.udev_device_get_devnode$FUNC
- );
- static final FunctionDescriptor udev_device_get_action$FUNC = FunctionDescriptor.of(Constants$root.C_POINTER$LAYOUT,
- Constants$root.C_POINTER$LAYOUT
- );
- static final MethodHandle udev_device_get_action$MH = RuntimeHelper.downcallHandle(
- "udev_device_get_action",
- constants$1.udev_device_get_action$FUNC
- );
- static final FunctionDescriptor udev_device_get_sysattr_value$FUNC = FunctionDescriptor.of(Constants$root.C_POINTER$LAYOUT,
- Constants$root.C_POINTER$LAYOUT,
- Constants$root.C_POINTER$LAYOUT
- );
- static final MethodHandle udev_device_get_sysattr_value$MH = RuntimeHelper.downcallHandle(
- "udev_device_get_sysattr_value",
- constants$1.udev_device_get_sysattr_value$FUNC
- );
- static final FunctionDescriptor udev_monitor_new_from_netlink$FUNC = FunctionDescriptor.of(Constants$root.C_POINTER$LAYOUT,
- Constants$root.C_POINTER$LAYOUT,
- Constants$root.C_POINTER$LAYOUT
- );
- static final MethodHandle udev_monitor_new_from_netlink$MH = RuntimeHelper.downcallHandle(
- "udev_monitor_new_from_netlink",
- constants$1.udev_monitor_new_from_netlink$FUNC
- );
- static final FunctionDescriptor udev_monitor_enable_receiving$FUNC = FunctionDescriptor.of(Constants$root.C_INT$LAYOUT,
- Constants$root.C_POINTER$LAYOUT
- );
- static final MethodHandle udev_monitor_enable_receiving$MH = RuntimeHelper.downcallHandle(
- "udev_monitor_enable_receiving",
- constants$1.udev_monitor_enable_receiving$FUNC
- );
- static final FunctionDescriptor udev_monitor_get_fd$FUNC = FunctionDescriptor.of(Constants$root.C_INT$LAYOUT,
- Constants$root.C_POINTER$LAYOUT
- );
- static final MethodHandle udev_monitor_get_fd$MH = RuntimeHelper.downcallHandle(
- "udev_monitor_get_fd",
- constants$1.udev_monitor_get_fd$FUNC
- );
-}
-
-
diff --git a/java-does-usb/src/main/java/net/codecrete/usb/linux/gen/udev/constants$2.java b/java-does-usb/src/main/java/net/codecrete/usb/linux/gen/udev/constants$2.java
deleted file mode 100644
index 47b95ed6..00000000
--- a/java-does-usb/src/main/java/net/codecrete/usb/linux/gen/udev/constants$2.java
+++ /dev/null
@@ -1,56 +0,0 @@
-// Generated by jextract
-
-package net.codecrete.usb.linux.gen.udev;
-
-import java.lang.foreign.FunctionDescriptor;
-import java.lang.invoke.MethodHandle;
-class constants$2 {
-
- static final FunctionDescriptor udev_monitor_receive_device$FUNC = FunctionDescriptor.of(Constants$root.C_POINTER$LAYOUT,
- Constants$root.C_POINTER$LAYOUT
- );
- static final MethodHandle udev_monitor_receive_device$MH = RuntimeHelper.downcallHandle(
- "udev_monitor_receive_device",
- constants$2.udev_monitor_receive_device$FUNC
- );
- static final FunctionDescriptor udev_monitor_filter_add_match_subsystem_devtype$FUNC = FunctionDescriptor.of(Constants$root.C_INT$LAYOUT,
- Constants$root.C_POINTER$LAYOUT,
- Constants$root.C_POINTER$LAYOUT,
- Constants$root.C_POINTER$LAYOUT
- );
- static final MethodHandle udev_monitor_filter_add_match_subsystem_devtype$MH = RuntimeHelper.downcallHandle(
- "udev_monitor_filter_add_match_subsystem_devtype",
- constants$2.udev_monitor_filter_add_match_subsystem_devtype$FUNC
- );
- static final FunctionDescriptor udev_enumerate_unref$FUNC = FunctionDescriptor.of(Constants$root.C_POINTER$LAYOUT,
- Constants$root.C_POINTER$LAYOUT
- );
- static final MethodHandle udev_enumerate_unref$MH = RuntimeHelper.downcallHandle(
- "udev_enumerate_unref",
- constants$2.udev_enumerate_unref$FUNC
- );
- static final FunctionDescriptor udev_enumerate_new$FUNC = FunctionDescriptor.of(Constants$root.C_POINTER$LAYOUT,
- Constants$root.C_POINTER$LAYOUT
- );
- static final MethodHandle udev_enumerate_new$MH = RuntimeHelper.downcallHandle(
- "udev_enumerate_new",
- constants$2.udev_enumerate_new$FUNC
- );
- static final FunctionDescriptor udev_enumerate_add_match_subsystem$FUNC = FunctionDescriptor.of(Constants$root.C_INT$LAYOUT,
- Constants$root.C_POINTER$LAYOUT,
- Constants$root.C_POINTER$LAYOUT
- );
- static final MethodHandle udev_enumerate_add_match_subsystem$MH = RuntimeHelper.downcallHandle(
- "udev_enumerate_add_match_subsystem",
- constants$2.udev_enumerate_add_match_subsystem$FUNC
- );
- static final FunctionDescriptor udev_enumerate_scan_devices$FUNC = FunctionDescriptor.of(Constants$root.C_INT$LAYOUT,
- Constants$root.C_POINTER$LAYOUT
- );
- static final MethodHandle udev_enumerate_scan_devices$MH = RuntimeHelper.downcallHandle(
- "udev_enumerate_scan_devices",
- constants$2.udev_enumerate_scan_devices$FUNC
- );
-}
-
-
diff --git a/java-does-usb/src/main/java/net/codecrete/usb/linux/gen/udev/constants$3.java b/java-does-usb/src/main/java/net/codecrete/usb/linux/gen/udev/constants$3.java
deleted file mode 100644
index 862a7270..00000000
--- a/java-does-usb/src/main/java/net/codecrete/usb/linux/gen/udev/constants$3.java
+++ /dev/null
@@ -1,18 +0,0 @@
-// Generated by jextract
-
-package net.codecrete.usb.linux.gen.udev;
-
-import java.lang.foreign.FunctionDescriptor;
-import java.lang.invoke.MethodHandle;
-class constants$3 {
-
- static final FunctionDescriptor udev_enumerate_get_list_entry$FUNC = FunctionDescriptor.of(Constants$root.C_POINTER$LAYOUT,
- Constants$root.C_POINTER$LAYOUT
- );
- static final MethodHandle udev_enumerate_get_list_entry$MH = RuntimeHelper.downcallHandle(
- "udev_enumerate_get_list_entry",
- constants$3.udev_enumerate_get_list_entry$FUNC
- );
-}
-
-
diff --git a/java-does-usb/src/main/java/net/codecrete/usb/linux/gen/udev/udev$shared.java b/java-does-usb/src/main/java/net/codecrete/usb/linux/gen/udev/udev$shared.java
new file mode 100644
index 00000000..b7958526
--- /dev/null
+++ b/java-does-usb/src/main/java/net/codecrete/usb/linux/gen/udev/udev$shared.java
@@ -0,0 +1,63 @@
+// Generated by jextract
+
+package net.codecrete.usb.linux.gen.udev;
+
+import java.lang.invoke.*;
+import java.lang.foreign.*;
+import java.nio.ByteOrder;
+import java.util.*;
+import java.util.function.*;
+import java.util.stream.*;
+
+import static java.lang.foreign.ValueLayout.*;
+import static java.lang.foreign.MemoryLayout.PathElement.*;
+
+public class udev$shared {
+
+ udev$shared() {
+ // Should not be called directly
+ }
+
+ public static final ValueLayout.OfBoolean C_BOOL = (ValueLayout.OfBoolean) Linker.nativeLinker().canonicalLayouts().get("bool");
+ public static final ValueLayout.OfByte C_CHAR =(ValueLayout.OfByte)Linker.nativeLinker().canonicalLayouts().get("char");
+ public static final ValueLayout.OfShort C_SHORT = (ValueLayout.OfShort) Linker.nativeLinker().canonicalLayouts().get("short");
+ public static final ValueLayout.OfInt C_INT = (ValueLayout.OfInt) Linker.nativeLinker().canonicalLayouts().get("int");
+ public static final ValueLayout.OfLong C_LONG_LONG = (ValueLayout.OfLong) Linker.nativeLinker().canonicalLayouts().get("long long");
+ public static final ValueLayout.OfFloat C_FLOAT = (ValueLayout.OfFloat) Linker.nativeLinker().canonicalLayouts().get("float");
+ public static final ValueLayout.OfDouble C_DOUBLE = (ValueLayout.OfDouble) Linker.nativeLinker().canonicalLayouts().get("double");
+ public static final AddressLayout C_POINTER = ((AddressLayout) Linker.nativeLinker().canonicalLayouts().get("void*"))
+ .withTargetLayout(MemoryLayout.sequenceLayout(java.lang.Long.MAX_VALUE, C_CHAR));
+ public static final ValueLayout.OfLong C_LONG = (ValueLayout.OfLong) Linker.nativeLinker().canonicalLayouts().get("long");
+
+ static final boolean TRACE_DOWNCALLS = Boolean.getBoolean("jextract.trace.downcalls");
+
+ static void traceDowncall(String name, Object... args) {
+ String traceArgs = Arrays.stream(args)
+ .map(Object::toString)
+ .collect(Collectors.joining(", "));
+ System.out.printf("%s(%s)\n", name, traceArgs);
+ }
+
+ static MethodHandle upcallHandle(Class> fi, String name, FunctionDescriptor fdesc) {
+ try {
+ return MethodHandles.lookup().findVirtual(fi, name, fdesc.toMethodType());
+ } catch (ReflectiveOperationException ex) {
+ throw new AssertionError(ex);
+ }
+ }
+
+ static MemoryLayout align(MemoryLayout layout, long align) {
+ return switch (layout) {
+ case PaddingLayout p -> p;
+ case ValueLayout v -> v.withByteAlignment(align);
+ case GroupLayout g -> {
+ MemoryLayout[] alignedMembers = g.memberLayouts().stream()
+ .map(m -> align(m, align)).toArray(MemoryLayout[]::new);
+ yield g instanceof StructLayout ?
+ MemoryLayout.structLayout(alignedMembers) : MemoryLayout.unionLayout(alignedMembers);
+ }
+ case SequenceLayout s -> MemoryLayout.sequenceLayout(s.elementCount(), align(s.elementLayout(), align));
+ };
+ }
+}
+
diff --git a/java-does-usb/src/main/java/net/codecrete/usb/linux/gen/udev/udev.java b/java-does-usb/src/main/java/net/codecrete/usb/linux/gen/udev/udev.java
index ea5160ae..f43a699d 100644
--- a/java-does-usb/src/main/java/net/codecrete/usb/linux/gen/udev/udev.java
+++ b/java-does-usb/src/main/java/net/codecrete/usb/linux/gen/udev/udev.java
@@ -2,231 +2,1171 @@
package net.codecrete.usb.linux.gen.udev;
-import java.lang.foreign.Addressable;
-import java.lang.foreign.MemoryAddress;
-import java.lang.invoke.MethodHandle;
+import java.lang.invoke.*;
+import java.lang.foreign.*;
+import java.nio.ByteOrder;
+import java.util.*;
+import java.util.function.*;
+import java.util.stream.*;
import static java.lang.foreign.ValueLayout.*;
-public class udev {
+import static java.lang.foreign.MemoryLayout.PathElement.*;
- /* package-private */ udev() {}
- public static OfByte C_CHAR = Constants$root.C_CHAR$LAYOUT;
- public static OfShort C_SHORT = Constants$root.C_SHORT$LAYOUT;
- public static OfInt C_INT = Constants$root.C_INT$LAYOUT;
- public static OfLong C_LONG = Constants$root.C_LONG_LONG$LAYOUT;
- public static OfLong C_LONG_LONG = Constants$root.C_LONG_LONG$LAYOUT;
- public static OfFloat C_FLOAT = Constants$root.C_FLOAT$LAYOUT;
- public static OfDouble C_DOUBLE = Constants$root.C_DOUBLE$LAYOUT;
- public static OfAddress C_POINTER = Constants$root.C_POINTER$LAYOUT;
- public static MethodHandle udev_new$MH() {
- return RuntimeHelper.requireNonNull(constants$0.udev_new$MH,"udev_new");
+public class udev extends udev$shared {
+
+ udev() {
+ // Should not be called directly
}
- public static MemoryAddress udev_new () {
- var mh$ = udev_new$MH();
+
+ static final Arena LIBRARY_ARENA = Arena.ofAuto();
+
+ static final SymbolLookup SYMBOL_LOOKUP = SymbolLookup.libraryLookup("libudev.so.1", LIBRARY_ARENA)
+ .or(SymbolLookup.loaderLookup())
+ .or(Linker.nativeLinker().defaultLookup());
+
+
+ private static class udev_new {
+ public static final FunctionDescriptor DESC = FunctionDescriptor.of(
+ udev.C_POINTER );
+
+ public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("udev_new");
+
+ public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC);
+ }
+
+ /**
+ * Function descriptor for:
+ * {@snippet lang=c :
+ * struct udev *udev_new(void)
+ * }
+ */
+ public static FunctionDescriptor udev_new$descriptor() {
+ return udev_new.DESC;
+ }
+
+ /**
+ * Downcall method handle for:
+ * {@snippet lang=c :
+ * struct udev *udev_new(void)
+ * }
+ */
+ public static MethodHandle udev_new$handle() {
+ return udev_new.HANDLE;
+ }
+
+ /**
+ * Address for:
+ * {@snippet lang=c :
+ * struct udev *udev_new(void)
+ * }
+ */
+ public static MemorySegment udev_new$address() {
+ return udev_new.ADDR;
+ }
+
+ /**
+ * {@snippet lang=c :
+ * struct udev *udev_new(void)
+ * }
+ */
+ public static MemorySegment udev_new() {
+ var mh$ = udev_new.HANDLE;
try {
- return (java.lang.foreign.MemoryAddress)mh$.invokeExact();
+ if (TRACE_DOWNCALLS) {
+ traceDowncall("udev_new");
+ }
+ return (MemorySegment)mh$.invokeExact();
+ } catch (Error | RuntimeException ex) {
+ throw ex;
} catch (Throwable ex$) {
- throw new AssertionError("should not reach here", ex$);
+ throw new AssertionError("should not reach here", ex$);
}
}
- public static MethodHandle udev_list_entry_get_next$MH() {
- return RuntimeHelper.requireNonNull(constants$0.udev_list_entry_get_next$MH,"udev_list_entry_get_next");
+
+ private static class udev_list_entry_get_next {
+ public static final FunctionDescriptor DESC = FunctionDescriptor.of(
+ udev.C_POINTER,
+ udev.C_POINTER
+ );
+
+ public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("udev_list_entry_get_next");
+
+ public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC);
+ }
+
+ /**
+ * Function descriptor for:
+ * {@snippet lang=c :
+ * struct udev_list_entry *udev_list_entry_get_next(struct udev_list_entry *list_entry)
+ * }
+ */
+ public static FunctionDescriptor udev_list_entry_get_next$descriptor() {
+ return udev_list_entry_get_next.DESC;
+ }
+
+ /**
+ * Downcall method handle for:
+ * {@snippet lang=c :
+ * struct udev_list_entry *udev_list_entry_get_next(struct udev_list_entry *list_entry)
+ * }
+ */
+ public static MethodHandle udev_list_entry_get_next$handle() {
+ return udev_list_entry_get_next.HANDLE;
}
- public static MemoryAddress udev_list_entry_get_next ( Addressable list_entry) {
- var mh$ = udev_list_entry_get_next$MH();
+
+ /**
+ * Address for:
+ * {@snippet lang=c :
+ * struct udev_list_entry *udev_list_entry_get_next(struct udev_list_entry *list_entry)
+ * }
+ */
+ public static MemorySegment udev_list_entry_get_next$address() {
+ return udev_list_entry_get_next.ADDR;
+ }
+
+ /**
+ * {@snippet lang=c :
+ * struct udev_list_entry *udev_list_entry_get_next(struct udev_list_entry *list_entry)
+ * }
+ */
+ public static MemorySegment udev_list_entry_get_next(MemorySegment list_entry) {
+ var mh$ = udev_list_entry_get_next.HANDLE;
try {
- return (java.lang.foreign.MemoryAddress)mh$.invokeExact(list_entry);
+ if (TRACE_DOWNCALLS) {
+ traceDowncall("udev_list_entry_get_next", list_entry);
+ }
+ return (MemorySegment)mh$.invokeExact(list_entry);
+ } catch (Error | RuntimeException ex) {
+ throw ex;
} catch (Throwable ex$) {
- throw new AssertionError("should not reach here", ex$);
+ throw new AssertionError("should not reach here", ex$);
}
}
- public static MethodHandle udev_list_entry_get_name$MH() {
- return RuntimeHelper.requireNonNull(constants$0.udev_list_entry_get_name$MH,"udev_list_entry_get_name");
+
+ private static class udev_list_entry_get_name {
+ public static final FunctionDescriptor DESC = FunctionDescriptor.of(
+ udev.C_POINTER,
+ udev.C_POINTER
+ );
+
+ public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("udev_list_entry_get_name");
+
+ public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC);
+ }
+
+ /**
+ * Function descriptor for:
+ * {@snippet lang=c :
+ * const char *udev_list_entry_get_name(struct udev_list_entry *list_entry)
+ * }
+ */
+ public static FunctionDescriptor udev_list_entry_get_name$descriptor() {
+ return udev_list_entry_get_name.DESC;
+ }
+
+ /**
+ * Downcall method handle for:
+ * {@snippet lang=c :
+ * const char *udev_list_entry_get_name(struct udev_list_entry *list_entry)
+ * }
+ */
+ public static MethodHandle udev_list_entry_get_name$handle() {
+ return udev_list_entry_get_name.HANDLE;
+ }
+
+ /**
+ * Address for:
+ * {@snippet lang=c :
+ * const char *udev_list_entry_get_name(struct udev_list_entry *list_entry)
+ * }
+ */
+ public static MemorySegment udev_list_entry_get_name$address() {
+ return udev_list_entry_get_name.ADDR;
}
- public static MemoryAddress udev_list_entry_get_name ( Addressable list_entry) {
- var mh$ = udev_list_entry_get_name$MH();
+
+ /**
+ * {@snippet lang=c :
+ * const char *udev_list_entry_get_name(struct udev_list_entry *list_entry)
+ * }
+ */
+ public static MemorySegment udev_list_entry_get_name(MemorySegment list_entry) {
+ var mh$ = udev_list_entry_get_name.HANDLE;
try {
- return (java.lang.foreign.MemoryAddress)mh$.invokeExact(list_entry);
+ if (TRACE_DOWNCALLS) {
+ traceDowncall("udev_list_entry_get_name", list_entry);
+ }
+ return (MemorySegment)mh$.invokeExact(list_entry);
+ } catch (Error | RuntimeException ex) {
+ throw ex;
} catch (Throwable ex$) {
- throw new AssertionError("should not reach here", ex$);
+ throw new AssertionError("should not reach here", ex$);
}
}
- public static MethodHandle udev_device_unref$MH() {
- return RuntimeHelper.requireNonNull(constants$0.udev_device_unref$MH,"udev_device_unref");
+
+ private static class udev_device_unref {
+ public static final FunctionDescriptor DESC = FunctionDescriptor.of(
+ udev.C_POINTER,
+ udev.C_POINTER
+ );
+
+ public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("udev_device_unref");
+
+ public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC);
+ }
+
+ /**
+ * Function descriptor for:
+ * {@snippet lang=c :
+ * struct udev_device *udev_device_unref(struct udev_device *udev_device)
+ * }
+ */
+ public static FunctionDescriptor udev_device_unref$descriptor() {
+ return udev_device_unref.DESC;
+ }
+
+ /**
+ * Downcall method handle for:
+ * {@snippet lang=c :
+ * struct udev_device *udev_device_unref(struct udev_device *udev_device)
+ * }
+ */
+ public static MethodHandle udev_device_unref$handle() {
+ return udev_device_unref.HANDLE;
+ }
+
+ /**
+ * Address for:
+ * {@snippet lang=c :
+ * struct udev_device *udev_device_unref(struct udev_device *udev_device)
+ * }
+ */
+ public static MemorySegment udev_device_unref$address() {
+ return udev_device_unref.ADDR;
}
- public static MemoryAddress udev_device_unref ( Addressable udev_device) {
- var mh$ = udev_device_unref$MH();
+
+ /**
+ * {@snippet lang=c :
+ * struct udev_device *udev_device_unref(struct udev_device *udev_device)
+ * }
+ */
+ public static MemorySegment udev_device_unref(MemorySegment udev_device) {
+ var mh$ = udev_device_unref.HANDLE;
try {
- return (java.lang.foreign.MemoryAddress)mh$.invokeExact(udev_device);
+ if (TRACE_DOWNCALLS) {
+ traceDowncall("udev_device_unref", udev_device);
+ }
+ return (MemorySegment)mh$.invokeExact(udev_device);
+ } catch (Error | RuntimeException ex) {
+ throw ex;
} catch (Throwable ex$) {
- throw new AssertionError("should not reach here", ex$);
+ throw new AssertionError("should not reach here", ex$);
}
}
- public static MethodHandle udev_device_new_from_syspath$MH() {
- return RuntimeHelper.requireNonNull(constants$0.udev_device_new_from_syspath$MH,"udev_device_new_from_syspath");
+
+ private static class udev_device_new_from_syspath {
+ public static final FunctionDescriptor DESC = FunctionDescriptor.of(
+ udev.C_POINTER,
+ udev.C_POINTER,
+ udev.C_POINTER
+ );
+
+ public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("udev_device_new_from_syspath");
+
+ public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC);
+ }
+
+ /**
+ * Function descriptor for:
+ * {@snippet lang=c :
+ * struct udev_device *udev_device_new_from_syspath(struct udev *udev, const char *syspath)
+ * }
+ */
+ public static FunctionDescriptor udev_device_new_from_syspath$descriptor() {
+ return udev_device_new_from_syspath.DESC;
+ }
+
+ /**
+ * Downcall method handle for:
+ * {@snippet lang=c :
+ * struct udev_device *udev_device_new_from_syspath(struct udev *udev, const char *syspath)
+ * }
+ */
+ public static MethodHandle udev_device_new_from_syspath$handle() {
+ return udev_device_new_from_syspath.HANDLE;
}
- public static MemoryAddress udev_device_new_from_syspath ( Addressable udev, Addressable syspath) {
- var mh$ = udev_device_new_from_syspath$MH();
+
+ /**
+ * Address for:
+ * {@snippet lang=c :
+ * struct udev_device *udev_device_new_from_syspath(struct udev *udev, const char *syspath)
+ * }
+ */
+ public static MemorySegment udev_device_new_from_syspath$address() {
+ return udev_device_new_from_syspath.ADDR;
+ }
+
+ /**
+ * {@snippet lang=c :
+ * struct udev_device *udev_device_new_from_syspath(struct udev *udev, const char *syspath)
+ * }
+ */
+ public static MemorySegment udev_device_new_from_syspath(MemorySegment udev, MemorySegment syspath) {
+ var mh$ = udev_device_new_from_syspath.HANDLE;
try {
- return (java.lang.foreign.MemoryAddress)mh$.invokeExact(udev, syspath);
+ if (TRACE_DOWNCALLS) {
+ traceDowncall("udev_device_new_from_syspath", udev, syspath);
+ }
+ return (MemorySegment)mh$.invokeExact(udev, syspath);
+ } catch (Error | RuntimeException ex) {
+ throw ex;
} catch (Throwable ex$) {
- throw new AssertionError("should not reach here", ex$);
+ throw new AssertionError("should not reach here", ex$);
}
}
- public static MethodHandle udev_device_get_devtype$MH() {
- return RuntimeHelper.requireNonNull(constants$0.udev_device_get_devtype$MH,"udev_device_get_devtype");
+
+ private static class udev_device_get_devtype {
+ public static final FunctionDescriptor DESC = FunctionDescriptor.of(
+ udev.C_POINTER,
+ udev.C_POINTER
+ );
+
+ public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("udev_device_get_devtype");
+
+ public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC);
+ }
+
+ /**
+ * Function descriptor for:
+ * {@snippet lang=c :
+ * const char *udev_device_get_devtype(struct udev_device *udev_device)
+ * }
+ */
+ public static FunctionDescriptor udev_device_get_devtype$descriptor() {
+ return udev_device_get_devtype.DESC;
+ }
+
+ /**
+ * Downcall method handle for:
+ * {@snippet lang=c :
+ * const char *udev_device_get_devtype(struct udev_device *udev_device)
+ * }
+ */
+ public static MethodHandle udev_device_get_devtype$handle() {
+ return udev_device_get_devtype.HANDLE;
}
- public static MemoryAddress udev_device_get_devtype ( Addressable udev_device) {
- var mh$ = udev_device_get_devtype$MH();
+
+ /**
+ * Address for:
+ * {@snippet lang=c :
+ * const char *udev_device_get_devtype(struct udev_device *udev_device)
+ * }
+ */
+ public static MemorySegment udev_device_get_devtype$address() {
+ return udev_device_get_devtype.ADDR;
+ }
+
+ /**
+ * {@snippet lang=c :
+ * const char *udev_device_get_devtype(struct udev_device *udev_device)
+ * }
+ */
+ public static MemorySegment udev_device_get_devtype(MemorySegment udev_device) {
+ var mh$ = udev_device_get_devtype.HANDLE;
try {
- return (java.lang.foreign.MemoryAddress)mh$.invokeExact(udev_device);
+ if (TRACE_DOWNCALLS) {
+ traceDowncall("udev_device_get_devtype", udev_device);
+ }
+ return (MemorySegment)mh$.invokeExact(udev_device);
+ } catch (Error | RuntimeException ex) {
+ throw ex;
} catch (Throwable ex$) {
- throw new AssertionError("should not reach here", ex$);
+ throw new AssertionError("should not reach here", ex$);
}
}
- public static MethodHandle udev_device_get_devnode$MH() {
- return RuntimeHelper.requireNonNull(constants$1.udev_device_get_devnode$MH,"udev_device_get_devnode");
+
+ private static class udev_device_get_devnode {
+ public static final FunctionDescriptor DESC = FunctionDescriptor.of(
+ udev.C_POINTER,
+ udev.C_POINTER
+ );
+
+ public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("udev_device_get_devnode");
+
+ public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC);
+ }
+
+ /**
+ * Function descriptor for:
+ * {@snippet lang=c :
+ * const char *udev_device_get_devnode(struct udev_device *udev_device)
+ * }
+ */
+ public static FunctionDescriptor udev_device_get_devnode$descriptor() {
+ return udev_device_get_devnode.DESC;
+ }
+
+ /**
+ * Downcall method handle for:
+ * {@snippet lang=c :
+ * const char *udev_device_get_devnode(struct udev_device *udev_device)
+ * }
+ */
+ public static MethodHandle udev_device_get_devnode$handle() {
+ return udev_device_get_devnode.HANDLE;
+ }
+
+ /**
+ * Address for:
+ * {@snippet lang=c :
+ * const char *udev_device_get_devnode(struct udev_device *udev_device)
+ * }
+ */
+ public static MemorySegment udev_device_get_devnode$address() {
+ return udev_device_get_devnode.ADDR;
}
- public static MemoryAddress udev_device_get_devnode ( Addressable udev_device) {
- var mh$ = udev_device_get_devnode$MH();
+
+ /**
+ * {@snippet lang=c :
+ * const char *udev_device_get_devnode(struct udev_device *udev_device)
+ * }
+ */
+ public static MemorySegment udev_device_get_devnode(MemorySegment udev_device) {
+ var mh$ = udev_device_get_devnode.HANDLE;
try {
- return (java.lang.foreign.MemoryAddress)mh$.invokeExact(udev_device);
+ if (TRACE_DOWNCALLS) {
+ traceDowncall("udev_device_get_devnode", udev_device);
+ }
+ return (MemorySegment)mh$.invokeExact(udev_device);
+ } catch (Error | RuntimeException ex) {
+ throw ex;
} catch (Throwable ex$) {
- throw new AssertionError("should not reach here", ex$);
+ throw new AssertionError("should not reach here", ex$);
}
}
- public static MethodHandle udev_device_get_action$MH() {
- return RuntimeHelper.requireNonNull(constants$1.udev_device_get_action$MH,"udev_device_get_action");
+
+ private static class udev_device_get_action {
+ public static final FunctionDescriptor DESC = FunctionDescriptor.of(
+ udev.C_POINTER,
+ udev.C_POINTER
+ );
+
+ public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("udev_device_get_action");
+
+ public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC);
+ }
+
+ /**
+ * Function descriptor for:
+ * {@snippet lang=c :
+ * const char *udev_device_get_action(struct udev_device *udev_device)
+ * }
+ */
+ public static FunctionDescriptor udev_device_get_action$descriptor() {
+ return udev_device_get_action.DESC;
+ }
+
+ /**
+ * Downcall method handle for:
+ * {@snippet lang=c :
+ * const char *udev_device_get_action(struct udev_device *udev_device)
+ * }
+ */
+ public static MethodHandle udev_device_get_action$handle() {
+ return udev_device_get_action.HANDLE;
+ }
+
+ /**
+ * Address for:
+ * {@snippet lang=c :
+ * const char *udev_device_get_action(struct udev_device *udev_device)
+ * }
+ */
+ public static MemorySegment udev_device_get_action$address() {
+ return udev_device_get_action.ADDR;
}
- public static MemoryAddress udev_device_get_action ( Addressable udev_device) {
- var mh$ = udev_device_get_action$MH();
+
+ /**
+ * {@snippet lang=c :
+ * const char *udev_device_get_action(struct udev_device *udev_device)
+ * }
+ */
+ public static MemorySegment udev_device_get_action(MemorySegment udev_device) {
+ var mh$ = udev_device_get_action.HANDLE;
try {
- return (java.lang.foreign.MemoryAddress)mh$.invokeExact(udev_device);
+ if (TRACE_DOWNCALLS) {
+ traceDowncall("udev_device_get_action", udev_device);
+ }
+ return (MemorySegment)mh$.invokeExact(udev_device);
+ } catch (Error | RuntimeException ex) {
+ throw ex;
} catch (Throwable ex$) {
- throw new AssertionError("should not reach here", ex$);
+ throw new AssertionError("should not reach here", ex$);
}
}
- public static MethodHandle udev_device_get_sysattr_value$MH() {
- return RuntimeHelper.requireNonNull(constants$1.udev_device_get_sysattr_value$MH,"udev_device_get_sysattr_value");
+
+ private static class udev_device_get_sysattr_value {
+ public static final FunctionDescriptor DESC = FunctionDescriptor.of(
+ udev.C_POINTER,
+ udev.C_POINTER,
+ udev.C_POINTER
+ );
+
+ public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("udev_device_get_sysattr_value");
+
+ public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC);
+ }
+
+ /**
+ * Function descriptor for:
+ * {@snippet lang=c :
+ * const char *udev_device_get_sysattr_value(struct udev_device *udev_device, const char *sysattr)
+ * }
+ */
+ public static FunctionDescriptor udev_device_get_sysattr_value$descriptor() {
+ return udev_device_get_sysattr_value.DESC;
+ }
+
+ /**
+ * Downcall method handle for:
+ * {@snippet lang=c :
+ * const char *udev_device_get_sysattr_value(struct udev_device *udev_device, const char *sysattr)
+ * }
+ */
+ public static MethodHandle udev_device_get_sysattr_value$handle() {
+ return udev_device_get_sysattr_value.HANDLE;
+ }
+
+ /**
+ * Address for:
+ * {@snippet lang=c :
+ * const char *udev_device_get_sysattr_value(struct udev_device *udev_device, const char *sysattr)
+ * }
+ */
+ public static MemorySegment udev_device_get_sysattr_value$address() {
+ return udev_device_get_sysattr_value.ADDR;
}
- public static MemoryAddress udev_device_get_sysattr_value ( Addressable udev_device, Addressable sysattr) {
- var mh$ = udev_device_get_sysattr_value$MH();
+
+ /**
+ * {@snippet lang=c :
+ * const char *udev_device_get_sysattr_value(struct udev_device *udev_device, const char *sysattr)
+ * }
+ */
+ public static MemorySegment udev_device_get_sysattr_value(MemorySegment udev_device, MemorySegment sysattr) {
+ var mh$ = udev_device_get_sysattr_value.HANDLE;
try {
- return (java.lang.foreign.MemoryAddress)mh$.invokeExact(udev_device, sysattr);
+ if (TRACE_DOWNCALLS) {
+ traceDowncall("udev_device_get_sysattr_value", udev_device, sysattr);
+ }
+ return (MemorySegment)mh$.invokeExact(udev_device, sysattr);
+ } catch (Error | RuntimeException ex) {
+ throw ex;
} catch (Throwable ex$) {
- throw new AssertionError("should not reach here", ex$);
+ throw new AssertionError("should not reach here", ex$);
}
}
- public static MethodHandle udev_monitor_new_from_netlink$MH() {
- return RuntimeHelper.requireNonNull(constants$1.udev_monitor_new_from_netlink$MH,"udev_monitor_new_from_netlink");
+
+ private static class udev_monitor_new_from_netlink {
+ public static final FunctionDescriptor DESC = FunctionDescriptor.of(
+ udev.C_POINTER,
+ udev.C_POINTER,
+ udev.C_POINTER
+ );
+
+ public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("udev_monitor_new_from_netlink");
+
+ public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC);
+ }
+
+ /**
+ * Function descriptor for:
+ * {@snippet lang=c :
+ * struct udev_monitor *udev_monitor_new_from_netlink(struct udev *udev, const char *name)
+ * }
+ */
+ public static FunctionDescriptor udev_monitor_new_from_netlink$descriptor() {
+ return udev_monitor_new_from_netlink.DESC;
+ }
+
+ /**
+ * Downcall method handle for:
+ * {@snippet lang=c :
+ * struct udev_monitor *udev_monitor_new_from_netlink(struct udev *udev, const char *name)
+ * }
+ */
+ public static MethodHandle udev_monitor_new_from_netlink$handle() {
+ return udev_monitor_new_from_netlink.HANDLE;
}
- public static MemoryAddress udev_monitor_new_from_netlink ( Addressable udev, Addressable name) {
- var mh$ = udev_monitor_new_from_netlink$MH();
+
+ /**
+ * Address for:
+ * {@snippet lang=c :
+ * struct udev_monitor *udev_monitor_new_from_netlink(struct udev *udev, const char *name)
+ * }
+ */
+ public static MemorySegment udev_monitor_new_from_netlink$address() {
+ return udev_monitor_new_from_netlink.ADDR;
+ }
+
+ /**
+ * {@snippet lang=c :
+ * struct udev_monitor *udev_monitor_new_from_netlink(struct udev *udev, const char *name)
+ * }
+ */
+ public static MemorySegment udev_monitor_new_from_netlink(MemorySegment udev, MemorySegment name) {
+ var mh$ = udev_monitor_new_from_netlink.HANDLE;
try {
- return (java.lang.foreign.MemoryAddress)mh$.invokeExact(udev, name);
+ if (TRACE_DOWNCALLS) {
+ traceDowncall("udev_monitor_new_from_netlink", udev, name);
+ }
+ return (MemorySegment)mh$.invokeExact(udev, name);
+ } catch (Error | RuntimeException ex) {
+ throw ex;
} catch (Throwable ex$) {
- throw new AssertionError("should not reach here", ex$);
+ throw new AssertionError("should not reach here", ex$);
}
}
- public static MethodHandle udev_monitor_enable_receiving$MH() {
- return RuntimeHelper.requireNonNull(constants$1.udev_monitor_enable_receiving$MH,"udev_monitor_enable_receiving");
+
+ private static class udev_monitor_enable_receiving {
+ public static final FunctionDescriptor DESC = FunctionDescriptor.of(
+ udev.C_INT,
+ udev.C_POINTER
+ );
+
+ public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("udev_monitor_enable_receiving");
+
+ public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC);
}
- public static int udev_monitor_enable_receiving ( Addressable udev_monitor) {
- var mh$ = udev_monitor_enable_receiving$MH();
+
+ /**
+ * Function descriptor for:
+ * {@snippet lang=c :
+ * int udev_monitor_enable_receiving(struct udev_monitor *udev_monitor)
+ * }
+ */
+ public static FunctionDescriptor udev_monitor_enable_receiving$descriptor() {
+ return udev_monitor_enable_receiving.DESC;
+ }
+
+ /**
+ * Downcall method handle for:
+ * {@snippet lang=c :
+ * int udev_monitor_enable_receiving(struct udev_monitor *udev_monitor)
+ * }
+ */
+ public static MethodHandle udev_monitor_enable_receiving$handle() {
+ return udev_monitor_enable_receiving.HANDLE;
+ }
+
+ /**
+ * Address for:
+ * {@snippet lang=c :
+ * int udev_monitor_enable_receiving(struct udev_monitor *udev_monitor)
+ * }
+ */
+ public static MemorySegment udev_monitor_enable_receiving$address() {
+ return udev_monitor_enable_receiving.ADDR;
+ }
+
+ /**
+ * {@snippet lang=c :
+ * int udev_monitor_enable_receiving(struct udev_monitor *udev_monitor)
+ * }
+ */
+ public static int udev_monitor_enable_receiving(MemorySegment udev_monitor) {
+ var mh$ = udev_monitor_enable_receiving.HANDLE;
try {
+ if (TRACE_DOWNCALLS) {
+ traceDowncall("udev_monitor_enable_receiving", udev_monitor);
+ }
return (int)mh$.invokeExact(udev_monitor);
+ } catch (Error | RuntimeException ex) {
+ throw ex;
} catch (Throwable ex$) {
- throw new AssertionError("should not reach here", ex$);
+ throw new AssertionError("should not reach here", ex$);
}
}
- public static MethodHandle udev_monitor_get_fd$MH() {
- return RuntimeHelper.requireNonNull(constants$1.udev_monitor_get_fd$MH,"udev_monitor_get_fd");
+
+ private static class udev_monitor_get_fd {
+ public static final FunctionDescriptor DESC = FunctionDescriptor.of(
+ udev.C_INT,
+ udev.C_POINTER
+ );
+
+ public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("udev_monitor_get_fd");
+
+ public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC);
+ }
+
+ /**
+ * Function descriptor for:
+ * {@snippet lang=c :
+ * int udev_monitor_get_fd(struct udev_monitor *udev_monitor)
+ * }
+ */
+ public static FunctionDescriptor udev_monitor_get_fd$descriptor() {
+ return udev_monitor_get_fd.DESC;
+ }
+
+ /**
+ * Downcall method handle for:
+ * {@snippet lang=c :
+ * int udev_monitor_get_fd(struct udev_monitor *udev_monitor)
+ * }
+ */
+ public static MethodHandle udev_monitor_get_fd$handle() {
+ return udev_monitor_get_fd.HANDLE;
}
- public static int udev_monitor_get_fd ( Addressable udev_monitor) {
- var mh$ = udev_monitor_get_fd$MH();
+
+ /**
+ * Address for:
+ * {@snippet lang=c :
+ * int udev_monitor_get_fd(struct udev_monitor *udev_monitor)
+ * }
+ */
+ public static MemorySegment udev_monitor_get_fd$address() {
+ return udev_monitor_get_fd.ADDR;
+ }
+
+ /**
+ * {@snippet lang=c :
+ * int udev_monitor_get_fd(struct udev_monitor *udev_monitor)
+ * }
+ */
+ public static int udev_monitor_get_fd(MemorySegment udev_monitor) {
+ var mh$ = udev_monitor_get_fd.HANDLE;
try {
+ if (TRACE_DOWNCALLS) {
+ traceDowncall("udev_monitor_get_fd", udev_monitor);
+ }
return (int)mh$.invokeExact(udev_monitor);
+ } catch (Error | RuntimeException ex) {
+ throw ex;
} catch (Throwable ex$) {
- throw new AssertionError("should not reach here", ex$);
+ throw new AssertionError("should not reach here", ex$);
}
}
- public static MethodHandle udev_monitor_receive_device$MH() {
- return RuntimeHelper.requireNonNull(constants$2.udev_monitor_receive_device$MH,"udev_monitor_receive_device");
+
+ private static class udev_monitor_receive_device {
+ public static final FunctionDescriptor DESC = FunctionDescriptor.of(
+ udev.C_POINTER,
+ udev.C_POINTER
+ );
+
+ public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("udev_monitor_receive_device");
+
+ public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC);
+ }
+
+ /**
+ * Function descriptor for:
+ * {@snippet lang=c :
+ * struct udev_device *udev_monitor_receive_device(struct udev_monitor *udev_monitor)
+ * }
+ */
+ public static FunctionDescriptor udev_monitor_receive_device$descriptor() {
+ return udev_monitor_receive_device.DESC;
+ }
+
+ /**
+ * Downcall method handle for:
+ * {@snippet lang=c :
+ * struct udev_device *udev_monitor_receive_device(struct udev_monitor *udev_monitor)
+ * }
+ */
+ public static MethodHandle udev_monitor_receive_device$handle() {
+ return udev_monitor_receive_device.HANDLE;
}
- public static MemoryAddress udev_monitor_receive_device ( Addressable udev_monitor) {
- var mh$ = udev_monitor_receive_device$MH();
+
+ /**
+ * Address for:
+ * {@snippet lang=c :
+ * struct udev_device *udev_monitor_receive_device(struct udev_monitor *udev_monitor)
+ * }
+ */
+ public static MemorySegment udev_monitor_receive_device$address() {
+ return udev_monitor_receive_device.ADDR;
+ }
+
+ /**
+ * {@snippet lang=c :
+ * struct udev_device *udev_monitor_receive_device(struct udev_monitor *udev_monitor)
+ * }
+ */
+ public static MemorySegment udev_monitor_receive_device(MemorySegment udev_monitor) {
+ var mh$ = udev_monitor_receive_device.HANDLE;
try {
- return (java.lang.foreign.MemoryAddress)mh$.invokeExact(udev_monitor);
+ if (TRACE_DOWNCALLS) {
+ traceDowncall("udev_monitor_receive_device", udev_monitor);
+ }
+ return (MemorySegment)mh$.invokeExact(udev_monitor);
+ } catch (Error | RuntimeException ex) {
+ throw ex;
} catch (Throwable ex$) {
- throw new AssertionError("should not reach here", ex$);
+ throw new AssertionError("should not reach here", ex$);
}
}
- public static MethodHandle udev_monitor_filter_add_match_subsystem_devtype$MH() {
- return RuntimeHelper.requireNonNull(constants$2.udev_monitor_filter_add_match_subsystem_devtype$MH,"udev_monitor_filter_add_match_subsystem_devtype");
+
+ private static class udev_monitor_filter_add_match_subsystem_devtype {
+ public static final FunctionDescriptor DESC = FunctionDescriptor.of(
+ udev.C_INT,
+ udev.C_POINTER,
+ udev.C_POINTER,
+ udev.C_POINTER
+ );
+
+ public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("udev_monitor_filter_add_match_subsystem_devtype");
+
+ public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC);
+ }
+
+ /**
+ * Function descriptor for:
+ * {@snippet lang=c :
+ * int udev_monitor_filter_add_match_subsystem_devtype(struct udev_monitor *udev_monitor, const char *subsystem, const char *devtype)
+ * }
+ */
+ public static FunctionDescriptor udev_monitor_filter_add_match_subsystem_devtype$descriptor() {
+ return udev_monitor_filter_add_match_subsystem_devtype.DESC;
+ }
+
+ /**
+ * Downcall method handle for:
+ * {@snippet lang=c :
+ * int udev_monitor_filter_add_match_subsystem_devtype(struct udev_monitor *udev_monitor, const char *subsystem, const char *devtype)
+ * }
+ */
+ public static MethodHandle udev_monitor_filter_add_match_subsystem_devtype$handle() {
+ return udev_monitor_filter_add_match_subsystem_devtype.HANDLE;
+ }
+
+ /**
+ * Address for:
+ * {@snippet lang=c :
+ * int udev_monitor_filter_add_match_subsystem_devtype(struct udev_monitor *udev_monitor, const char *subsystem, const char *devtype)
+ * }
+ */
+ public static MemorySegment udev_monitor_filter_add_match_subsystem_devtype$address() {
+ return udev_monitor_filter_add_match_subsystem_devtype.ADDR;
}
- public static int udev_monitor_filter_add_match_subsystem_devtype ( Addressable udev_monitor, Addressable subsystem, Addressable devtype) {
- var mh$ = udev_monitor_filter_add_match_subsystem_devtype$MH();
+
+ /**
+ * {@snippet lang=c :
+ * int udev_monitor_filter_add_match_subsystem_devtype(struct udev_monitor *udev_monitor, const char *subsystem, const char *devtype)
+ * }
+ */
+ public static int udev_monitor_filter_add_match_subsystem_devtype(MemorySegment udev_monitor, MemorySegment subsystem, MemorySegment devtype) {
+ var mh$ = udev_monitor_filter_add_match_subsystem_devtype.HANDLE;
try {
+ if (TRACE_DOWNCALLS) {
+ traceDowncall("udev_monitor_filter_add_match_subsystem_devtype", udev_monitor, subsystem, devtype);
+ }
return (int)mh$.invokeExact(udev_monitor, subsystem, devtype);
+ } catch (Error | RuntimeException ex) {
+ throw ex;
} catch (Throwable ex$) {
- throw new AssertionError("should not reach here", ex$);
+ throw new AssertionError("should not reach here", ex$);
}
}
- public static MethodHandle udev_enumerate_unref$MH() {
- return RuntimeHelper.requireNonNull(constants$2.udev_enumerate_unref$MH,"udev_enumerate_unref");
+
+ private static class udev_enumerate_unref {
+ public static final FunctionDescriptor DESC = FunctionDescriptor.of(
+ udev.C_POINTER,
+ udev.C_POINTER
+ );
+
+ public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("udev_enumerate_unref");
+
+ public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC);
+ }
+
+ /**
+ * Function descriptor for:
+ * {@snippet lang=c :
+ * struct udev_enumerate *udev_enumerate_unref(struct udev_enumerate *udev_enumerate)
+ * }
+ */
+ public static FunctionDescriptor udev_enumerate_unref$descriptor() {
+ return udev_enumerate_unref.DESC;
+ }
+
+ /**
+ * Downcall method handle for:
+ * {@snippet lang=c :
+ * struct udev_enumerate *udev_enumerate_unref(struct udev_enumerate *udev_enumerate)
+ * }
+ */
+ public static MethodHandle udev_enumerate_unref$handle() {
+ return udev_enumerate_unref.HANDLE;
+ }
+
+ /**
+ * Address for:
+ * {@snippet lang=c :
+ * struct udev_enumerate *udev_enumerate_unref(struct udev_enumerate *udev_enumerate)
+ * }
+ */
+ public static MemorySegment udev_enumerate_unref$address() {
+ return udev_enumerate_unref.ADDR;
}
- public static MemoryAddress udev_enumerate_unref ( Addressable udev_enumerate) {
- var mh$ = udev_enumerate_unref$MH();
+
+ /**
+ * {@snippet lang=c :
+ * struct udev_enumerate *udev_enumerate_unref(struct udev_enumerate *udev_enumerate)
+ * }
+ */
+ public static MemorySegment udev_enumerate_unref(MemorySegment udev_enumerate) {
+ var mh$ = udev_enumerate_unref.HANDLE;
try {
- return (java.lang.foreign.MemoryAddress)mh$.invokeExact(udev_enumerate);
+ if (TRACE_DOWNCALLS) {
+ traceDowncall("udev_enumerate_unref", udev_enumerate);
+ }
+ return (MemorySegment)mh$.invokeExact(udev_enumerate);
+ } catch (Error | RuntimeException ex) {
+ throw ex;
} catch (Throwable ex$) {
- throw new AssertionError("should not reach here", ex$);
+ throw new AssertionError("should not reach here", ex$);
}
}
- public static MethodHandle udev_enumerate_new$MH() {
- return RuntimeHelper.requireNonNull(constants$2.udev_enumerate_new$MH,"udev_enumerate_new");
+
+ private static class udev_enumerate_new {
+ public static final FunctionDescriptor DESC = FunctionDescriptor.of(
+ udev.C_POINTER,
+ udev.C_POINTER
+ );
+
+ public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("udev_enumerate_new");
+
+ public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC);
+ }
+
+ /**
+ * Function descriptor for:
+ * {@snippet lang=c :
+ * struct udev_enumerate *udev_enumerate_new(struct udev *udev)
+ * }
+ */
+ public static FunctionDescriptor udev_enumerate_new$descriptor() {
+ return udev_enumerate_new.DESC;
}
- public static MemoryAddress udev_enumerate_new ( Addressable udev) {
- var mh$ = udev_enumerate_new$MH();
+
+ /**
+ * Downcall method handle for:
+ * {@snippet lang=c :
+ * struct udev_enumerate *udev_enumerate_new(struct udev *udev)
+ * }
+ */
+ public static MethodHandle udev_enumerate_new$handle() {
+ return udev_enumerate_new.HANDLE;
+ }
+
+ /**
+ * Address for:
+ * {@snippet lang=c :
+ * struct udev_enumerate *udev_enumerate_new(struct udev *udev)
+ * }
+ */
+ public static MemorySegment udev_enumerate_new$address() {
+ return udev_enumerate_new.ADDR;
+ }
+
+ /**
+ * {@snippet lang=c :
+ * struct udev_enumerate *udev_enumerate_new(struct udev *udev)
+ * }
+ */
+ public static MemorySegment udev_enumerate_new(MemorySegment udev) {
+ var mh$ = udev_enumerate_new.HANDLE;
try {
- return (java.lang.foreign.MemoryAddress)mh$.invokeExact(udev);
+ if (TRACE_DOWNCALLS) {
+ traceDowncall("udev_enumerate_new", udev);
+ }
+ return (MemorySegment)mh$.invokeExact(udev);
+ } catch (Error | RuntimeException ex) {
+ throw ex;
} catch (Throwable ex$) {
- throw new AssertionError("should not reach here", ex$);
+ throw new AssertionError("should not reach here", ex$);
}
}
- public static MethodHandle udev_enumerate_add_match_subsystem$MH() {
- return RuntimeHelper.requireNonNull(constants$2.udev_enumerate_add_match_subsystem$MH,"udev_enumerate_add_match_subsystem");
+
+ private static class udev_enumerate_add_match_subsystem {
+ public static final FunctionDescriptor DESC = FunctionDescriptor.of(
+ udev.C_INT,
+ udev.C_POINTER,
+ udev.C_POINTER
+ );
+
+ public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("udev_enumerate_add_match_subsystem");
+
+ public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC);
+ }
+
+ /**
+ * Function descriptor for:
+ * {@snippet lang=c :
+ * int udev_enumerate_add_match_subsystem(struct udev_enumerate *udev_enumerate, const char *subsystem)
+ * }
+ */
+ public static FunctionDescriptor udev_enumerate_add_match_subsystem$descriptor() {
+ return udev_enumerate_add_match_subsystem.DESC;
+ }
+
+ /**
+ * Downcall method handle for:
+ * {@snippet lang=c :
+ * int udev_enumerate_add_match_subsystem(struct udev_enumerate *udev_enumerate, const char *subsystem)
+ * }
+ */
+ public static MethodHandle udev_enumerate_add_match_subsystem$handle() {
+ return udev_enumerate_add_match_subsystem.HANDLE;
}
- public static int udev_enumerate_add_match_subsystem ( Addressable udev_enumerate, Addressable subsystem) {
- var mh$ = udev_enumerate_add_match_subsystem$MH();
+
+ /**
+ * Address for:
+ * {@snippet lang=c :
+ * int udev_enumerate_add_match_subsystem(struct udev_enumerate *udev_enumerate, const char *subsystem)
+ * }
+ */
+ public static MemorySegment udev_enumerate_add_match_subsystem$address() {
+ return udev_enumerate_add_match_subsystem.ADDR;
+ }
+
+ /**
+ * {@snippet lang=c :
+ * int udev_enumerate_add_match_subsystem(struct udev_enumerate *udev_enumerate, const char *subsystem)
+ * }
+ */
+ public static int udev_enumerate_add_match_subsystem(MemorySegment udev_enumerate, MemorySegment subsystem) {
+ var mh$ = udev_enumerate_add_match_subsystem.HANDLE;
try {
+ if (TRACE_DOWNCALLS) {
+ traceDowncall("udev_enumerate_add_match_subsystem", udev_enumerate, subsystem);
+ }
return (int)mh$.invokeExact(udev_enumerate, subsystem);
+ } catch (Error | RuntimeException ex) {
+ throw ex;
} catch (Throwable ex$) {
- throw new AssertionError("should not reach here", ex$);
+ throw new AssertionError("should not reach here", ex$);
}
}
- public static MethodHandle udev_enumerate_scan_devices$MH() {
- return RuntimeHelper.requireNonNull(constants$2.udev_enumerate_scan_devices$MH,"udev_enumerate_scan_devices");
+
+ private static class udev_enumerate_scan_devices {
+ public static final FunctionDescriptor DESC = FunctionDescriptor.of(
+ udev.C_INT,
+ udev.C_POINTER
+ );
+
+ public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("udev_enumerate_scan_devices");
+
+ public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC);
+ }
+
+ /**
+ * Function descriptor for:
+ * {@snippet lang=c :
+ * int udev_enumerate_scan_devices(struct udev_enumerate *udev_enumerate)
+ * }
+ */
+ public static FunctionDescriptor udev_enumerate_scan_devices$descriptor() {
+ return udev_enumerate_scan_devices.DESC;
+ }
+
+ /**
+ * Downcall method handle for:
+ * {@snippet lang=c :
+ * int udev_enumerate_scan_devices(struct udev_enumerate *udev_enumerate)
+ * }
+ */
+ public static MethodHandle udev_enumerate_scan_devices$handle() {
+ return udev_enumerate_scan_devices.HANDLE;
+ }
+
+ /**
+ * Address for:
+ * {@snippet lang=c :
+ * int udev_enumerate_scan_devices(struct udev_enumerate *udev_enumerate)
+ * }
+ */
+ public static MemorySegment udev_enumerate_scan_devices$address() {
+ return udev_enumerate_scan_devices.ADDR;
}
- public static int udev_enumerate_scan_devices ( Addressable udev_enumerate) {
- var mh$ = udev_enumerate_scan_devices$MH();
+
+ /**
+ * {@snippet lang=c :
+ * int udev_enumerate_scan_devices(struct udev_enumerate *udev_enumerate)
+ * }
+ */
+ public static int udev_enumerate_scan_devices(MemorySegment udev_enumerate) {
+ var mh$ = udev_enumerate_scan_devices.HANDLE;
try {
+ if (TRACE_DOWNCALLS) {
+ traceDowncall("udev_enumerate_scan_devices", udev_enumerate);
+ }
return (int)mh$.invokeExact(udev_enumerate);
+ } catch (Error | RuntimeException ex) {
+ throw ex;
} catch (Throwable ex$) {
- throw new AssertionError("should not reach here", ex$);
+ throw new AssertionError("should not reach here", ex$);
}
}
- public static MethodHandle udev_enumerate_get_list_entry$MH() {
- return RuntimeHelper.requireNonNull(constants$3.udev_enumerate_get_list_entry$MH,"udev_enumerate_get_list_entry");
+
+ private static class udev_enumerate_get_list_entry {
+ public static final FunctionDescriptor DESC = FunctionDescriptor.of(
+ udev.C_POINTER,
+ udev.C_POINTER
+ );
+
+ public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("udev_enumerate_get_list_entry");
+
+ public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC);
+ }
+
+ /**
+ * Function descriptor for:
+ * {@snippet lang=c :
+ * struct udev_list_entry *udev_enumerate_get_list_entry(struct udev_enumerate *udev_enumerate)
+ * }
+ */
+ public static FunctionDescriptor udev_enumerate_get_list_entry$descriptor() {
+ return udev_enumerate_get_list_entry.DESC;
+ }
+
+ /**
+ * Downcall method handle for:
+ * {@snippet lang=c :
+ * struct udev_list_entry *udev_enumerate_get_list_entry(struct udev_enumerate *udev_enumerate)
+ * }
+ */
+ public static MethodHandle udev_enumerate_get_list_entry$handle() {
+ return udev_enumerate_get_list_entry.HANDLE;
+ }
+
+ /**
+ * Address for:
+ * {@snippet lang=c :
+ * struct udev_list_entry *udev_enumerate_get_list_entry(struct udev_enumerate *udev_enumerate)
+ * }
+ */
+ public static MemorySegment udev_enumerate_get_list_entry$address() {
+ return udev_enumerate_get_list_entry.ADDR;
}
- public static MemoryAddress udev_enumerate_get_list_entry ( Addressable udev_enumerate) {
- var mh$ = udev_enumerate_get_list_entry$MH();
+
+ /**
+ * {@snippet lang=c :
+ * struct udev_list_entry *udev_enumerate_get_list_entry(struct udev_enumerate *udev_enumerate)
+ * }
+ */
+ public static MemorySegment udev_enumerate_get_list_entry(MemorySegment udev_enumerate) {
+ var mh$ = udev_enumerate_get_list_entry.HANDLE;
try {
- return (java.lang.foreign.MemoryAddress)mh$.invokeExact(udev_enumerate);
+ if (TRACE_DOWNCALLS) {
+ traceDowncall("udev_enumerate_get_list_entry", udev_enumerate);
+ }
+ return (MemorySegment)mh$.invokeExact(udev_enumerate);
+ } catch (Error | RuntimeException ex) {
+ throw ex;
} catch (Throwable ex$) {
- throw new AssertionError("should not reach here", ex$);
+ throw new AssertionError("should not reach here", ex$);
}
}
}
-
diff --git a/java-does-usb/src/main/java/net/codecrete/usb/linux/gen/unistd/Constants$root.java b/java-does-usb/src/main/java/net/codecrete/usb/linux/gen/unistd/Constants$root.java
deleted file mode 100644
index 1fe4e3c0..00000000
--- a/java-does-usb/src/main/java/net/codecrete/usb/linux/gen/unistd/Constants$root.java
+++ /dev/null
@@ -1,19 +0,0 @@
-// Generated by jextract
-
-package net.codecrete.usb.linux.gen.unistd;
-
-import static java.lang.foreign.ValueLayout.*;
-public class Constants$root {
-
- static final OfBoolean C_BOOL$LAYOUT = JAVA_BOOLEAN;
- static final OfByte C_CHAR$LAYOUT = JAVA_BYTE;
- static final OfShort C_SHORT$LAYOUT = JAVA_SHORT.withBitAlignment(16);
- static final OfInt C_INT$LAYOUT = JAVA_INT.withBitAlignment(32);
- static final OfLong C_LONG$LAYOUT = JAVA_LONG.withBitAlignment(64);
- static final OfLong C_LONG_LONG$LAYOUT = JAVA_LONG.withBitAlignment(64);
- static final OfFloat C_FLOAT$LAYOUT = JAVA_FLOAT.withBitAlignment(32);
- static final OfDouble C_DOUBLE$LAYOUT = JAVA_DOUBLE.withBitAlignment(64);
- static final OfAddress C_POINTER$LAYOUT = ADDRESS.withBitAlignment(64);
-}
-
-
diff --git a/java-does-usb/src/main/java/net/codecrete/usb/linux/gen/unistd/RuntimeHelper.java b/java-does-usb/src/main/java/net/codecrete/usb/linux/gen/unistd/RuntimeHelper.java
deleted file mode 100644
index b771271b..00000000
--- a/java-does-usb/src/main/java/net/codecrete/usb/linux/gen/unistd/RuntimeHelper.java
+++ /dev/null
@@ -1,216 +0,0 @@
-package net.codecrete.usb.linux.gen.unistd;
-// Generated by jextract
-
-import java.lang.foreign.*;
-import java.lang.invoke.MethodHandle;
-import java.lang.invoke.MethodHandles;
-import java.lang.invoke.MethodType;
-
-import static java.lang.foreign.ValueLayout.*;
-
-final class RuntimeHelper {
-
- private RuntimeHelper() {}
- private final static Linker LINKER = Linker.nativeLinker();
- private final static ClassLoader LOADER = RuntimeHelper.class.getClassLoader();
- private final static MethodHandles.Lookup MH_LOOKUP = MethodHandles.lookup();
- private final static SymbolLookup SYMBOL_LOOKUP;
-
- final static SegmentAllocator CONSTANT_ALLOCATOR =
- (size, align) -> MemorySegment.allocateNative(size, align, MemorySession.openImplicit());
-
- static {
-
- SymbolLookup loaderLookup = SymbolLookup.loaderLookup();
- SYMBOL_LOOKUP = name -> loaderLookup.lookup(name).or(() -> LINKER.defaultLookup().lookup(name));
- }
-
- static T requireNonNull(T obj, String symbolName) {
- if (obj == null) {
- throw new UnsatisfiedLinkError("unresolved symbol: " + symbolName);
- }
- return obj;
- }
-
- private final static SegmentAllocator THROWING_ALLOCATOR = (x, y) -> { throw new AssertionError("should not reach here"); };
-
- static final MemorySegment lookupGlobalVariable(String name, MemoryLayout layout) {
- return SYMBOL_LOOKUP.lookup(name).map(symbol -> MemorySegment.ofAddress(symbol.address(), layout.byteSize(), MemorySession.openShared())).orElse(null);
- }
-
- static final MethodHandle downcallHandle(String name, FunctionDescriptor fdesc) {
- return SYMBOL_LOOKUP.lookup(name).
- map(addr -> LINKER.downcallHandle(addr, fdesc)).
- orElse(null);
- }
-
- static final MethodHandle downcallHandle(FunctionDescriptor fdesc) {
- return LINKER.downcallHandle(fdesc);
- }
-
- static final MethodHandle downcallHandleVariadic(String name, FunctionDescriptor fdesc) {
- return SYMBOL_LOOKUP.lookup(name).
- map(addr -> VarargsInvoker.make(addr, fdesc)).
- orElse(null);
- }
-
- static final MemorySegment upcallStub(Class fi, Z z, FunctionDescriptor fdesc, MemorySession session) {
- try {
- MethodHandle handle = MH_LOOKUP.findVirtual(fi, "apply", Linker.upcallType(fdesc));
- handle = handle.bindTo(z);
- return LINKER.upcallStub(handle, fdesc, session);
- } catch (Throwable ex) {
- throw new AssertionError(ex);
- }
- }
-
- static MemorySegment asArray(MemoryAddress addr, MemoryLayout layout, int numElements, MemorySession session) {
- return MemorySegment.ofAddress(addr, numElements * layout.byteSize(), session);
- }
-
- // Internals only below this point
-
- private static class VarargsInvoker {
- private static final MethodHandle INVOKE_MH;
- private final MemorySegment symbol;
- private final FunctionDescriptor function;
-
- private VarargsInvoker(MemorySegment symbol, FunctionDescriptor function) {
- this.symbol = symbol;
- this.function = function;
- }
-
- static {
- try {
- INVOKE_MH = MethodHandles.lookup().findVirtual(VarargsInvoker.class, "invoke", MethodType.methodType(Object.class, SegmentAllocator.class, Object[].class));
- } catch (ReflectiveOperationException e) {
- throw new RuntimeException(e);
- }
- }
-
- static MethodHandle make(MemorySegment symbol, FunctionDescriptor function) {
- VarargsInvoker invoker = new VarargsInvoker(symbol, function);
- MethodHandle handle = INVOKE_MH.bindTo(invoker).asCollector(Object[].class, function.argumentLayouts().size() + 1);
- MethodType mtype = MethodType.methodType(function.returnLayout().isPresent() ? carrier(function.returnLayout().get(), true) : void.class);
- for (MemoryLayout layout : function.argumentLayouts()) {
- mtype = mtype.appendParameterTypes(carrier(layout, false));
- }
- mtype = mtype.appendParameterTypes(Object[].class);
- if (mtype.returnType().equals(MemorySegment.class)) {
- mtype = mtype.insertParameterTypes(0, SegmentAllocator.class);
- } else {
- handle = MethodHandles.insertArguments(handle, 0, THROWING_ALLOCATOR);
- }
- return handle.asType(mtype);
- }
-
- static Class> carrier(MemoryLayout layout, boolean ret) {
- if (layout instanceof ValueLayout valueLayout) {
- return (ret || valueLayout.carrier() != MemoryAddress.class) ?
- valueLayout.carrier() : Addressable.class;
- } else if (layout instanceof GroupLayout) {
- return MemorySegment.class;
- } else {
- throw new AssertionError("Cannot get here!");
- }
- }
-
- private Object invoke(SegmentAllocator allocator, Object[] args) throws Throwable {
- // one trailing Object[]
- int nNamedArgs = function.argumentLayouts().size();
- assert(args.length == nNamedArgs + 1);
- // The last argument is the array of vararg collector
- Object[] unnamedArgs = (Object[]) args[args.length - 1];
-
- int argsCount = nNamedArgs + unnamedArgs.length;
- Class>[] argTypes = new Class>[argsCount];
- MemoryLayout[] argLayouts = new MemoryLayout[nNamedArgs + unnamedArgs.length];
-
- int pos = 0;
- for (pos = 0; pos < nNamedArgs; pos++) {
- argLayouts[pos] = function.argumentLayouts().get(pos);
- }
-
- assert pos == nNamedArgs;
- for (Object o: unnamedArgs) {
- argLayouts[pos] = variadicLayout(normalize(o.getClass()));
- pos++;
- }
- assert pos == argsCount;
-
- FunctionDescriptor f = (function.returnLayout().isEmpty()) ?
- FunctionDescriptor.ofVoid(argLayouts) :
- FunctionDescriptor.of(function.returnLayout().get(), argLayouts);
- MethodHandle mh = LINKER.downcallHandle(symbol, f);
- if (mh.type().returnType() == MemorySegment.class) {
- mh = mh.bindTo(allocator);
- }
- // flatten argument list so that it can be passed to an asSpreader MH
- Object[] allArgs = new Object[nNamedArgs + unnamedArgs.length];
- System.arraycopy(args, 0, allArgs, 0, nNamedArgs);
- System.arraycopy(unnamedArgs, 0, allArgs, nNamedArgs, unnamedArgs.length);
-
- return mh.asSpreader(Object[].class, argsCount).invoke(allArgs);
- }
-
- private static Class> unboxIfNeeded(Class> clazz) {
- if (clazz == Boolean.class) {
- return boolean.class;
- } else if (clazz == Void.class) {
- return void.class;
- } else if (clazz == Byte.class) {
- return byte.class;
- } else if (clazz == Character.class) {
- return char.class;
- } else if (clazz == Short.class) {
- return short.class;
- } else if (clazz == Integer.class) {
- return int.class;
- } else if (clazz == Long.class) {
- return long.class;
- } else if (clazz == Float.class) {
- return float.class;
- } else if (clazz == Double.class) {
- return double.class;
- } else {
- return clazz;
- }
- }
-
- private Class> promote(Class> c) {
- if (c == byte.class || c == char.class || c == short.class || c == int.class) {
- return long.class;
- } else if (c == float.class) {
- return double.class;
- } else {
- return c;
- }
- }
-
- private Class> normalize(Class> c) {
- c = unboxIfNeeded(c);
- if (c.isPrimitive()) {
- return promote(c);
- }
- if (MemoryAddress.class.isAssignableFrom(c)) {
- return MemoryAddress.class;
- }
- if (MemorySegment.class.isAssignableFrom(c)) {
- return MemorySegment.class;
- }
- throw new IllegalArgumentException("Invalid type for ABI: " + c.getTypeName());
- }
-
- private MemoryLayout variadicLayout(Class> c) {
- if (c == long.class) {
- return JAVA_LONG;
- } else if (c == double.class) {
- return JAVA_DOUBLE;
- } else if (MemoryAddress.class.isAssignableFrom(c)) {
- return ADDRESS;
- } else {
- throw new IllegalArgumentException("Unhandled variadic argument class: " + c);
- }
- }
- }
-}
diff --git a/java-does-usb/src/main/java/net/codecrete/usb/linux/gen/unistd/constants$0.java b/java-does-usb/src/main/java/net/codecrete/usb/linux/gen/unistd/constants$0.java
deleted file mode 100644
index 26995ca4..00000000
--- a/java-does-usb/src/main/java/net/codecrete/usb/linux/gen/unistd/constants$0.java
+++ /dev/null
@@ -1,18 +0,0 @@
-// Generated by jextract
-
-package net.codecrete.usb.linux.gen.unistd;
-
-import java.lang.foreign.FunctionDescriptor;
-import java.lang.invoke.MethodHandle;
-class constants$0 {
-
- static final FunctionDescriptor close$FUNC = FunctionDescriptor.of(Constants$root.C_INT$LAYOUT,
- Constants$root.C_INT$LAYOUT
- );
- static final MethodHandle close$MH = RuntimeHelper.downcallHandle(
- "close",
- constants$0.close$FUNC
- );
-}
-
-
diff --git a/java-does-usb/src/main/java/net/codecrete/usb/linux/gen/unistd/unistd$shared.java b/java-does-usb/src/main/java/net/codecrete/usb/linux/gen/unistd/unistd$shared.java
new file mode 100644
index 00000000..11064c54
--- /dev/null
+++ b/java-does-usb/src/main/java/net/codecrete/usb/linux/gen/unistd/unistd$shared.java
@@ -0,0 +1,63 @@
+// Generated by jextract
+
+package net.codecrete.usb.linux.gen.unistd;
+
+import java.lang.invoke.*;
+import java.lang.foreign.*;
+import java.nio.ByteOrder;
+import java.util.*;
+import java.util.function.*;
+import java.util.stream.*;
+
+import static java.lang.foreign.ValueLayout.*;
+import static java.lang.foreign.MemoryLayout.PathElement.*;
+
+public class unistd$shared {
+
+ unistd$shared() {
+ // Should not be called directly
+ }
+
+ public static final ValueLayout.OfBoolean C_BOOL = (ValueLayout.OfBoolean) Linker.nativeLinker().canonicalLayouts().get("bool");
+ public static final ValueLayout.OfByte C_CHAR =(ValueLayout.OfByte)Linker.nativeLinker().canonicalLayouts().get("char");
+ public static final ValueLayout.OfShort C_SHORT = (ValueLayout.OfShort) Linker.nativeLinker().canonicalLayouts().get("short");
+ public static final ValueLayout.OfInt C_INT = (ValueLayout.OfInt) Linker.nativeLinker().canonicalLayouts().get("int");
+ public static final ValueLayout.OfLong C_LONG_LONG = (ValueLayout.OfLong) Linker.nativeLinker().canonicalLayouts().get("long long");
+ public static final ValueLayout.OfFloat C_FLOAT = (ValueLayout.OfFloat) Linker.nativeLinker().canonicalLayouts().get("float");
+ public static final ValueLayout.OfDouble C_DOUBLE = (ValueLayout.OfDouble) Linker.nativeLinker().canonicalLayouts().get("double");
+ public static final AddressLayout C_POINTER = ((AddressLayout) Linker.nativeLinker().canonicalLayouts().get("void*"))
+ .withTargetLayout(MemoryLayout.sequenceLayout(java.lang.Long.MAX_VALUE, C_CHAR));
+ public static final ValueLayout.OfLong C_LONG = (ValueLayout.OfLong) Linker.nativeLinker().canonicalLayouts().get("long");
+
+ static final boolean TRACE_DOWNCALLS = Boolean.getBoolean("jextract.trace.downcalls");
+
+ static void traceDowncall(String name, Object... args) {
+ String traceArgs = Arrays.stream(args)
+ .map(Object::toString)
+ .collect(Collectors.joining(", "));
+ System.out.printf("%s(%s)\n", name, traceArgs);
+ }
+
+ static MethodHandle upcallHandle(Class> fi, String name, FunctionDescriptor fdesc) {
+ try {
+ return MethodHandles.lookup().findVirtual(fi, name, fdesc.toMethodType());
+ } catch (ReflectiveOperationException ex) {
+ throw new AssertionError(ex);
+ }
+ }
+
+ static MemoryLayout align(MemoryLayout layout, long align) {
+ return switch (layout) {
+ case PaddingLayout p -> p;
+ case ValueLayout v -> v.withByteAlignment(align);
+ case GroupLayout g -> {
+ MemoryLayout[] alignedMembers = g.memberLayouts().stream()
+ .map(m -> align(m, align)).toArray(MemoryLayout[]::new);
+ yield g instanceof StructLayout ?
+ MemoryLayout.structLayout(alignedMembers) : MemoryLayout.unionLayout(alignedMembers);
+ }
+ case SequenceLayout s -> MemoryLayout.sequenceLayout(s.elementCount(), align(s.elementLayout(), align));
+ };
+ }
+}
+
diff --git a/java-does-usb/src/main/java/net/codecrete/usb/linux/gen/unistd/unistd.java b/java-does-usb/src/main/java/net/codecrete/usb/linux/gen/unistd/unistd.java
index 74045e41..e0f89fcf 100644
--- a/java-does-usb/src/main/java/net/codecrete/usb/linux/gen/unistd/unistd.java
+++ b/java-does-usb/src/main/java/net/codecrete/usb/linux/gen/unistd/unistd.java
@@ -2,31 +2,86 @@
package net.codecrete.usb.linux.gen.unistd;
-import java.lang.invoke.MethodHandle;
+import java.lang.invoke.*;
+import java.lang.foreign.*;
+import java.nio.ByteOrder;
+import java.util.*;
+import java.util.function.*;
+import java.util.stream.*;
import static java.lang.foreign.ValueLayout.*;
-public class unistd {
-
- /* package-private */ unistd() {}
- public static OfByte C_CHAR = Constants$root.C_CHAR$LAYOUT;
- public static OfShort C_SHORT = Constants$root.C_SHORT$LAYOUT;
- public static OfInt C_INT = Constants$root.C_INT$LAYOUT;
- public static OfLong C_LONG = Constants$root.C_LONG_LONG$LAYOUT;
- public static OfLong C_LONG_LONG = Constants$root.C_LONG_LONG$LAYOUT;
- public static OfFloat C_FLOAT = Constants$root.C_FLOAT$LAYOUT;
- public static OfDouble C_DOUBLE = Constants$root.C_DOUBLE$LAYOUT;
- public static OfAddress C_POINTER = Constants$root.C_POINTER$LAYOUT;
- public static MethodHandle close$MH() {
- return RuntimeHelper.requireNonNull(constants$0.close$MH,"close");
+import static java.lang.foreign.MemoryLayout.PathElement.*;
+
+public class unistd extends unistd$shared {
+
+ unistd() {
+ // Should not be called directly
+ }
+
+ static final Arena LIBRARY_ARENA = Arena.ofAuto();
+
+ static final SymbolLookup SYMBOL_LOOKUP = SymbolLookup.loaderLookup()
+ .or(Linker.nativeLinker().defaultLookup());
+
+
+ private static class close {
+ public static final FunctionDescriptor DESC = FunctionDescriptor.of(
+ unistd.C_INT,
+ unistd.C_INT
+ );
+
+ public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("close");
+
+ public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC);
}
- public static int close ( int __fd) {
- var mh$ = close$MH();
+
+ /**
+ * Function descriptor for:
+ * {@snippet lang=c :
+ * extern int close(int __fd)
+ * }
+ */
+ public static FunctionDescriptor close$descriptor() {
+ return close.DESC;
+ }
+
+ /**
+ * Downcall method handle for:
+ * {@snippet lang=c :
+ * extern int close(int __fd)
+ * }
+ */
+ public static MethodHandle close$handle() {
+ return close.HANDLE;
+ }
+
+ /**
+ * Address for:
+ * {@snippet lang=c :
+ * extern int close(int __fd)
+ * }
+ */
+ public static MemorySegment close$address() {
+ return close.ADDR;
+ }
+
+ /**
+ * {@snippet lang=c :
+ * extern int close(int __fd)
+ * }
+ */
+ public static int close(int __fd) {
+ var mh$ = close.HANDLE;
try {
+ if (TRACE_DOWNCALLS) {
+ traceDowncall("close", __fd);
+ }
return (int)mh$.invokeExact(__fd);
+ } catch (Error | RuntimeException ex) {
+ throw ex;
} catch (Throwable ex$) {
- throw new AssertionError("should not reach here", ex$);
+ throw new AssertionError("should not reach here", ex$);
}
}
}
-
diff --git a/java-does-usb/src/main/java/net/codecrete/usb/linux/gen/usbdevice_fs/Constants$root.java b/java-does-usb/src/main/java/net/codecrete/usb/linux/gen/usbdevice_fs/Constants$root.java
deleted file mode 100644
index e013f088..00000000
--- a/java-does-usb/src/main/java/net/codecrete/usb/linux/gen/usbdevice_fs/Constants$root.java
+++ /dev/null
@@ -1,19 +0,0 @@
-// Generated by jextract
-
-package net.codecrete.usb.linux.gen.usbdevice_fs;
-
-import static java.lang.foreign.ValueLayout.*;
-public class Constants$root {
-
- static final OfBoolean C_BOOL$LAYOUT = JAVA_BOOLEAN;
- static final OfByte C_CHAR$LAYOUT = JAVA_BYTE;
- static final OfShort C_SHORT$LAYOUT = JAVA_SHORT.withBitAlignment(16);
- static final OfInt C_INT$LAYOUT = JAVA_INT.withBitAlignment(32);
- static final OfLong C_LONG$LAYOUT = JAVA_LONG.withBitAlignment(64);
- static final OfLong C_LONG_LONG$LAYOUT = JAVA_LONG.withBitAlignment(64);
- static final OfFloat C_FLOAT$LAYOUT = JAVA_FLOAT.withBitAlignment(32);
- static final OfDouble C_DOUBLE$LAYOUT = JAVA_DOUBLE.withBitAlignment(64);
- static final OfAddress C_POINTER$LAYOUT = ADDRESS.withBitAlignment(64);
-}
-
-
diff --git a/java-does-usb/src/main/java/net/codecrete/usb/linux/gen/usbdevice_fs/RuntimeHelper.java b/java-does-usb/src/main/java/net/codecrete/usb/linux/gen/usbdevice_fs/RuntimeHelper.java
deleted file mode 100644
index 8d30b2af..00000000
--- a/java-does-usb/src/main/java/net/codecrete/usb/linux/gen/usbdevice_fs/RuntimeHelper.java
+++ /dev/null
@@ -1,216 +0,0 @@
-package net.codecrete.usb.linux.gen.usbdevice_fs;
-// Generated by jextract
-
-import java.lang.foreign.*;
-import java.lang.invoke.MethodHandle;
-import java.lang.invoke.MethodHandles;
-import java.lang.invoke.MethodType;
-
-import static java.lang.foreign.ValueLayout.*;
-
-final class RuntimeHelper {
-
- private RuntimeHelper() {}
- private final static Linker LINKER = Linker.nativeLinker();
- private final static ClassLoader LOADER = RuntimeHelper.class.getClassLoader();
- private final static MethodHandles.Lookup MH_LOOKUP = MethodHandles.lookup();
- private final static SymbolLookup SYMBOL_LOOKUP;
-
- final static SegmentAllocator CONSTANT_ALLOCATOR =
- (size, align) -> MemorySegment.allocateNative(size, align, MemorySession.openImplicit());
-
- static {
-
- SymbolLookup loaderLookup = SymbolLookup.loaderLookup();
- SYMBOL_LOOKUP = name -> loaderLookup.lookup(name).or(() -> LINKER.defaultLookup().lookup(name));
- }
-
- static T requireNonNull(T obj, String symbolName) {
- if (obj == null) {
- throw new UnsatisfiedLinkError("unresolved symbol: " + symbolName);
- }
- return obj;
- }
-
- private final static SegmentAllocator THROWING_ALLOCATOR = (x, y) -> { throw new AssertionError("should not reach here"); };
-
- static final MemorySegment lookupGlobalVariable(String name, MemoryLayout layout) {
- return SYMBOL_LOOKUP.lookup(name).map(symbol -> MemorySegment.ofAddress(symbol.address(), layout.byteSize(), MemorySession.openShared())).orElse(null);
- }
-
- static final MethodHandle downcallHandle(String name, FunctionDescriptor fdesc) {
- return SYMBOL_LOOKUP.lookup(name).
- map(addr -> LINKER.downcallHandle(addr, fdesc)).
- orElse(null);
- }
-
- static final MethodHandle downcallHandle(FunctionDescriptor fdesc) {
- return LINKER.downcallHandle(fdesc);
- }
-
- static final MethodHandle downcallHandleVariadic(String name, FunctionDescriptor fdesc) {
- return SYMBOL_LOOKUP.lookup(name).
- map(addr -> VarargsInvoker.make(addr, fdesc)).
- orElse(null);
- }
-
- static final MemorySegment upcallStub(Class fi, Z z, FunctionDescriptor fdesc, MemorySession session) {
- try {
- MethodHandle handle = MH_LOOKUP.findVirtual(fi, "apply", Linker.upcallType(fdesc));
- handle = handle.bindTo(z);
- return LINKER.upcallStub(handle, fdesc, session);
- } catch (Throwable ex) {
- throw new AssertionError(ex);
- }
- }
-
- static MemorySegment asArray(MemoryAddress addr, MemoryLayout layout, int numElements, MemorySession session) {
- return MemorySegment.ofAddress(addr, numElements * layout.byteSize(), session);
- }
-
- // Internals only below this point
-
- private static class VarargsInvoker {
- private static final MethodHandle INVOKE_MH;
- private final MemorySegment symbol;
- private final FunctionDescriptor function;
-
- private VarargsInvoker(MemorySegment symbol, FunctionDescriptor function) {
- this.symbol = symbol;
- this.function = function;
- }
-
- static {
- try {
- INVOKE_MH = MethodHandles.lookup().findVirtual(VarargsInvoker.class, "invoke", MethodType.methodType(Object.class, SegmentAllocator.class, Object[].class));
- } catch (ReflectiveOperationException e) {
- throw new RuntimeException(e);
- }
- }
-
- static MethodHandle make(MemorySegment symbol, FunctionDescriptor function) {
- VarargsInvoker invoker = new VarargsInvoker(symbol, function);
- MethodHandle handle = INVOKE_MH.bindTo(invoker).asCollector(Object[].class, function.argumentLayouts().size() + 1);
- MethodType mtype = MethodType.methodType(function.returnLayout().isPresent() ? carrier(function.returnLayout().get(), true) : void.class);
- for (MemoryLayout layout : function.argumentLayouts()) {
- mtype = mtype.appendParameterTypes(carrier(layout, false));
- }
- mtype = mtype.appendParameterTypes(Object[].class);
- if (mtype.returnType().equals(MemorySegment.class)) {
- mtype = mtype.insertParameterTypes(0, SegmentAllocator.class);
- } else {
- handle = MethodHandles.insertArguments(handle, 0, THROWING_ALLOCATOR);
- }
- return handle.asType(mtype);
- }
-
- static Class> carrier(MemoryLayout layout, boolean ret) {
- if (layout instanceof ValueLayout valueLayout) {
- return (ret || valueLayout.carrier() != MemoryAddress.class) ?
- valueLayout.carrier() : Addressable.class;
- } else if (layout instanceof GroupLayout) {
- return MemorySegment.class;
- } else {
- throw new AssertionError("Cannot get here!");
- }
- }
-
- private Object invoke(SegmentAllocator allocator, Object[] args) throws Throwable {
- // one trailing Object[]
- int nNamedArgs = function.argumentLayouts().size();
- assert(args.length == nNamedArgs + 1);
- // The last argument is the array of vararg collector
- Object[] unnamedArgs = (Object[]) args[args.length - 1];
-
- int argsCount = nNamedArgs + unnamedArgs.length;
- Class>[] argTypes = new Class>[argsCount];
- MemoryLayout[] argLayouts = new MemoryLayout[nNamedArgs + unnamedArgs.length];
-
- int pos = 0;
- for (pos = 0; pos < nNamedArgs; pos++) {
- argLayouts[pos] = function.argumentLayouts().get(pos);
- }
-
- assert pos == nNamedArgs;
- for (Object o: unnamedArgs) {
- argLayouts[pos] = variadicLayout(normalize(o.getClass()));
- pos++;
- }
- assert pos == argsCount;
-
- FunctionDescriptor f = (function.returnLayout().isEmpty()) ?
- FunctionDescriptor.ofVoid(argLayouts) :
- FunctionDescriptor.of(function.returnLayout().get(), argLayouts);
- MethodHandle mh = LINKER.downcallHandle(symbol, f);
- if (mh.type().returnType() == MemorySegment.class) {
- mh = mh.bindTo(allocator);
- }
- // flatten argument list so that it can be passed to an asSpreader MH
- Object[] allArgs = new Object[nNamedArgs + unnamedArgs.length];
- System.arraycopy(args, 0, allArgs, 0, nNamedArgs);
- System.arraycopy(unnamedArgs, 0, allArgs, nNamedArgs, unnamedArgs.length);
-
- return mh.asSpreader(Object[].class, argsCount).invoke(allArgs);
- }
-
- private static Class> unboxIfNeeded(Class> clazz) {
- if (clazz == Boolean.class) {
- return boolean.class;
- } else if (clazz == Void.class) {
- return void.class;
- } else if (clazz == Byte.class) {
- return byte.class;
- } else if (clazz == Character.class) {
- return char.class;
- } else if (clazz == Short.class) {
- return short.class;
- } else if (clazz == Integer.class) {
- return int.class;
- } else if (clazz == Long.class) {
- return long.class;
- } else if (clazz == Float.class) {
- return float.class;
- } else if (clazz == Double.class) {
- return double.class;
- } else {
- return clazz;
- }
- }
-
- private Class> promote(Class> c) {
- if (c == byte.class || c == char.class || c == short.class || c == int.class) {
- return long.class;
- } else if (c == float.class) {
- return double.class;
- } else {
- return c;
- }
- }
-
- private Class> normalize(Class> c) {
- c = unboxIfNeeded(c);
- if (c.isPrimitive()) {
- return promote(c);
- }
- if (MemoryAddress.class.isAssignableFrom(c)) {
- return MemoryAddress.class;
- }
- if (MemorySegment.class.isAssignableFrom(c)) {
- return MemorySegment.class;
- }
- throw new IllegalArgumentException("Invalid type for ABI: " + c.getTypeName());
- }
-
- private MemoryLayout variadicLayout(Class> c) {
- if (c == long.class) {
- return JAVA_LONG;
- } else if (c == double.class) {
- return JAVA_DOUBLE;
- } else if (MemoryAddress.class.isAssignableFrom(c)) {
- return ADDRESS;
- } else {
- throw new IllegalArgumentException("Unhandled variadic argument class: " + c);
- }
- }
- }
-}
diff --git a/java-does-usb/src/main/java/net/codecrete/usb/linux/gen/usbdevice_fs/usbdevfs_bulktransfer.java b/java-does-usb/src/main/java/net/codecrete/usb/linux/gen/usbdevice_fs/usbdevfs_bulktransfer.java
index 670dc414..5e4eba47 100644
--- a/java-does-usb/src/main/java/net/codecrete/usb/linux/gen/usbdevice_fs/usbdevfs_bulktransfer.java
+++ b/java-does-usb/src/main/java/net/codecrete/usb/linux/gen/usbdevice_fs/usbdevfs_bulktransfer.java
@@ -2,90 +2,265 @@
package net.codecrete.usb.linux.gen.usbdevice_fs;
+import java.lang.invoke.*;
import java.lang.foreign.*;
-import java.lang.invoke.VarHandle;
+import java.nio.ByteOrder;
+import java.util.*;
+import java.util.function.*;
+import java.util.stream.*;
+
+import static java.lang.foreign.ValueLayout.*;
+import static java.lang.foreign.MemoryLayout.PathElement.*;
+
+/**
+ * {@snippet lang=c :
+ * struct usbdevfs_bulktransfer {
+ * unsigned int ep;
+ * unsigned int len;
+ * unsigned int timeout;
+ * void *data;
+ * }
+ * }
+ */
public class usbdevfs_bulktransfer {
- static final GroupLayout $struct$LAYOUT = MemoryLayout.structLayout(
- Constants$root.C_INT$LAYOUT.withName("ep"),
- Constants$root.C_INT$LAYOUT.withName("len"),
- Constants$root.C_INT$LAYOUT.withName("timeout"),
- MemoryLayout.paddingLayout(32),
- Constants$root.C_POINTER$LAYOUT.withName("data")
+ usbdevfs_bulktransfer() {
+ // Should not be called directly
+ }
+
+ private static final GroupLayout $LAYOUT = MemoryLayout.structLayout(
+ usbdevice_fs.C_INT.withName("ep"),
+ usbdevice_fs.C_INT.withName("len"),
+ usbdevice_fs.C_INT.withName("timeout"),
+ MemoryLayout.paddingLayout(4),
+ usbdevice_fs.C_POINTER.withName("data")
).withName("usbdevfs_bulktransfer");
- public static MemoryLayout $LAYOUT() {
- return usbdevfs_bulktransfer.$struct$LAYOUT;
+
+ /**
+ * The layout of this struct
+ */
+ public static final GroupLayout layout() {
+ return $LAYOUT;
}
- static final VarHandle ep$VH = $struct$LAYOUT.varHandle(MemoryLayout.PathElement.groupElement("ep"));
- public static VarHandle ep$VH() {
- return usbdevfs_bulktransfer.ep$VH;
+
+ private static final OfInt ep$LAYOUT = (OfInt)$LAYOUT.select(groupElement("ep"));
+
+ /**
+ * Layout for field:
+ * {@snippet lang=c :
+ * unsigned int ep
+ * }
+ */
+ public static final OfInt ep$layout() {
+ return ep$LAYOUT;
}
- public static int ep$get(MemorySegment seg) {
- return (int)usbdevfs_bulktransfer.ep$VH.get(seg);
+
+ private static final long ep$OFFSET = $LAYOUT.byteOffset(groupElement("ep"));
+
+ /**
+ * Offset for field:
+ * {@snippet lang=c :
+ * unsigned int ep
+ * }
+ */
+ public static final long ep$offset() {
+ return ep$OFFSET;
}
- public static void ep$set( MemorySegment seg, int x) {
- usbdevfs_bulktransfer.ep$VH.set(seg, x);
+
+ /**
+ * Getter for field:
+ * {@snippet lang=c :
+ * unsigned int ep
+ * }
+ */
+ public static int ep(MemorySegment struct) {
+ return struct.get(ep$LAYOUT, ep$OFFSET);
}
- public static int ep$get(MemorySegment seg, long index) {
- return (int)usbdevfs_bulktransfer.ep$VH.get(seg.asSlice(index*sizeof()));
+
+ /**
+ * Setter for field:
+ * {@snippet lang=c :
+ * unsigned int ep
+ * }
+ */
+ public static void ep(MemorySegment struct, int fieldValue) {
+ struct.set(ep$LAYOUT, ep$OFFSET, fieldValue);
}
- public static void ep$set(MemorySegment seg, long index, int x) {
- usbdevfs_bulktransfer.ep$VH.set(seg.asSlice(index*sizeof()), x);
+
+ private static final OfInt len$LAYOUT = (OfInt)$LAYOUT.select(groupElement("len"));
+
+ /**
+ * Layout for field:
+ * {@snippet lang=c :
+ * unsigned int len
+ * }
+ */
+ public static final OfInt len$layout() {
+ return len$LAYOUT;
}
- static final VarHandle len$VH = $struct$LAYOUT.varHandle(MemoryLayout.PathElement.groupElement("len"));
- public static VarHandle len$VH() {
- return usbdevfs_bulktransfer.len$VH;
+
+ private static final long len$OFFSET = $LAYOUT.byteOffset(groupElement("len"));
+
+ /**
+ * Offset for field:
+ * {@snippet lang=c :
+ * unsigned int len
+ * }
+ */
+ public static final long len$offset() {
+ return len$OFFSET;
}
- public static int len$get(MemorySegment seg) {
- return (int)usbdevfs_bulktransfer.len$VH.get(seg);
+
+ /**
+ * Getter for field:
+ * {@snippet lang=c :
+ * unsigned int len
+ * }
+ */
+ public static int len(MemorySegment struct) {
+ return struct.get(len$LAYOUT, len$OFFSET);
}
- public static void len$set( MemorySegment seg, int x) {
- usbdevfs_bulktransfer.len$VH.set(seg, x);
+
+ /**
+ * Setter for field:
+ * {@snippet lang=c :
+ * unsigned int len
+ * }
+ */
+ public static void len(MemorySegment struct, int fieldValue) {
+ struct.set(len$LAYOUT, len$OFFSET, fieldValue);
}
- public static int len$get(MemorySegment seg, long index) {
- return (int)usbdevfs_bulktransfer.len$VH.get(seg.asSlice(index*sizeof()));
+
+ private static final OfInt timeout$LAYOUT = (OfInt)$LAYOUT.select(groupElement("timeout"));
+
+ /**
+ * Layout for field:
+ * {@snippet lang=c :
+ * unsigned int timeout
+ * }
+ */
+ public static final OfInt timeout$layout() {
+ return timeout$LAYOUT;
}
- public static void len$set(MemorySegment seg, long index, int x) {
- usbdevfs_bulktransfer.len$VH.set(seg.asSlice(index*sizeof()), x);
+
+ private static final long timeout$OFFSET = $LAYOUT.byteOffset(groupElement("timeout"));
+
+ /**
+ * Offset for field:
+ * {@snippet lang=c :
+ * unsigned int timeout
+ * }
+ */
+ public static final long timeout$offset() {
+ return timeout$OFFSET;
}
- static final VarHandle timeout$VH = $struct$LAYOUT.varHandle(MemoryLayout.PathElement.groupElement("timeout"));
- public static VarHandle timeout$VH() {
- return usbdevfs_bulktransfer.timeout$VH;
+
+ /**
+ * Getter for field:
+ * {@snippet lang=c :
+ * unsigned int timeout
+ * }
+ */
+ public static int timeout(MemorySegment struct) {
+ return struct.get(timeout$LAYOUT, timeout$OFFSET);
}
- public static int timeout$get(MemorySegment seg) {
- return (int)usbdevfs_bulktransfer.timeout$VH.get(seg);
+
+ /**
+ * Setter for field:
+ * {@snippet lang=c :
+ * unsigned int timeout
+ * }
+ */
+ public static void timeout(MemorySegment struct, int fieldValue) {
+ struct.set(timeout$LAYOUT, timeout$OFFSET, fieldValue);
}
- public static void timeout$set( MemorySegment seg, int x) {
- usbdevfs_bulktransfer.timeout$VH.set(seg, x);
+
+ private static final AddressLayout data$LAYOUT = (AddressLayout)$LAYOUT.select(groupElement("data"));
+
+ /**
+ * Layout for field:
+ * {@snippet lang=c :
+ * void *data
+ * }
+ */
+ public static final AddressLayout data$layout() {
+ return data$LAYOUT;
}
- public static int timeout$get(MemorySegment seg, long index) {
- return (int)usbdevfs_bulktransfer.timeout$VH.get(seg.asSlice(index*sizeof()));
+
+ private static final long data$OFFSET = $LAYOUT.byteOffset(groupElement("data"));
+
+ /**
+ * Offset for field:
+ * {@snippet lang=c :
+ * void *data
+ * }
+ */
+ public static final long data$offset() {
+ return data$OFFSET;
}
- public static void timeout$set(MemorySegment seg, long index, int x) {
- usbdevfs_bulktransfer.timeout$VH.set(seg.asSlice(index*sizeof()), x);
+
+ /**
+ * Getter for field:
+ * {@snippet lang=c :
+ * void *data
+ * }
+ */
+ public static MemorySegment data(MemorySegment struct) {
+ return struct.get(data$LAYOUT, data$OFFSET);
}
- static final VarHandle data$VH = $struct$LAYOUT.varHandle(MemoryLayout.PathElement.groupElement("data"));
- public static VarHandle data$VH() {
- return usbdevfs_bulktransfer.data$VH;
+
+ /**
+ * Setter for field:
+ * {@snippet lang=c :
+ * void *data
+ * }
+ */
+ public static void data(MemorySegment struct, MemorySegment fieldValue) {
+ struct.set(data$LAYOUT, data$OFFSET, fieldValue);
}
- public static MemoryAddress data$get(MemorySegment seg) {
- return (java.lang.foreign.MemoryAddress)usbdevfs_bulktransfer.data$VH.get(seg);
+
+ /**
+ * Obtains a slice of {@code arrayParam} which selects the array element at {@code index}.
+ * The returned segment has address {@code arrayParam.address() + index * layout().byteSize()}
+ */
+ public static MemorySegment asSlice(MemorySegment array, long index) {
+ return array.asSlice(layout().byteSize() * index);
}
- public static void data$set( MemorySegment seg, MemoryAddress x) {
- usbdevfs_bulktransfer.data$VH.set(seg, x);
+
+ /**
+ * The size (in bytes) of this struct
+ */
+ public static long sizeof() { return layout().byteSize(); }
+
+ /**
+ * Allocate a segment of size {@code layout().byteSize()} using {@code allocator}
+ */
+ public static MemorySegment allocate(SegmentAllocator allocator) {
+ return allocator.allocate(layout());
}
- public static MemoryAddress data$get(MemorySegment seg, long index) {
- return (java.lang.foreign.MemoryAddress)usbdevfs_bulktransfer.data$VH.get(seg.asSlice(index*sizeof()));
+
+ /**
+ * Allocate an array of size {@code elementCount} using {@code allocator}.
+ * The returned segment has size {@code elementCount * layout().byteSize()}.
+ */
+ public static MemorySegment allocateArray(long elementCount, SegmentAllocator allocator) {
+ return allocator.allocate(MemoryLayout.sequenceLayout(elementCount, layout()));
}
- public static void data$set(MemorySegment seg, long index, MemoryAddress x) {
- usbdevfs_bulktransfer.data$VH.set(seg.asSlice(index*sizeof()), x);
+
+ /**
+ * Reinterprets {@code addr} using target {@code arena} and {@code cleanupAction} (if any).
+ * The returned segment has size {@code layout().byteSize()}
+ */
+ public static MemorySegment reinterpret(MemorySegment addr, Arena arena, Consumer cleanup) {
+ return reinterpret(addr, 1, arena, cleanup);
}
- public static long sizeof() { return $LAYOUT().byteSize(); }
- public static MemorySegment allocate(SegmentAllocator allocator) { return allocator.allocate($LAYOUT()); }
- public static MemorySegment allocateArray(int len, SegmentAllocator allocator) {
- return allocator.allocate(MemoryLayout.sequenceLayout(len, $LAYOUT()));
+
+ /**
+ * Reinterprets {@code addr} using target {@code arena} and {@code cleanupAction} (if any).
+ * The returned segment has size {@code elementCount * layout().byteSize()}
+ */
+ public static MemorySegment reinterpret(MemorySegment addr, long elementCount, Arena arena, Consumer cleanup) {
+ return addr.reinterpret(layout().byteSize() * elementCount, arena, cleanup);
}
- public static MemorySegment ofAddress(MemoryAddress addr, MemorySession session) { return RuntimeHelper.asArray(addr, $LAYOUT(), 1, session); }
}
-
diff --git a/java-does-usb/src/main/java/net/codecrete/usb/linux/gen/usbdevice_fs/usbdevfs_ctrltransfer.java b/java-does-usb/src/main/java/net/codecrete/usb/linux/gen/usbdevice_fs/usbdevfs_ctrltransfer.java
index 2dd9c901..c5071566 100644
--- a/java-does-usb/src/main/java/net/codecrete/usb/linux/gen/usbdevice_fs/usbdevfs_ctrltransfer.java
+++ b/java-does-usb/src/main/java/net/codecrete/usb/linux/gen/usbdevice_fs/usbdevfs_ctrltransfer.java
@@ -2,141 +2,403 @@
package net.codecrete.usb.linux.gen.usbdevice_fs;
+import java.lang.invoke.*;
import java.lang.foreign.*;
-import java.lang.invoke.VarHandle;
+import java.nio.ByteOrder;
+import java.util.*;
+import java.util.function.*;
+import java.util.stream.*;
+
+import static java.lang.foreign.ValueLayout.*;
+import static java.lang.foreign.MemoryLayout.PathElement.*;
+
+/**
+ * {@snippet lang=c :
+ * struct usbdevfs_ctrltransfer {
+ * __u8 bRequestType;
+ * __u8 bRequest;
+ * __u16 wValue;
+ * __u16 wIndex;
+ * __u16 wLength;
+ * __u32 timeout;
+ * void *data;
+ * }
+ * }
+ */
public class usbdevfs_ctrltransfer {
- static final GroupLayout $struct$LAYOUT = MemoryLayout.structLayout(
- Constants$root.C_CHAR$LAYOUT.withName("bRequestType"),
- Constants$root.C_CHAR$LAYOUT.withName("bRequest"),
- Constants$root.C_SHORT$LAYOUT.withName("wValue"),
- Constants$root.C_SHORT$LAYOUT.withName("wIndex"),
- Constants$root.C_SHORT$LAYOUT.withName("wLength"),
- Constants$root.C_INT$LAYOUT.withName("timeout"),
- MemoryLayout.paddingLayout(32),
- Constants$root.C_POINTER$LAYOUT.withName("data")
- ).withName("usbdevfs_ctrltransfer");
- public static MemoryLayout $LAYOUT() {
- return usbdevfs_ctrltransfer.$struct$LAYOUT;
- }
- static final VarHandle bRequestType$VH = $struct$LAYOUT.varHandle(MemoryLayout.PathElement.groupElement("bRequestType"));
- public static VarHandle bRequestType$VH() {
- return usbdevfs_ctrltransfer.bRequestType$VH;
+ usbdevfs_ctrltransfer() {
+ // Should not be called directly
}
- public static byte bRequestType$get(MemorySegment seg) {
- return (byte)usbdevfs_ctrltransfer.bRequestType$VH.get(seg);
- }
- public static void bRequestType$set( MemorySegment seg, byte x) {
- usbdevfs_ctrltransfer.bRequestType$VH.set(seg, x);
+
+ private static final GroupLayout $LAYOUT = MemoryLayout.structLayout(
+ usbdevice_fs.C_CHAR.withName("bRequestType"),
+ usbdevice_fs.C_CHAR.withName("bRequest"),
+ usbdevice_fs.C_SHORT.withName("wValue"),
+ usbdevice_fs.C_SHORT.withName("wIndex"),
+ usbdevice_fs.C_SHORT.withName("wLength"),
+ usbdevice_fs.C_INT.withName("timeout"),
+ MemoryLayout.paddingLayout(4),
+ usbdevice_fs.C_POINTER.withName("data")
+ ).withName("usbdevfs_ctrltransfer");
+
+ /**
+ * The layout of this struct
+ */
+ public static final GroupLayout layout() {
+ return $LAYOUT;
}
- public static byte bRequestType$get(MemorySegment seg, long index) {
- return (byte)usbdevfs_ctrltransfer.bRequestType$VH.get(seg.asSlice(index*sizeof()));
+
+ private static final OfByte bRequestType$LAYOUT = (OfByte)$LAYOUT.select(groupElement("bRequestType"));
+
+ /**
+ * Layout for field:
+ * {@snippet lang=c :
+ * __u8 bRequestType
+ * }
+ */
+ public static final OfByte bRequestType$layout() {
+ return bRequestType$LAYOUT;
}
- public static void bRequestType$set(MemorySegment seg, long index, byte x) {
- usbdevfs_ctrltransfer.bRequestType$VH.set(seg.asSlice(index*sizeof()), x);
+
+ private static final long bRequestType$OFFSET = $LAYOUT.byteOffset(groupElement("bRequestType"));
+
+ /**
+ * Offset for field:
+ * {@snippet lang=c :
+ * __u8 bRequestType
+ * }
+ */
+ public static final long bRequestType$offset() {
+ return bRequestType$OFFSET;
}
- static final VarHandle bRequest$VH = $struct$LAYOUT.varHandle(MemoryLayout.PathElement.groupElement("bRequest"));
- public static VarHandle bRequest$VH() {
- return usbdevfs_ctrltransfer.bRequest$VH;
+
+ /**
+ * Getter for field:
+ * {@snippet lang=c :
+ * __u8 bRequestType
+ * }
+ */
+ public static byte bRequestType(MemorySegment struct) {
+ return struct.get(bRequestType$LAYOUT, bRequestType$OFFSET);
}
- public static byte bRequest$get(MemorySegment seg) {
- return (byte)usbdevfs_ctrltransfer.bRequest$VH.get(seg);
+
+ /**
+ * Setter for field:
+ * {@snippet lang=c :
+ * __u8 bRequestType
+ * }
+ */
+ public static void bRequestType(MemorySegment struct, byte fieldValue) {
+ struct.set(bRequestType$LAYOUT, bRequestType$OFFSET, fieldValue);
}
- public static void bRequest$set( MemorySegment seg, byte x) {
- usbdevfs_ctrltransfer.bRequest$VH.set(seg, x);
+
+ private static final OfByte bRequest$LAYOUT = (OfByte)$LAYOUT.select(groupElement("bRequest"));
+
+ /**
+ * Layout for field:
+ * {@snippet lang=c :
+ * __u8 bRequest
+ * }
+ */
+ public static final OfByte bRequest$layout() {
+ return bRequest$LAYOUT;
}
- public static byte bRequest$get(MemorySegment seg, long index) {
- return (byte)usbdevfs_ctrltransfer.bRequest$VH.get(seg.asSlice(index*sizeof()));
+
+ private static final long bRequest$OFFSET = $LAYOUT.byteOffset(groupElement("bRequest"));
+
+ /**
+ * Offset for field:
+ * {@snippet lang=c :
+ * __u8 bRequest
+ * }
+ */
+ public static final long bRequest$offset() {
+ return bRequest$OFFSET;
}
- public static void bRequest$set(MemorySegment seg, long index, byte x) {
- usbdevfs_ctrltransfer.bRequest$VH.set(seg.asSlice(index*sizeof()), x);
+
+ /**
+ * Getter for field:
+ * {@snippet lang=c :
+ * __u8 bRequest
+ * }
+ */
+ public static byte bRequest(MemorySegment struct) {
+ return struct.get(bRequest$LAYOUT, bRequest$OFFSET);
}
- static final VarHandle wValue$VH = $struct$LAYOUT.varHandle(MemoryLayout.PathElement.groupElement("wValue"));
- public static VarHandle wValue$VH() {
- return usbdevfs_ctrltransfer.wValue$VH;
+
+ /**
+ * Setter for field:
+ * {@snippet lang=c :
+ * __u8 bRequest
+ * }
+ */
+ public static void bRequest(MemorySegment struct, byte fieldValue) {
+ struct.set(bRequest$LAYOUT, bRequest$OFFSET, fieldValue);
}
- public static short wValue$get(MemorySegment seg) {
- return (short)usbdevfs_ctrltransfer.wValue$VH.get(seg);
+
+ private static final OfShort wValue$LAYOUT = (OfShort)$LAYOUT.select(groupElement("wValue"));
+
+ /**
+ * Layout for field:
+ * {@snippet lang=c :
+ * __u16 wValue
+ * }
+ */
+ public static final OfShort wValue$layout() {
+ return wValue$LAYOUT;
}
- public static void wValue$set( MemorySegment seg, short x) {
- usbdevfs_ctrltransfer.wValue$VH.set(seg, x);
+
+ private static final long wValue$OFFSET = $LAYOUT.byteOffset(groupElement("wValue"));
+
+ /**
+ * Offset for field:
+ * {@snippet lang=c :
+ * __u16 wValue
+ * }
+ */
+ public static final long wValue$offset() {
+ return wValue$OFFSET;
}
- public static short wValue$get(MemorySegment seg, long index) {
- return (short)usbdevfs_ctrltransfer.wValue$VH.get(seg.asSlice(index*sizeof()));
+
+ /**
+ * Getter for field:
+ * {@snippet lang=c :
+ * __u16 wValue
+ * }
+ */
+ public static short wValue(MemorySegment struct) {
+ return struct.get(wValue$LAYOUT, wValue$OFFSET);
}
- public static void wValue$set(MemorySegment seg, long index, short x) {
- usbdevfs_ctrltransfer.wValue$VH.set(seg.asSlice(index*sizeof()), x);
+
+ /**
+ * Setter for field:
+ * {@snippet lang=c :
+ * __u16 wValue
+ * }
+ */
+ public static void wValue(MemorySegment struct, short fieldValue) {
+ struct.set(wValue$LAYOUT, wValue$OFFSET, fieldValue);
}
- static final VarHandle wIndex$VH = $struct$LAYOUT.varHandle(MemoryLayout.PathElement.groupElement("wIndex"));
- public static VarHandle wIndex$VH() {
- return usbdevfs_ctrltransfer.wIndex$VH;
+
+ private static final OfShort wIndex$LAYOUT = (OfShort)$LAYOUT.select(groupElement("wIndex"));
+
+ /**
+ * Layout for field:
+ * {@snippet lang=c :
+ * __u16 wIndex
+ * }
+ */
+ public static final OfShort wIndex$layout() {
+ return wIndex$LAYOUT;
}
- public static short wIndex$get(MemorySegment seg) {
- return (short)usbdevfs_ctrltransfer.wIndex$VH.get(seg);
+
+ private static final long wIndex$OFFSET = $LAYOUT.byteOffset(groupElement("wIndex"));
+
+ /**
+ * Offset for field:
+ * {@snippet lang=c :
+ * __u16 wIndex
+ * }
+ */
+ public static final long wIndex$offset() {
+ return wIndex$OFFSET;
}
- public static void wIndex$set( MemorySegment seg, short x) {
- usbdevfs_ctrltransfer.wIndex$VH.set(seg, x);
+
+ /**
+ * Getter for field:
+ * {@snippet lang=c :
+ * __u16 wIndex
+ * }
+ */
+ public static short wIndex(MemorySegment struct) {
+ return struct.get(wIndex$LAYOUT, wIndex$OFFSET);
}
- public static short wIndex$get(MemorySegment seg, long index) {
- return (short)usbdevfs_ctrltransfer.wIndex$VH.get(seg.asSlice(index*sizeof()));
+
+ /**
+ * Setter for field:
+ * {@snippet lang=c :
+ * __u16 wIndex
+ * }
+ */
+ public static void wIndex(MemorySegment struct, short fieldValue) {
+ struct.set(wIndex$LAYOUT, wIndex$OFFSET, fieldValue);
}
- public static void wIndex$set(MemorySegment seg, long index, short x) {
- usbdevfs_ctrltransfer.wIndex$VH.set(seg.asSlice(index*sizeof()), x);
+
+ private static final OfShort wLength$LAYOUT = (OfShort)$LAYOUT.select(groupElement("wLength"));
+
+ /**
+ * Layout for field:
+ * {@snippet lang=c :
+ * __u16 wLength
+ * }
+ */
+ public static final OfShort wLength$layout() {
+ return wLength$LAYOUT;
}
- static final VarHandle wLength$VH = $struct$LAYOUT.varHandle(MemoryLayout.PathElement.groupElement("wLength"));
- public static VarHandle wLength$VH() {
- return usbdevfs_ctrltransfer.wLength$VH;
+
+ private static final long wLength$OFFSET = $LAYOUT.byteOffset(groupElement("wLength"));
+
+ /**
+ * Offset for field:
+ * {@snippet lang=c :
+ * __u16 wLength
+ * }
+ */
+ public static final long wLength$offset() {
+ return wLength$OFFSET;
}
- public static short wLength$get(MemorySegment seg) {
- return (short)usbdevfs_ctrltransfer.wLength$VH.get(seg);
+
+ /**
+ * Getter for field:
+ * {@snippet lang=c :
+ * __u16 wLength
+ * }
+ */
+ public static short wLength(MemorySegment struct) {
+ return struct.get(wLength$LAYOUT, wLength$OFFSET);
}
- public static void wLength$set( MemorySegment seg, short x) {
- usbdevfs_ctrltransfer.wLength$VH.set(seg, x);
+
+ /**
+ * Setter for field:
+ * {@snippet lang=c :
+ * __u16 wLength
+ * }
+ */
+ public static void wLength(MemorySegment struct, short fieldValue) {
+ struct.set(wLength$LAYOUT, wLength$OFFSET, fieldValue);
}
- public static short wLength$get(MemorySegment seg, long index) {
- return (short)usbdevfs_ctrltransfer.wLength$VH.get(seg.asSlice(index*sizeof()));
+
+ private static final OfInt timeout$LAYOUT = (OfInt)$LAYOUT.select(groupElement("timeout"));
+
+ /**
+ * Layout for field:
+ * {@snippet lang=c :
+ * __u32 timeout
+ * }
+ */
+ public static final OfInt timeout$layout() {
+ return timeout$LAYOUT;
}
- public static void wLength$set(MemorySegment seg, long index, short x) {
- usbdevfs_ctrltransfer.wLength$VH.set(seg.asSlice(index*sizeof()), x);
+
+ private static final long timeout$OFFSET = $LAYOUT.byteOffset(groupElement("timeout"));
+
+ /**
+ * Offset for field:
+ * {@snippet lang=c :
+ * __u32 timeout
+ * }
+ */
+ public static final long timeout$offset() {
+ return timeout$OFFSET;
}
- static final VarHandle timeout$VH = $struct$LAYOUT.varHandle(MemoryLayout.PathElement.groupElement("timeout"));
- public static VarHandle timeout$VH() {
- return usbdevfs_ctrltransfer.timeout$VH;
+
+ /**
+ * Getter for field:
+ * {@snippet lang=c :
+ * __u32 timeout
+ * }
+ */
+ public static int timeout(MemorySegment struct) {
+ return struct.get(timeout$LAYOUT, timeout$OFFSET);
}
- public static int timeout$get(MemorySegment seg) {
- return (int)usbdevfs_ctrltransfer.timeout$VH.get(seg);
+
+ /**
+ * Setter for field:
+ * {@snippet lang=c :
+ * __u32 timeout
+ * }
+ */
+ public static void timeout(MemorySegment struct, int fieldValue) {
+ struct.set(timeout$LAYOUT, timeout$OFFSET, fieldValue);
}
- public static void timeout$set( MemorySegment seg, int x) {
- usbdevfs_ctrltransfer.timeout$VH.set(seg, x);
+
+ private static final AddressLayout data$LAYOUT = (AddressLayout)$LAYOUT.select(groupElement("data"));
+
+ /**
+ * Layout for field:
+ * {@snippet lang=c :
+ * void *data
+ * }
+ */
+ public static final AddressLayout data$layout() {
+ return data$LAYOUT;
}
- public static int timeout$get(MemorySegment seg, long index) {
- return (int)usbdevfs_ctrltransfer.timeout$VH.get(seg.asSlice(index*sizeof()));
+
+ private static final long data$OFFSET = $LAYOUT.byteOffset(groupElement("data"));
+
+ /**
+ * Offset for field:
+ * {@snippet lang=c :
+ * void *data
+ * }
+ */
+ public static final long data$offset() {
+ return data$OFFSET;
}
- public static void timeout$set(MemorySegment seg, long index, int x) {
- usbdevfs_ctrltransfer.timeout$VH.set(seg.asSlice(index*sizeof()), x);
+
+ /**
+ * Getter for field:
+ * {@snippet lang=c :
+ * void *data
+ * }
+ */
+ public static MemorySegment data(MemorySegment struct) {
+ return struct.get(data$LAYOUT, data$OFFSET);
}
- static final VarHandle data$VH = $struct$LAYOUT.varHandle(MemoryLayout.PathElement.groupElement("data"));
- public static VarHandle data$VH() {
- return usbdevfs_ctrltransfer.data$VH;
+
+ /**
+ * Setter for field:
+ * {@snippet lang=c :
+ * void *data
+ * }
+ */
+ public static void data(MemorySegment struct, MemorySegment fieldValue) {
+ struct.set(data$LAYOUT, data$OFFSET, fieldValue);
}
- public static MemoryAddress data$get(MemorySegment seg) {
- return (java.lang.foreign.MemoryAddress)usbdevfs_ctrltransfer.data$VH.get(seg);
+
+ /**
+ * Obtains a slice of {@code arrayParam} which selects the array element at {@code index}.
+ * The returned segment has address {@code arrayParam.address() + index * layout().byteSize()}
+ */
+ public static MemorySegment asSlice(MemorySegment array, long index) {
+ return array.asSlice(layout().byteSize() * index);
}
- public static void data$set( MemorySegment seg, MemoryAddress x) {
- usbdevfs_ctrltransfer.data$VH.set(seg, x);
+
+ /**
+ * The size (in bytes) of this struct
+ */
+ public static long sizeof() { return layout().byteSize(); }
+
+ /**
+ * Allocate a segment of size {@code layout().byteSize()} using {@code allocator}
+ */
+ public static MemorySegment allocate(SegmentAllocator allocator) {
+ return allocator.allocate(layout());
}
- public static MemoryAddress data$get(MemorySegment seg, long index) {
- return (java.lang.foreign.MemoryAddress)usbdevfs_ctrltransfer.data$VH.get(seg.asSlice(index*sizeof()));
+
+ /**
+ * Allocate an array of size {@code elementCount} using {@code allocator}.
+ * The returned segment has size {@code elementCount * layout().byteSize()}.
+ */
+ public static MemorySegment allocateArray(long elementCount, SegmentAllocator allocator) {
+ return allocator.allocate(MemoryLayout.sequenceLayout(elementCount, layout()));
}
- public static void data$set(MemorySegment seg, long index, MemoryAddress x) {
- usbdevfs_ctrltransfer.data$VH.set(seg.asSlice(index*sizeof()), x);
+
+ /**
+ * Reinterprets {@code addr} using target {@code arena} and {@code cleanupAction} (if any).
+ * The returned segment has size {@code layout().byteSize()}
+ */
+ public static MemorySegment reinterpret(MemorySegment addr, Arena arena, Consumer cleanup) {
+ return reinterpret(addr, 1, arena, cleanup);
}
- public static long sizeof() { return $LAYOUT().byteSize(); }
- public static MemorySegment allocate(SegmentAllocator allocator) { return allocator.allocate($LAYOUT()); }
- public static MemorySegment allocateArray(int len, SegmentAllocator allocator) {
- return allocator.allocate(MemoryLayout.sequenceLayout(len, $LAYOUT()));
+
+ /**
+ * Reinterprets {@code addr} using target {@code arena} and {@code cleanupAction} (if any).
+ * The returned segment has size {@code elementCount * layout().byteSize()}
+ */
+ public static MemorySegment reinterpret(MemorySegment addr, long elementCount, Arena arena, Consumer cleanup) {
+ return addr.reinterpret(layout().byteSize() * elementCount, arena, cleanup);
}
- public static MemorySegment ofAddress(MemoryAddress addr, MemorySession session) { return RuntimeHelper.asArray(addr, $LAYOUT(), 1, session); }
}
-
diff --git a/java-does-usb/src/main/java/net/codecrete/usb/linux/gen/usbdevice_fs/usbdevfs_disconnect_claim.java b/java-does-usb/src/main/java/net/codecrete/usb/linux/gen/usbdevice_fs/usbdevfs_disconnect_claim.java
new file mode 100644
index 00000000..a41ba1db
--- /dev/null
+++ b/java-does-usb/src/main/java/net/codecrete/usb/linux/gen/usbdevice_fs/usbdevfs_disconnect_claim.java
@@ -0,0 +1,252 @@
+// Generated by jextract
+
+package net.codecrete.usb.linux.gen.usbdevice_fs;
+
+import java.lang.invoke.*;
+import java.lang.foreign.*;
+import java.nio.ByteOrder;
+import java.util.*;
+import java.util.function.*;
+import java.util.stream.*;
+
+import static java.lang.foreign.ValueLayout.*;
+import static java.lang.foreign.MemoryLayout.PathElement.*;
+
+/**
+ * {@snippet lang=c :
+ * struct usbdevfs_disconnect_claim {
+ * unsigned int interface;
+ * unsigned int flags;
+ * char driver[256];
+ * }
+ * }
+ */
+public class usbdevfs_disconnect_claim {
+
+ usbdevfs_disconnect_claim() {
+ // Should not be called directly
+ }
+
+ private static final GroupLayout $LAYOUT = MemoryLayout.structLayout(
+ usbdevice_fs.C_INT.withName("interface"),
+ usbdevice_fs.C_INT.withName("flags"),
+ MemoryLayout.sequenceLayout(256, usbdevice_fs.C_CHAR).withName("driver")
+ ).withName("usbdevfs_disconnect_claim");
+
+ /**
+ * The layout of this struct
+ */
+ public static final GroupLayout layout() {
+ return $LAYOUT;
+ }
+
+ private static final OfInt interface_$LAYOUT = (OfInt)$LAYOUT.select(groupElement("interface"));
+
+ /**
+ * Layout for field:
+ * {@snippet lang=c :
+ * unsigned int interface
+ * }
+ */
+ public static final OfInt interface_$layout() {
+ return interface_$LAYOUT;
+ }
+
+ private static final long interface_$OFFSET = $LAYOUT.byteOffset(groupElement("interface"));
+
+ /**
+ * Offset for field:
+ * {@snippet lang=c :
+ * unsigned int interface
+ * }
+ */
+ public static final long interface_$offset() {
+ return interface_$OFFSET;
+ }
+
+ /**
+ * Getter for field:
+ * {@snippet lang=c :
+ * unsigned int interface
+ * }
+ */
+ public static int interface_(MemorySegment struct) {
+ return struct.get(interface_$LAYOUT, interface_$OFFSET);
+ }
+
+ /**
+ * Setter for field:
+ * {@snippet lang=c :
+ * unsigned int interface
+ * }
+ */
+ public static void interface_(MemorySegment struct, int fieldValue) {
+ struct.set(interface_$LAYOUT, interface_$OFFSET, fieldValue);
+ }
+
+ private static final OfInt flags$LAYOUT = (OfInt)$LAYOUT.select(groupElement("flags"));
+
+ /**
+ * Layout for field:
+ * {@snippet lang=c :
+ * unsigned int flags
+ * }
+ */
+ public static final OfInt flags$layout() {
+ return flags$LAYOUT;
+ }
+
+ private static final long flags$OFFSET = $LAYOUT.byteOffset(groupElement("flags"));
+
+ /**
+ * Offset for field:
+ * {@snippet lang=c :
+ * unsigned int flags
+ * }
+ */
+ public static final long flags$offset() {
+ return flags$OFFSET;
+ }
+
+ /**
+ * Getter for field:
+ * {@snippet lang=c :
+ * unsigned int flags
+ * }
+ */
+ public static int flags(MemorySegment struct) {
+ return struct.get(flags$LAYOUT, flags$OFFSET);
+ }
+
+ /**
+ * Setter for field:
+ * {@snippet lang=c :
+ * unsigned int flags
+ * }
+ */
+ public static void flags(MemorySegment struct, int fieldValue) {
+ struct.set(flags$LAYOUT, flags$OFFSET, fieldValue);
+ }
+
+ private static final SequenceLayout driver$LAYOUT = (SequenceLayout)$LAYOUT.select(groupElement("driver"));
+
+ /**
+ * Layout for field:
+ * {@snippet lang=c :
+ * char driver[256]
+ * }
+ */
+ public static final SequenceLayout driver$layout() {
+ return driver$LAYOUT;
+ }
+
+ private static final long driver$OFFSET = $LAYOUT.byteOffset(groupElement("driver"));
+
+ /**
+ * Offset for field:
+ * {@snippet lang=c :
+ * char driver[256]
+ * }
+ */
+ public static final long driver$offset() {
+ return driver$OFFSET;
+ }
+
+ /**
+ * Getter for field:
+ * {@snippet lang=c :
+ * char driver[256]
+ * }
+ */
+ public static MemorySegment driver(MemorySegment struct) {
+ return struct.asSlice(driver$OFFSET, driver$LAYOUT.byteSize());
+ }
+
+ /**
+ * Setter for field:
+ * {@snippet lang=c :
+ * char driver[256]
+ * }
+ */
+ public static void driver(MemorySegment struct, MemorySegment fieldValue) {
+ MemorySegment.copy(fieldValue, 0L, struct, driver$OFFSET, driver$LAYOUT.byteSize());
+ }
+
+ private static long[] driver$DIMS = { 256 };
+
+ /**
+ * Dimensions for array field:
+ * {@snippet lang=c :
+ * char driver[256]
+ * }
+ */
+ public static long[] driver$dimensions() {
+ return driver$DIMS;
+ }
+ private static final VarHandle driver$ELEM_HANDLE = driver$LAYOUT.varHandle(sequenceElement());
+
+ /**
+ * Indexed getter for field:
+ * {@snippet lang=c :
+ * char driver[256]
+ * }
+ */
+ public static byte driver(MemorySegment struct, long index0) {
+ return (byte)driver$ELEM_HANDLE.get(struct, 0L, index0);
+ }
+
+ /**
+ * Indexed setter for field:
+ * {@snippet lang=c :
+ * char driver[256]
+ * }
+ */
+ public static void driver(MemorySegment struct, long index0, byte fieldValue) {
+ driver$ELEM_HANDLE.set(struct, 0L, index0, fieldValue);
+ }
+
+ /**
+ * Obtains a slice of {@code arrayParam} which selects the array element at {@code index}.
+ * The returned segment has address {@code arrayParam.address() + index * layout().byteSize()}
+ */
+ public static MemorySegment asSlice(MemorySegment array, long index) {
+ return array.asSlice(layout().byteSize() * index);
+ }
+
+ /**
+ * The size (in bytes) of this struct
+ */
+ public static long sizeof() { return layout().byteSize(); }
+
+ /**
+ * Allocate a segment of size {@code layout().byteSize()} using {@code allocator}
+ */
+ public static MemorySegment allocate(SegmentAllocator allocator) {
+ return allocator.allocate(layout());
+ }
+
+ /**
+ * Allocate an array of size {@code elementCount} using {@code allocator}.
+ * The returned segment has size {@code elementCount * layout().byteSize()}.
+ */
+ public static MemorySegment allocateArray(long elementCount, SegmentAllocator allocator) {
+ return allocator.allocate(MemoryLayout.sequenceLayout(elementCount, layout()));
+ }
+
+ /**
+ * Reinterprets {@code addr} using target {@code arena} and {@code cleanupAction} (if any).
+ * The returned segment has size {@code layout().byteSize()}
+ */
+ public static MemorySegment reinterpret(MemorySegment addr, Arena arena, Consumer cleanup) {
+ return reinterpret(addr, 1, arena, cleanup);
+ }
+
+ /**
+ * Reinterprets {@code addr} using target {@code arena} and {@code cleanupAction} (if any).
+ * The returned segment has size {@code elementCount * layout().byteSize()}
+ */
+ public static MemorySegment reinterpret(MemorySegment addr, long elementCount, Arena arena, Consumer cleanup) {
+ return addr.reinterpret(layout().byteSize() * elementCount, arena, cleanup);
+ }
+}
+
diff --git a/java-does-usb/src/main/java/net/codecrete/usb/linux/gen/usbdevice_fs/usbdevfs_ioctl.java b/java-does-usb/src/main/java/net/codecrete/usb/linux/gen/usbdevice_fs/usbdevfs_ioctl.java
new file mode 100644
index 00000000..8d4b6143
--- /dev/null
+++ b/java-does-usb/src/main/java/net/codecrete/usb/linux/gen/usbdevice_fs/usbdevfs_ioctl.java
@@ -0,0 +1,219 @@
+// Generated by jextract
+
+package net.codecrete.usb.linux.gen.usbdevice_fs;
+
+import java.lang.invoke.*;
+import java.lang.foreign.*;
+import java.nio.ByteOrder;
+import java.util.*;
+import java.util.function.*;
+import java.util.stream.*;
+
+import static java.lang.foreign.ValueLayout.*;
+import static java.lang.foreign.MemoryLayout.PathElement.*;
+
+/**
+ * {@snippet lang=c :
+ * struct usbdevfs_ioctl {
+ * int ifno;
+ * int ioctl_code;
+ * void *data;
+ * }
+ * }
+ */
+public class usbdevfs_ioctl {
+
+ usbdevfs_ioctl() {
+ // Should not be called directly
+ }
+
+ private static final GroupLayout $LAYOUT = MemoryLayout.structLayout(
+ usbdevice_fs.C_INT.withName("ifno"),
+ usbdevice_fs.C_INT.withName("ioctl_code"),
+ usbdevice_fs.C_POINTER.withName("data")
+ ).withName("usbdevfs_ioctl");
+
+ /**
+ * The layout of this struct
+ */
+ public static final GroupLayout layout() {
+ return $LAYOUT;
+ }
+
+ private static final OfInt ifno$LAYOUT = (OfInt)$LAYOUT.select(groupElement("ifno"));
+
+ /**
+ * Layout for field:
+ * {@snippet lang=c :
+ * int ifno
+ * }
+ */
+ public static final OfInt ifno$layout() {
+ return ifno$LAYOUT;
+ }
+
+ private static final long ifno$OFFSET = $LAYOUT.byteOffset(groupElement("ifno"));
+
+ /**
+ * Offset for field:
+ * {@snippet lang=c :
+ * int ifno
+ * }
+ */
+ public static final long ifno$offset() {
+ return ifno$OFFSET;
+ }
+
+ /**
+ * Getter for field:
+ * {@snippet lang=c :
+ * int ifno
+ * }
+ */
+ public static int ifno(MemorySegment struct) {
+ return struct.get(ifno$LAYOUT, ifno$OFFSET);
+ }
+
+ /**
+ * Setter for field:
+ * {@snippet lang=c :
+ * int ifno
+ * }
+ */
+ public static void ifno(MemorySegment struct, int fieldValue) {
+ struct.set(ifno$LAYOUT, ifno$OFFSET, fieldValue);
+ }
+
+ private static final OfInt ioctl_code$LAYOUT = (OfInt)$LAYOUT.select(groupElement("ioctl_code"));
+
+ /**
+ * Layout for field:
+ * {@snippet lang=c :
+ * int ioctl_code
+ * }
+ */
+ public static final OfInt ioctl_code$layout() {
+ return ioctl_code$LAYOUT;
+ }
+
+ private static final long ioctl_code$OFFSET = $LAYOUT.byteOffset(groupElement("ioctl_code"));
+
+ /**
+ * Offset for field:
+ * {@snippet lang=c :
+ * int ioctl_code
+ * }
+ */
+ public static final long ioctl_code$offset() {
+ return ioctl_code$OFFSET;
+ }
+
+ /**
+ * Getter for field:
+ * {@snippet lang=c :
+ * int ioctl_code
+ * }
+ */
+ public static int ioctl_code(MemorySegment struct) {
+ return struct.get(ioctl_code$LAYOUT, ioctl_code$OFFSET);
+ }
+
+ /**
+ * Setter for field:
+ * {@snippet lang=c :
+ * int ioctl_code
+ * }
+ */
+ public static void ioctl_code(MemorySegment struct, int fieldValue) {
+ struct.set(ioctl_code$LAYOUT, ioctl_code$OFFSET, fieldValue);
+ }
+
+ private static final AddressLayout data$LAYOUT = (AddressLayout)$LAYOUT.select(groupElement("data"));
+
+ /**
+ * Layout for field:
+ * {@snippet lang=c :
+ * void *data
+ * }
+ */
+ public static final AddressLayout data$layout() {
+ return data$LAYOUT;
+ }
+
+ private static final long data$OFFSET = $LAYOUT.byteOffset(groupElement("data"));
+
+ /**
+ * Offset for field:
+ * {@snippet lang=c :
+ * void *data
+ * }
+ */
+ public static final long data$offset() {
+ return data$OFFSET;
+ }
+
+ /**
+ * Getter for field:
+ * {@snippet lang=c :
+ * void *data
+ * }
+ */
+ public static MemorySegment data(MemorySegment struct) {
+ return struct.get(data$LAYOUT, data$OFFSET);
+ }
+
+ /**
+ * Setter for field:
+ * {@snippet lang=c :
+ * void *data
+ * }
+ */
+ public static void data(MemorySegment struct, MemorySegment fieldValue) {
+ struct.set(data$LAYOUT, data$OFFSET, fieldValue);
+ }
+
+ /**
+ * Obtains a slice of {@code arrayParam} which selects the array element at {@code index}.
+ * The returned segment has address {@code arrayParam.address() + index * layout().byteSize()}
+ */
+ public static MemorySegment asSlice(MemorySegment array, long index) {
+ return array.asSlice(layout().byteSize() * index);
+ }
+
+ /**
+ * The size (in bytes) of this struct
+ */
+ public static long sizeof() { return layout().byteSize(); }
+
+ /**
+ * Allocate a segment of size {@code layout().byteSize()} using {@code allocator}
+ */
+ public static MemorySegment allocate(SegmentAllocator allocator) {
+ return allocator.allocate(layout());
+ }
+
+ /**
+ * Allocate an array of size {@code elementCount} using {@code allocator}.
+ * The returned segment has size {@code elementCount * layout().byteSize()}.
+ */
+ public static MemorySegment allocateArray(long elementCount, SegmentAllocator allocator) {
+ return allocator.allocate(MemoryLayout.sequenceLayout(elementCount, layout()));
+ }
+
+ /**
+ * Reinterprets {@code addr} using target {@code arena} and {@code cleanupAction} (if any).
+ * The returned segment has size {@code layout().byteSize()}
+ */
+ public static MemorySegment reinterpret(MemorySegment addr, Arena arena, Consumer cleanup) {
+ return reinterpret(addr, 1, arena, cleanup);
+ }
+
+ /**
+ * Reinterprets {@code addr} using target {@code arena} and {@code cleanupAction} (if any).
+ * The returned segment has size {@code elementCount * layout().byteSize()}
+ */
+ public static MemorySegment reinterpret(MemorySegment addr, long elementCount, Arena arena, Consumer cleanup) {
+ return addr.reinterpret(layout().byteSize() * elementCount, arena, cleanup);
+ }
+}
+
diff --git a/java-does-usb/src/main/java/net/codecrete/usb/linux/gen/usbdevice_fs/usbdevfs_iso_packet_desc.java b/java-does-usb/src/main/java/net/codecrete/usb/linux/gen/usbdevice_fs/usbdevfs_iso_packet_desc.java
new file mode 100644
index 00000000..c269241e
--- /dev/null
+++ b/java-does-usb/src/main/java/net/codecrete/usb/linux/gen/usbdevice_fs/usbdevfs_iso_packet_desc.java
@@ -0,0 +1,219 @@
+// Generated by jextract
+
+package net.codecrete.usb.linux.gen.usbdevice_fs;
+
+import java.lang.invoke.*;
+import java.lang.foreign.*;
+import java.nio.ByteOrder;
+import java.util.*;
+import java.util.function.*;
+import java.util.stream.*;
+
+import static java.lang.foreign.ValueLayout.*;
+import static java.lang.foreign.MemoryLayout.PathElement.*;
+
+/**
+ * {@snippet lang=c :
+ * struct usbdevfs_iso_packet_desc {
+ * unsigned int length;
+ * unsigned int actual_length;
+ * unsigned int status;
+ * }
+ * }
+ */
+public class usbdevfs_iso_packet_desc {
+
+ usbdevfs_iso_packet_desc() {
+ // Should not be called directly
+ }
+
+ private static final GroupLayout $LAYOUT = MemoryLayout.structLayout(
+ usbdevice_fs.C_INT.withName("length"),
+ usbdevice_fs.C_INT.withName("actual_length"),
+ usbdevice_fs.C_INT.withName("status")
+ ).withName("usbdevfs_iso_packet_desc");
+
+ /**
+ * The layout of this struct
+ */
+ public static final GroupLayout layout() {
+ return $LAYOUT;
+ }
+
+ private static final OfInt length$LAYOUT = (OfInt)$LAYOUT.select(groupElement("length"));
+
+ /**
+ * Layout for field:
+ * {@snippet lang=c :
+ * unsigned int length
+ * }
+ */
+ public static final OfInt length$layout() {
+ return length$LAYOUT;
+ }
+
+ private static final long length$OFFSET = $LAYOUT.byteOffset(groupElement("length"));
+
+ /**
+ * Offset for field:
+ * {@snippet lang=c :
+ * unsigned int length
+ * }
+ */
+ public static final long length$offset() {
+ return length$OFFSET;
+ }
+
+ /**
+ * Getter for field:
+ * {@snippet lang=c :
+ * unsigned int length
+ * }
+ */
+ public static int length(MemorySegment struct) {
+ return struct.get(length$LAYOUT, length$OFFSET);
+ }
+
+ /**
+ * Setter for field:
+ * {@snippet lang=c :
+ * unsigned int length
+ * }
+ */
+ public static void length(MemorySegment struct, int fieldValue) {
+ struct.set(length$LAYOUT, length$OFFSET, fieldValue);
+ }
+
+ private static final OfInt actual_length$LAYOUT = (OfInt)$LAYOUT.select(groupElement("actual_length"));
+
+ /**
+ * Layout for field:
+ * {@snippet lang=c :
+ * unsigned int actual_length
+ * }
+ */
+ public static final OfInt actual_length$layout() {
+ return actual_length$LAYOUT;
+ }
+
+ private static final long actual_length$OFFSET = $LAYOUT.byteOffset(groupElement("actual_length"));
+
+ /**
+ * Offset for field:
+ * {@snippet lang=c :
+ * unsigned int actual_length
+ * }
+ */
+ public static final long actual_length$offset() {
+ return actual_length$OFFSET;
+ }
+
+ /**
+ * Getter for field:
+ * {@snippet lang=c :
+ * unsigned int actual_length
+ * }
+ */
+ public static int actual_length(MemorySegment struct) {
+ return struct.get(actual_length$LAYOUT, actual_length$OFFSET);
+ }
+
+ /**
+ * Setter for field:
+ * {@snippet lang=c :
+ * unsigned int actual_length
+ * }
+ */
+ public static void actual_length(MemorySegment struct, int fieldValue) {
+ struct.set(actual_length$LAYOUT, actual_length$OFFSET, fieldValue);
+ }
+
+ private static final OfInt status$LAYOUT = (OfInt)$LAYOUT.select(groupElement("status"));
+
+ /**
+ * Layout for field:
+ * {@snippet lang=c :
+ * unsigned int status
+ * }
+ */
+ public static final OfInt status$layout() {
+ return status$LAYOUT;
+ }
+
+ private static final long status$OFFSET = $LAYOUT.byteOffset(groupElement("status"));
+
+ /**
+ * Offset for field:
+ * {@snippet lang=c :
+ * unsigned int status
+ * }
+ */
+ public static final long status$offset() {
+ return status$OFFSET;
+ }
+
+ /**
+ * Getter for field:
+ * {@snippet lang=c :
+ * unsigned int status
+ * }
+ */
+ public static int status(MemorySegment struct) {
+ return struct.get(status$LAYOUT, status$OFFSET);
+ }
+
+ /**
+ * Setter for field:
+ * {@snippet lang=c :
+ * unsigned int status
+ * }
+ */
+ public static void status(MemorySegment struct, int fieldValue) {
+ struct.set(status$LAYOUT, status$OFFSET, fieldValue);
+ }
+
+ /**
+ * Obtains a slice of {@code arrayParam} which selects the array element at {@code index}.
+ * The returned segment has address {@code arrayParam.address() + index * layout().byteSize()}
+ */
+ public static MemorySegment asSlice(MemorySegment array, long index) {
+ return array.asSlice(layout().byteSize() * index);
+ }
+
+ /**
+ * The size (in bytes) of this struct
+ */
+ public static long sizeof() { return layout().byteSize(); }
+
+ /**
+ * Allocate a segment of size {@code layout().byteSize()} using {@code allocator}
+ */
+ public static MemorySegment allocate(SegmentAllocator allocator) {
+ return allocator.allocate(layout());
+ }
+
+ /**
+ * Allocate an array of size {@code elementCount} using {@code allocator}.
+ * The returned segment has size {@code elementCount * layout().byteSize()}.
+ */
+ public static MemorySegment allocateArray(long elementCount, SegmentAllocator allocator) {
+ return allocator.allocate(MemoryLayout.sequenceLayout(elementCount, layout()));
+ }
+
+ /**
+ * Reinterprets {@code addr} using target {@code arena} and {@code cleanupAction} (if any).
+ * The returned segment has size {@code layout().byteSize()}
+ */
+ public static MemorySegment reinterpret(MemorySegment addr, Arena arena, Consumer cleanup) {
+ return reinterpret(addr, 1, arena, cleanup);
+ }
+
+ /**
+ * Reinterprets {@code addr} using target {@code arena} and {@code cleanupAction} (if any).
+ * The returned segment has size {@code elementCount * layout().byteSize()}
+ */
+ public static MemorySegment reinterpret(MemorySegment addr, long elementCount, Arena arena, Consumer cleanup) {
+ return addr.reinterpret(layout().byteSize() * elementCount, arena, cleanup);
+ }
+}
+
diff --git a/java-does-usb/src/main/java/net/codecrete/usb/linux/gen/usbdevice_fs/usbdevfs_setinterface.java b/java-does-usb/src/main/java/net/codecrete/usb/linux/gen/usbdevice_fs/usbdevfs_setinterface.java
new file mode 100644
index 00000000..fdae516e
--- /dev/null
+++ b/java-does-usb/src/main/java/net/codecrete/usb/linux/gen/usbdevice_fs/usbdevfs_setinterface.java
@@ -0,0 +1,173 @@
+// Generated by jextract
+
+package net.codecrete.usb.linux.gen.usbdevice_fs;
+
+import java.lang.invoke.*;
+import java.lang.foreign.*;
+import java.nio.ByteOrder;
+import java.util.*;
+import java.util.function.*;
+import java.util.stream.*;
+
+import static java.lang.foreign.ValueLayout.*;
+import static java.lang.foreign.MemoryLayout.PathElement.*;
+
+/**
+ * {@snippet lang=c :
+ * struct usbdevfs_setinterface {
+ * unsigned int interface;
+ * unsigned int altsetting;
+ * }
+ * }
+ */
+public class usbdevfs_setinterface {
+
+ usbdevfs_setinterface() {
+ // Should not be called directly
+ }
+
+ private static final GroupLayout $LAYOUT = MemoryLayout.structLayout(
+ usbdevice_fs.C_INT.withName("interface"),
+ usbdevice_fs.C_INT.withName("altsetting")
+ ).withName("usbdevfs_setinterface");
+
+ /**
+ * The layout of this struct
+ */
+ public static final GroupLayout layout() {
+ return $LAYOUT;
+ }
+
+ private static final OfInt interface_$LAYOUT = (OfInt)$LAYOUT.select(groupElement("interface"));
+
+ /**
+ * Layout for field:
+ * {@snippet lang=c :
+ * unsigned int interface
+ * }
+ */
+ public static final OfInt interface_$layout() {
+ return interface_$LAYOUT;
+ }
+
+ private static final long interface_$OFFSET = $LAYOUT.byteOffset(groupElement("interface"));
+
+ /**
+ * Offset for field:
+ * {@snippet lang=c :
+ * unsigned int interface
+ * }
+ */
+ public static final long interface_$offset() {
+ return interface_$OFFSET;
+ }
+
+ /**
+ * Getter for field:
+ * {@snippet lang=c :
+ * unsigned int interface
+ * }
+ */
+ public static int interface_(MemorySegment struct) {
+ return struct.get(interface_$LAYOUT, interface_$OFFSET);
+ }
+
+ /**
+ * Setter for field:
+ * {@snippet lang=c :
+ * unsigned int interface
+ * }
+ */
+ public static void interface_(MemorySegment struct, int fieldValue) {
+ struct.set(interface_$LAYOUT, interface_$OFFSET, fieldValue);
+ }
+
+ private static final OfInt altsetting$LAYOUT = (OfInt)$LAYOUT.select(groupElement("altsetting"));
+
+ /**
+ * Layout for field:
+ * {@snippet lang=c :
+ * unsigned int altsetting
+ * }
+ */
+ public static final OfInt altsetting$layout() {
+ return altsetting$LAYOUT;
+ }
+
+ private static final long altsetting$OFFSET = $LAYOUT.byteOffset(groupElement("altsetting"));
+
+ /**
+ * Offset for field:
+ * {@snippet lang=c :
+ * unsigned int altsetting
+ * }
+ */
+ public static final long altsetting$offset() {
+ return altsetting$OFFSET;
+ }
+
+ /**
+ * Getter for field:
+ * {@snippet lang=c :
+ * unsigned int altsetting
+ * }
+ */
+ public static int altsetting(MemorySegment struct) {
+ return struct.get(altsetting$LAYOUT, altsetting$OFFSET);
+ }
+
+ /**
+ * Setter for field:
+ * {@snippet lang=c :
+ * unsigned int altsetting
+ * }
+ */
+ public static void altsetting(MemorySegment struct, int fieldValue) {
+ struct.set(altsetting$LAYOUT, altsetting$OFFSET, fieldValue);
+ }
+
+ /**
+ * Obtains a slice of {@code arrayParam} which selects the array element at {@code index}.
+ * The returned segment has address {@code arrayParam.address() + index * layout().byteSize()}
+ */
+ public static MemorySegment asSlice(MemorySegment array, long index) {
+ return array.asSlice(layout().byteSize() * index);
+ }
+
+ /**
+ * The size (in bytes) of this struct
+ */
+ public static long sizeof() { return layout().byteSize(); }
+
+ /**
+ * Allocate a segment of size {@code layout().byteSize()} using {@code allocator}
+ */
+ public static MemorySegment allocate(SegmentAllocator allocator) {
+ return allocator.allocate(layout());
+ }
+
+ /**
+ * Allocate an array of size {@code elementCount} using {@code allocator}.
+ * The returned segment has size {@code elementCount * layout().byteSize()}.
+ */
+ public static MemorySegment allocateArray(long elementCount, SegmentAllocator allocator) {
+ return allocator.allocate(MemoryLayout.sequenceLayout(elementCount, layout()));
+ }
+
+ /**
+ * Reinterprets {@code addr} using target {@code arena} and {@code cleanupAction} (if any).
+ * The returned segment has size {@code layout().byteSize()}
+ */
+ public static MemorySegment reinterpret(MemorySegment addr, Arena arena, Consumer cleanup) {
+ return reinterpret(addr, 1, arena, cleanup);
+ }
+
+ /**
+ * Reinterprets {@code addr} using target {@code arena} and {@code cleanupAction} (if any).
+ * The returned segment has size {@code elementCount * layout().byteSize()}
+ */
+ public static MemorySegment reinterpret(MemorySegment addr, long elementCount, Arena arena, Consumer cleanup) {
+ return addr.reinterpret(layout().byteSize() * elementCount, arena, cleanup);
+ }
+}
+
diff --git a/java-does-usb/src/main/java/net/codecrete/usb/linux/gen/usbdevice_fs/usbdevfs_urb.java b/java-does-usb/src/main/java/net/codecrete/usb/linux/gen/usbdevice_fs/usbdevfs_urb.java
new file mode 100644
index 00000000..556be6c4
--- /dev/null
+++ b/java-does-usb/src/main/java/net/codecrete/usb/linux/gen/usbdevice_fs/usbdevfs_urb.java
@@ -0,0 +1,640 @@
+// Generated by jextract
+
+package net.codecrete.usb.linux.gen.usbdevice_fs;
+
+import java.lang.invoke.*;
+import java.lang.foreign.*;
+import java.nio.ByteOrder;
+import java.util.*;
+import java.util.function.*;
+import java.util.stream.*;
+
+import static java.lang.foreign.ValueLayout.*;
+import static java.lang.foreign.MemoryLayout.PathElement.*;
+
+/**
+ * {@snippet lang=c :
+ * struct usbdevfs_urb {
+ * unsigned char type;
+ * unsigned char endpoint;
+ * int status;
+ * unsigned int flags;
+ * void *buffer;
+ * int buffer_length;
+ * int actual_length;
+ * int start_frame;
+ * union {
+ * int number_of_packets;
+ * unsigned int stream_id;
+ * };
+ * int error_count;
+ * unsigned int signr;
+ * void *usercontext;
+ * struct usbdevfs_iso_packet_desc iso_frame_desc[];
+ * }
+ * }
+ */
+public class usbdevfs_urb {
+
+ usbdevfs_urb() {
+ // Should not be called directly
+ }
+
+ private static final GroupLayout $LAYOUT = MemoryLayout.structLayout(
+ usbdevice_fs.C_CHAR.withName("type"),
+ usbdevice_fs.C_CHAR.withName("endpoint"),
+ MemoryLayout.paddingLayout(2),
+ usbdevice_fs.C_INT.withName("status"),
+ usbdevice_fs.C_INT.withName("flags"),
+ MemoryLayout.paddingLayout(4),
+ usbdevice_fs.C_POINTER.withName("buffer"),
+ usbdevice_fs.C_INT.withName("buffer_length"),
+ usbdevice_fs.C_INT.withName("actual_length"),
+ usbdevice_fs.C_INT.withName("start_frame"),
+ MemoryLayout.paddingLayout(4),
+ usbdevice_fs.C_INT.withName("error_count"),
+ usbdevice_fs.C_INT.withName("signr"),
+ usbdevice_fs.C_POINTER.withName("usercontext"),
+ MemoryLayout.sequenceLayout(0, usbdevfs_iso_packet_desc.layout()).withName("iso_frame_desc")
+ ).withName("usbdevfs_urb");
+
+ /**
+ * The layout of this struct
+ */
+ public static final GroupLayout layout() {
+ return $LAYOUT;
+ }
+
+ private static final OfByte type$LAYOUT = (OfByte)$LAYOUT.select(groupElement("type"));
+
+ /**
+ * Layout for field:
+ * {@snippet lang=c :
+ * unsigned char type
+ * }
+ */
+ public static final OfByte type$layout() {
+ return type$LAYOUT;
+ }
+
+ private static final long type$OFFSET = $LAYOUT.byteOffset(groupElement("type"));
+
+ /**
+ * Offset for field:
+ * {@snippet lang=c :
+ * unsigned char type
+ * }
+ */
+ public static final long type$offset() {
+ return type$OFFSET;
+ }
+
+ /**
+ * Getter for field:
+ * {@snippet lang=c :
+ * unsigned char type
+ * }
+ */
+ public static byte type(MemorySegment struct) {
+ return struct.get(type$LAYOUT, type$OFFSET);
+ }
+
+ /**
+ * Setter for field:
+ * {@snippet lang=c :
+ * unsigned char type
+ * }
+ */
+ public static void type(MemorySegment struct, byte fieldValue) {
+ struct.set(type$LAYOUT, type$OFFSET, fieldValue);
+ }
+
+ private static final OfByte endpoint$LAYOUT = (OfByte)$LAYOUT.select(groupElement("endpoint"));
+
+ /**
+ * Layout for field:
+ * {@snippet lang=c :
+ * unsigned char endpoint
+ * }
+ */
+ public static final OfByte endpoint$layout() {
+ return endpoint$LAYOUT;
+ }
+
+ private static final long endpoint$OFFSET = $LAYOUT.byteOffset(groupElement("endpoint"));
+
+ /**
+ * Offset for field:
+ * {@snippet lang=c :
+ * unsigned char endpoint
+ * }
+ */
+ public static final long endpoint$offset() {
+ return endpoint$OFFSET;
+ }
+
+ /**
+ * Getter for field:
+ * {@snippet lang=c :
+ * unsigned char endpoint
+ * }
+ */
+ public static byte endpoint(MemorySegment struct) {
+ return struct.get(endpoint$LAYOUT, endpoint$OFFSET);
+ }
+
+ /**
+ * Setter for field:
+ * {@snippet lang=c :
+ * unsigned char endpoint
+ * }
+ */
+ public static void endpoint(MemorySegment struct, byte fieldValue) {
+ struct.set(endpoint$LAYOUT, endpoint$OFFSET, fieldValue);
+ }
+
+ private static final OfInt status$LAYOUT = (OfInt)$LAYOUT.select(groupElement("status"));
+
+ /**
+ * Layout for field:
+ * {@snippet lang=c :
+ * int status
+ * }
+ */
+ public static final OfInt status$layout() {
+ return status$LAYOUT;
+ }
+
+ private static final long status$OFFSET = $LAYOUT.byteOffset(groupElement("status"));
+
+ /**
+ * Offset for field:
+ * {@snippet lang=c :
+ * int status
+ * }
+ */
+ public static final long status$offset() {
+ return status$OFFSET;
+ }
+
+ /**
+ * Getter for field:
+ * {@snippet lang=c :
+ * int status
+ * }
+ */
+ public static int status(MemorySegment struct) {
+ return struct.get(status$LAYOUT, status$OFFSET);
+ }
+
+ /**
+ * Setter for field:
+ * {@snippet lang=c :
+ * int status
+ * }
+ */
+ public static void status(MemorySegment struct, int fieldValue) {
+ struct.set(status$LAYOUT, status$OFFSET, fieldValue);
+ }
+
+ private static final OfInt flags$LAYOUT = (OfInt)$LAYOUT.select(groupElement("flags"));
+
+ /**
+ * Layout for field:
+ * {@snippet lang=c :
+ * unsigned int flags
+ * }
+ */
+ public static final OfInt flags$layout() {
+ return flags$LAYOUT;
+ }
+
+ private static final long flags$OFFSET = $LAYOUT.byteOffset(groupElement("flags"));
+
+ /**
+ * Offset for field:
+ * {@snippet lang=c :
+ * unsigned int flags
+ * }
+ */
+ public static final long flags$offset() {
+ return flags$OFFSET;
+ }
+
+ /**
+ * Getter for field:
+ * {@snippet lang=c :
+ * unsigned int flags
+ * }
+ */
+ public static int flags(MemorySegment struct) {
+ return struct.get(flags$LAYOUT, flags$OFFSET);
+ }
+
+ /**
+ * Setter for field:
+ * {@snippet lang=c :
+ * unsigned int flags
+ * }
+ */
+ public static void flags(MemorySegment struct, int fieldValue) {
+ struct.set(flags$LAYOUT, flags$OFFSET, fieldValue);
+ }
+
+ private static final AddressLayout buffer$LAYOUT = (AddressLayout)$LAYOUT.select(groupElement("buffer"));
+
+ /**
+ * Layout for field:
+ * {@snippet lang=c :
+ * void *buffer
+ * }
+ */
+ public static final AddressLayout buffer$layout() {
+ return buffer$LAYOUT;
+ }
+
+ private static final long buffer$OFFSET = $LAYOUT.byteOffset(groupElement("buffer"));
+
+ /**
+ * Offset for field:
+ * {@snippet lang=c :
+ * void *buffer
+ * }
+ */
+ public static final long buffer$offset() {
+ return buffer$OFFSET;
+ }
+
+ /**
+ * Getter for field:
+ * {@snippet lang=c :
+ * void *buffer
+ * }
+ */
+ public static MemorySegment buffer(MemorySegment struct) {
+ return struct.get(buffer$LAYOUT, buffer$OFFSET);
+ }
+
+ /**
+ * Setter for field:
+ * {@snippet lang=c :
+ * void *buffer
+ * }
+ */
+ public static void buffer(MemorySegment struct, MemorySegment fieldValue) {
+ struct.set(buffer$LAYOUT, buffer$OFFSET, fieldValue);
+ }
+
+ private static final OfInt buffer_length$LAYOUT = (OfInt)$LAYOUT.select(groupElement("buffer_length"));
+
+ /**
+ * Layout for field:
+ * {@snippet lang=c :
+ * int buffer_length
+ * }
+ */
+ public static final OfInt buffer_length$layout() {
+ return buffer_length$LAYOUT;
+ }
+
+ private static final long buffer_length$OFFSET = $LAYOUT.byteOffset(groupElement("buffer_length"));
+
+ /**
+ * Offset for field:
+ * {@snippet lang=c :
+ * int buffer_length
+ * }
+ */
+ public static final long buffer_length$offset() {
+ return buffer_length$OFFSET;
+ }
+
+ /**
+ * Getter for field:
+ * {@snippet lang=c :
+ * int buffer_length
+ * }
+ */
+ public static int buffer_length(MemorySegment struct) {
+ return struct.get(buffer_length$LAYOUT, buffer_length$OFFSET);
+ }
+
+ /**
+ * Setter for field:
+ * {@snippet lang=c :
+ * int buffer_length
+ * }
+ */
+ public static void buffer_length(MemorySegment struct, int fieldValue) {
+ struct.set(buffer_length$LAYOUT, buffer_length$OFFSET, fieldValue);
+ }
+
+ private static final OfInt actual_length$LAYOUT = (OfInt)$LAYOUT.select(groupElement("actual_length"));
+
+ /**
+ * Layout for field:
+ * {@snippet lang=c :
+ * int actual_length
+ * }
+ */
+ public static final OfInt actual_length$layout() {
+ return actual_length$LAYOUT;
+ }
+
+ private static final long actual_length$OFFSET = $LAYOUT.byteOffset(groupElement("actual_length"));
+
+ /**
+ * Offset for field:
+ * {@snippet lang=c :
+ * int actual_length
+ * }
+ */
+ public static final long actual_length$offset() {
+ return actual_length$OFFSET;
+ }
+
+ /**
+ * Getter for field:
+ * {@snippet lang=c :
+ * int actual_length
+ * }
+ */
+ public static int actual_length(MemorySegment struct) {
+ return struct.get(actual_length$LAYOUT, actual_length$OFFSET);
+ }
+
+ /**
+ * Setter for field:
+ * {@snippet lang=c :
+ * int actual_length
+ * }
+ */
+ public static void actual_length(MemorySegment struct, int fieldValue) {
+ struct.set(actual_length$LAYOUT, actual_length$OFFSET, fieldValue);
+ }
+
+ private static final OfInt start_frame$LAYOUT = (OfInt)$LAYOUT.select(groupElement("start_frame"));
+
+ /**
+ * Layout for field:
+ * {@snippet lang=c :
+ * int start_frame
+ * }
+ */
+ public static final OfInt start_frame$layout() {
+ return start_frame$LAYOUT;
+ }
+
+ private static final long start_frame$OFFSET = $LAYOUT.byteOffset(groupElement("start_frame"));
+
+ /**
+ * Offset for field:
+ * {@snippet lang=c :
+ * int start_frame
+ * }
+ */
+ public static final long start_frame$offset() {
+ return start_frame$OFFSET;
+ }
+
+ /**
+ * Getter for field:
+ * {@snippet lang=c :
+ * int start_frame
+ * }
+ */
+ public static int start_frame(MemorySegment struct) {
+ return struct.get(start_frame$LAYOUT, start_frame$OFFSET);
+ }
+
+ /**
+ * Setter for field:
+ * {@snippet lang=c :
+ * int start_frame
+ * }
+ */
+ public static void start_frame(MemorySegment struct, int fieldValue) {
+ struct.set(start_frame$LAYOUT, start_frame$OFFSET, fieldValue);
+ }
+
+ private static final OfInt error_count$LAYOUT = (OfInt)$LAYOUT.select(groupElement("error_count"));
+
+ /**
+ * Layout for field:
+ * {@snippet lang=c :
+ * int error_count
+ * }
+ */
+ public static final OfInt error_count$layout() {
+ return error_count$LAYOUT;
+ }
+
+ private static final long error_count$OFFSET = $LAYOUT.byteOffset(groupElement("error_count"));
+
+ /**
+ * Offset for field:
+ * {@snippet lang=c :
+ * int error_count
+ * }
+ */
+ public static final long error_count$offset() {
+ return error_count$OFFSET;
+ }
+
+ /**
+ * Getter for field:
+ * {@snippet lang=c :
+ * int error_count
+ * }
+ */
+ public static int error_count(MemorySegment struct) {
+ return struct.get(error_count$LAYOUT, error_count$OFFSET);
+ }
+
+ /**
+ * Setter for field:
+ * {@snippet lang=c :
+ * int error_count
+ * }
+ */
+ public static void error_count(MemorySegment struct, int fieldValue) {
+ struct.set(error_count$LAYOUT, error_count$OFFSET, fieldValue);
+ }
+
+ private static final OfInt signr$LAYOUT = (OfInt)$LAYOUT.select(groupElement("signr"));
+
+ /**
+ * Layout for field:
+ * {@snippet lang=c :
+ * unsigned int signr
+ * }
+ */
+ public static final OfInt signr$layout() {
+ return signr$LAYOUT;
+ }
+
+ private static final long signr$OFFSET = $LAYOUT.byteOffset(groupElement("signr"));
+
+ /**
+ * Offset for field:
+ * {@snippet lang=c :
+ * unsigned int signr
+ * }
+ */
+ public static final long signr$offset() {
+ return signr$OFFSET;
+ }
+
+ /**
+ * Getter for field:
+ * {@snippet lang=c :
+ * unsigned int signr
+ * }
+ */
+ public static int signr(MemorySegment struct) {
+ return struct.get(signr$LAYOUT, signr$OFFSET);
+ }
+
+ /**
+ * Setter for field:
+ * {@snippet lang=c :
+ * unsigned int signr
+ * }
+ */
+ public static void signr(MemorySegment struct, int fieldValue) {
+ struct.set(signr$LAYOUT, signr$OFFSET, fieldValue);
+ }
+
+ private static final AddressLayout usercontext$LAYOUT = (AddressLayout)$LAYOUT.select(groupElement("usercontext"));
+
+ /**
+ * Layout for field:
+ * {@snippet lang=c :
+ * void *usercontext
+ * }
+ */
+ public static final AddressLayout usercontext$layout() {
+ return usercontext$LAYOUT;
+ }
+
+ private static final long usercontext$OFFSET = $LAYOUT.byteOffset(groupElement("usercontext"));
+
+ /**
+ * Offset for field:
+ * {@snippet lang=c :
+ * void *usercontext
+ * }
+ */
+ public static final long usercontext$offset() {
+ return usercontext$OFFSET;
+ }
+
+ /**
+ * Getter for field:
+ * {@snippet lang=c :
+ * void *usercontext
+ * }
+ */
+ public static MemorySegment usercontext(MemorySegment struct) {
+ return struct.get(usercontext$LAYOUT, usercontext$OFFSET);
+ }
+
+ /**
+ * Setter for field:
+ * {@snippet lang=c :
+ * void *usercontext
+ * }
+ */
+ public static void usercontext(MemorySegment struct, MemorySegment fieldValue) {
+ struct.set(usercontext$LAYOUT, usercontext$OFFSET, fieldValue);
+ }
+
+ private static final SequenceLayout iso_frame_desc$LAYOUT = (SequenceLayout)$LAYOUT.select(groupElement("iso_frame_desc"));
+
+ /**
+ * Layout for field:
+ * {@snippet lang=c :
+ * struct usbdevfs_iso_packet_desc iso_frame_desc[]
+ * }
+ */
+ public static final SequenceLayout iso_frame_desc$layout() {
+ return iso_frame_desc$LAYOUT;
+ }
+
+ private static final long iso_frame_desc$OFFSET = $LAYOUT.byteOffset(groupElement("iso_frame_desc"));
+
+ /**
+ * Offset for field:
+ * {@snippet lang=c :
+ * struct usbdevfs_iso_packet_desc iso_frame_desc[]
+ * }
+ */
+ public static final long iso_frame_desc$offset() {
+ return iso_frame_desc$OFFSET;
+ }
+
+ /**
+ * Getter for field:
+ * {@snippet lang=c :
+ * struct usbdevfs_iso_packet_desc iso_frame_desc[]
+ * }
+ */
+ public static MemorySegment iso_frame_desc(MemorySegment struct) {
+ return struct.asSlice(iso_frame_desc$OFFSET, iso_frame_desc$LAYOUT.byteSize());
+ }
+
+ /**
+ * Setter for field:
+ * {@snippet lang=c :
+ * struct usbdevfs_iso_packet_desc iso_frame_desc[]
+ * }
+ */
+ public static void iso_frame_desc(MemorySegment struct, MemorySegment fieldValue) {
+ MemorySegment.copy(fieldValue, 0L, struct, iso_frame_desc$OFFSET, iso_frame_desc$LAYOUT.byteSize());
+ }
+
+ /**
+ * Obtains a slice of {@code arrayParam} which selects the array element at {@code index}.
+ * The returned segment has address {@code arrayParam.address() + index * layout().byteSize()}
+ */
+ public static MemorySegment asSlice(MemorySegment array, long index) {
+ return array.asSlice(layout().byteSize() * index);
+ }
+
+ /**
+ * The size (in bytes) of this struct
+ */
+ public static long sizeof() { return layout().byteSize(); }
+
+ /**
+ * Allocate a segment of size {@code layout().byteSize()} using {@code allocator}
+ */
+ public static MemorySegment allocate(SegmentAllocator allocator) {
+ return allocator.allocate(layout());
+ }
+
+ /**
+ * Allocate an array of size {@code elementCount} using {@code allocator}.
+ * The returned segment has size {@code elementCount * layout().byteSize()}.
+ */
+ public static MemorySegment allocateArray(long elementCount, SegmentAllocator allocator) {
+ return allocator.allocate(MemoryLayout.sequenceLayout(elementCount, layout()));
+ }
+
+ /**
+ * Reinterprets {@code addr} using target {@code arena} and {@code cleanupAction} (if any).
+ * The returned segment has size {@code layout().byteSize()}
+ */
+ public static MemorySegment reinterpret(MemorySegment addr, Arena arena, Consumer cleanup) {
+ return reinterpret(addr, 1, arena, cleanup);
+ }
+
+ /**
+ * Reinterprets {@code addr} using target {@code arena} and {@code cleanupAction} (if any).
+ * The returned segment has size {@code elementCount * layout().byteSize()}
+ */
+ public static MemorySegment reinterpret(MemorySegment addr, long elementCount, Arena arena, Consumer cleanup) {
+ return addr.reinterpret(layout().byteSize() * elementCount, arena, cleanup);
+ }
+}
+
diff --git a/java-does-usb/src/main/java/net/codecrete/usb/linux/gen/usbdevice_fs/usbdevice_fs$shared.java b/java-does-usb/src/main/java/net/codecrete/usb/linux/gen/usbdevice_fs/usbdevice_fs$shared.java
new file mode 100644
index 00000000..bdf645da
--- /dev/null
+++ b/java-does-usb/src/main/java/net/codecrete/usb/linux/gen/usbdevice_fs/usbdevice_fs$shared.java
@@ -0,0 +1,63 @@
+// Generated by jextract
+
+package net.codecrete.usb.linux.gen.usbdevice_fs;
+
+import java.lang.invoke.*;
+import java.lang.foreign.*;
+import java.nio.ByteOrder;
+import java.util.*;
+import java.util.function.*;
+import java.util.stream.*;
+
+import static java.lang.foreign.ValueLayout.*;
+import static java.lang.foreign.MemoryLayout.PathElement.*;
+
+public class usbdevice_fs$shared {
+
+ usbdevice_fs$shared() {
+ // Should not be called directly
+ }
+
+ public static final ValueLayout.OfBoolean C_BOOL = (ValueLayout.OfBoolean) Linker.nativeLinker().canonicalLayouts().get("bool");
+ public static final ValueLayout.OfByte C_CHAR =(ValueLayout.OfByte)Linker.nativeLinker().canonicalLayouts().get("char");
+ public static final ValueLayout.OfShort C_SHORT = (ValueLayout.OfShort) Linker.nativeLinker().canonicalLayouts().get("short");
+ public static final ValueLayout.OfInt C_INT = (ValueLayout.OfInt) Linker.nativeLinker().canonicalLayouts().get("int");
+ public static final ValueLayout.OfLong C_LONG_LONG = (ValueLayout.OfLong) Linker.nativeLinker().canonicalLayouts().get("long long");
+ public static final ValueLayout.OfFloat C_FLOAT = (ValueLayout.OfFloat) Linker.nativeLinker().canonicalLayouts().get("float");
+ public static final ValueLayout.OfDouble C_DOUBLE = (ValueLayout.OfDouble) Linker.nativeLinker().canonicalLayouts().get("double");
+ public static final AddressLayout C_POINTER = ((AddressLayout) Linker.nativeLinker().canonicalLayouts().get("void*"))
+ .withTargetLayout(MemoryLayout.sequenceLayout(java.lang.Long.MAX_VALUE, C_CHAR));
+ public static final ValueLayout.OfLong C_LONG = (ValueLayout.OfLong) Linker.nativeLinker().canonicalLayouts().get("long");
+
+ static final boolean TRACE_DOWNCALLS = Boolean.getBoolean("jextract.trace.downcalls");
+
+ static void traceDowncall(String name, Object... args) {
+ String traceArgs = Arrays.stream(args)
+ .map(Object::toString)
+ .collect(Collectors.joining(", "));
+ System.out.printf("%s(%s)\n", name, traceArgs);
+ }
+
+ static MethodHandle upcallHandle(Class> fi, String name, FunctionDescriptor fdesc) {
+ try {
+ return MethodHandles.lookup().findVirtual(fi, name, fdesc.toMethodType());
+ } catch (ReflectiveOperationException ex) {
+ throw new AssertionError(ex);
+ }
+ }
+
+ static MemoryLayout align(MemoryLayout layout, long align) {
+ return switch (layout) {
+ case PaddingLayout p -> p;
+ case ValueLayout v -> v.withByteAlignment(align);
+ case GroupLayout g -> {
+ MemoryLayout[] alignedMembers = g.memberLayouts().stream()
+ .map(m -> align(m, align)).toArray(MemoryLayout[]::new);
+ yield g instanceof StructLayout ?
+ MemoryLayout.structLayout(alignedMembers) : MemoryLayout.unionLayout(alignedMembers);
+ }
+ case SequenceLayout s -> MemoryLayout.sequenceLayout(s.elementCount(), align(s.elementLayout(), align));
+ };
+ }
+}
+
diff --git a/java-does-usb/src/main/java/net/codecrete/usb/linux/gen/usbdevice_fs/usbdevice_fs.java b/java-does-usb/src/main/java/net/codecrete/usb/linux/gen/usbdevice_fs/usbdevice_fs.java
index e396cb35..8521d71b 100644
--- a/java-does-usb/src/main/java/net/codecrete/usb/linux/gen/usbdevice_fs/usbdevice_fs.java
+++ b/java-does-usb/src/main/java/net/codecrete/usb/linux/gen/usbdevice_fs/usbdevice_fs.java
@@ -2,18 +2,71 @@
package net.codecrete.usb.linux.gen.usbdevice_fs;
+import java.lang.invoke.*;
+import java.lang.foreign.*;
+import java.nio.ByteOrder;
+import java.util.*;
+import java.util.function.*;
+import java.util.stream.*;
+
import static java.lang.foreign.ValueLayout.*;
-public class usbdevice_fs {
-
- /* package-private */ usbdevice_fs() {}
- public static OfByte C_CHAR = Constants$root.C_CHAR$LAYOUT;
- public static OfShort C_SHORT = Constants$root.C_SHORT$LAYOUT;
- public static OfInt C_INT = Constants$root.C_INT$LAYOUT;
- public static OfLong C_LONG = Constants$root.C_LONG_LONG$LAYOUT;
- public static OfLong C_LONG_LONG = Constants$root.C_LONG_LONG$LAYOUT;
- public static OfFloat C_FLOAT = Constants$root.C_FLOAT$LAYOUT;
- public static OfDouble C_DOUBLE = Constants$root.C_DOUBLE$LAYOUT;
- public static OfAddress C_POINTER = Constants$root.C_POINTER$LAYOUT;
-}
+import static java.lang.foreign.MemoryLayout.PathElement.*;
+
+public class usbdevice_fs extends usbdevice_fs$shared {
+
+ usbdevice_fs() {
+ // Should not be called directly
+ }
+ static final Arena LIBRARY_ARENA = Arena.ofAuto();
+
+ static final SymbolLookup SYMBOL_LOOKUP = SymbolLookup.loaderLookup()
+ .or(Linker.nativeLinker().defaultLookup());
+
+ private static final int USBDEVFS_URB_TYPE_ISO = (int)0L;
+ /**
+ * {@snippet lang=c :
+ * #define USBDEVFS_URB_TYPE_ISO 0
+ * }
+ */
+ public static int USBDEVFS_URB_TYPE_ISO() {
+ return USBDEVFS_URB_TYPE_ISO;
+ }
+ private static final int USBDEVFS_URB_TYPE_INTERRUPT = (int)1L;
+ /**
+ * {@snippet lang=c :
+ * #define USBDEVFS_URB_TYPE_INTERRUPT 1
+ * }
+ */
+ public static int USBDEVFS_URB_TYPE_INTERRUPT() {
+ return USBDEVFS_URB_TYPE_INTERRUPT;
+ }
+ private static final int USBDEVFS_URB_TYPE_CONTROL = (int)2L;
+ /**
+ * {@snippet lang=c :
+ * #define USBDEVFS_URB_TYPE_CONTROL 2
+ * }
+ */
+ public static int USBDEVFS_URB_TYPE_CONTROL() {
+ return USBDEVFS_URB_TYPE_CONTROL;
+ }
+ private static final int USBDEVFS_URB_TYPE_BULK = (int)3L;
+ /**
+ * {@snippet lang=c :
+ * #define USBDEVFS_URB_TYPE_BULK 3
+ * }
+ */
+ public static int USBDEVFS_URB_TYPE_BULK() {
+ return USBDEVFS_URB_TYPE_BULK;
+ }
+ private static final int USBDEVFS_DISCONNECT_CLAIM_EXCEPT_DRIVER = (int)2L;
+ /**
+ * {@snippet lang=c :
+ * #define USBDEVFS_DISCONNECT_CLAIM_EXCEPT_DRIVER 2
+ * }
+ */
+ public static int USBDEVFS_DISCONNECT_CLAIM_EXCEPT_DRIVER() {
+ return USBDEVFS_DISCONNECT_CLAIM_EXCEPT_DRIVER;
+ }
+}
diff --git a/java-does-usb/src/main/java/net/codecrete/usb/macos/CoreFoundationHelper.java b/java-does-usb/src/main/java/net/codecrete/usb/macos/CoreFoundationHelper.java
index 6c663f8a..206d7a63 100644
--- a/java-does-usb/src/main/java/net/codecrete/usb/macos/CoreFoundationHelper.java
+++ b/java-does-usb/src/main/java/net/codecrete/usb/macos/CoreFoundationHelper.java
@@ -10,39 +10,37 @@
import net.codecrete.usb.macos.gen.corefoundation.CFRange;
import net.codecrete.usb.macos.gen.corefoundation.CoreFoundation;
-import java.lang.foreign.MemoryAddress;
+import java.lang.foreign.Arena;
import java.lang.foreign.MemorySegment;
-import java.lang.foreign.MemorySession;
+import java.lang.foreign.SegmentAllocator;
-import static java.lang.foreign.MemoryAddress.NULL;
+import static java.lang.foreign.MemorySegment.NULL;
import static java.lang.foreign.ValueLayout.JAVA_CHAR;
/**
* Core Foundation helper functions
*/
-public class CoreFoundationHelper {
+class CoreFoundationHelper {
+
+ private CoreFoundationHelper() {
+ }
/**
* Gets Java string as a copy of the {@code CFStringRef}.
*
* @param string the string to copy ({@code CFStringRef})
+ * @param arena the arena to allocate memory
* @return copied string
*/
- public static String stringFromCFStringRef(MemoryAddress string) {
-
- try (var session = MemorySession.openConfined()) {
-
- long strLen = CoreFoundation.CFStringGetLength(string);
- var buffer = session.allocateArray(JAVA_CHAR, strLen);
- var range = session.allocate(CFRange.$LAYOUT());
- CFRange.location$set(range, 0);
- CFRange.length$set(range, strLen);
- CoreFoundation.CFStringGetCharacters(string, range, buffer);
- return new String(buffer.toArray(JAVA_CHAR));
-
- } catch (Throwable t) {
- throw new RuntimeException(t);
- }
+ static String stringFromCFStringRef(MemorySegment string, Arena arena) {
+
+ var strLen = CoreFoundation.CFStringGetLength(string);
+ var buffer = arena.allocate(JAVA_CHAR, strLen);
+ var range = CFRange.allocate(arena);
+ CFRange.location(range, 0);
+ CFRange.length(range, strLen);
+ CoreFoundation.CFStringGetCharacters(string, range, buffer);
+ return new String(buffer.toArray(JAVA_CHAR));
}
/**
@@ -52,15 +50,14 @@ public static String stringFromCFStringRef(MemoryAddress string) {
* ownership without incrementing the reference count.
*
*
- * @param string the string
+ * @param string the string
+ * @param allocator the allocator for allocating memory
* @return {@code CFStringRef}
*/
- public static MemoryAddress createCFStringRef(String string) {
- try (var session = MemorySession.openConfined()) {
- char[] charArray = string.toCharArray();
- var chars = session.allocateArray(JAVA_CHAR, charArray.length);
- chars.copyFrom(MemorySegment.ofArray(charArray));
- return CoreFoundation.CFStringCreateWithCharacters(NULL, chars, string.length());
- }
+ static MemorySegment createCFStringRef(String string, SegmentAllocator allocator) {
+ var charArray = string.toCharArray();
+ var chars = allocator.allocate(JAVA_CHAR, charArray.length);
+ chars.copyFrom(MemorySegment.ofArray(charArray));
+ return CoreFoundation.CFStringCreateWithCharacters(NULL, chars, string.length());
}
}
diff --git a/java-does-usb/src/main/java/net/codecrete/usb/macos/IoKitHelper.java b/java-does-usb/src/main/java/net/codecrete/usb/macos/IoKitHelper.java
index 4e03bda3..08d35279 100644
--- a/java-does-usb/src/main/java/net/codecrete/usb/macos/IoKitHelper.java
+++ b/java-does-usb/src/main/java/net/codecrete/usb/macos/IoKitHelper.java
@@ -10,73 +10,125 @@
import net.codecrete.usb.macos.gen.corefoundation.CoreFoundation;
import net.codecrete.usb.macos.gen.iokit.IOKit;
-import java.lang.foreign.Addressable;
-import java.lang.foreign.MemoryAddress;
+import java.lang.foreign.Arena;
+import java.lang.foreign.MemoryLayout;
+import java.lang.foreign.MemoryLayout.PathElement;
import java.lang.foreign.MemorySegment;
-import java.lang.foreign.MemorySession;
+import java.lang.foreign.StructLayout;
+import java.lang.invoke.VarHandle;
-import static java.lang.foreign.MemoryAddress.NULL;
+import static java.lang.foreign.MemorySegment.NULL;
import static java.lang.foreign.ValueLayout.ADDRESS;
import static java.lang.foreign.ValueLayout.JAVA_INT;
+import static net.codecrete.usb.common.ForeignMemory.dereference;
/**
* Constants and helper functions for the IOKit framework.
*/
-public class IoKitHelper {
- public static final MemoryAddress kIOUSBDeviceUserClientTypeID = UUID.CreateCFUUID(
- new byte[]{(byte) 0x9d, (byte) 0xc7, (byte) 0xb7, (byte) 0x80, (byte) 0x9e, (byte) 0xc0, (byte) 0x11,
- (byte) 0xD4, (byte) 0xa5, (byte) 0x4f, (byte) 0x00, (byte) 0x0a, (byte) 0x27, (byte) 0x05,
- (byte) 0x28, (byte) 0x61});
- public static final MemoryAddress kIOUSBInterfaceUserClientTypeID = UUID.CreateCFUUID(
- new byte[]{(byte) 0x2d, (byte) 0x97, (byte) 0x86, (byte) 0xc6, (byte) 0x9e, (byte) 0xf3, (byte) 0x11,
- (byte) 0xD4, (byte) 0xad, (byte) 0x51, (byte) 0x00, (byte) 0x0a, (byte) 0x27, (byte) 0x05,
- (byte) 0x28, (byte) 0x61});
- public static final MemoryAddress kIOUSBDeviceInterfaceID100 = UUID.CreateCFUUID(
- new byte[]{(byte) 0x5c, (byte) 0x81, (byte) 0x87, (byte) 0xd0, (byte) 0x9e, (byte) 0xf3, (byte) 0x11,
- (byte) 0xD4, (byte) 0x8b, (byte) 0x45, (byte) 0x00, (byte) 0x0a, (byte) 0x27, (byte) 0x05,
- (byte) 0x28, (byte) 0x61});
- public static final MemoryAddress kIOUSBInterfaceInterfaceID100 = UUID.CreateCFUUID(
- new byte[]{(byte) 0x73, (byte) 0xc9, (byte) 0x7a, (byte) 0xe8, (byte) 0x9e, (byte) 0xf3, (byte) 0x11,
- (byte) 0xD4, (byte) 0xb1, (byte) 0xd0, (byte) 0x00, (byte) 0x0a, (byte) 0x27, (byte) 0x05,
- (byte) 0x28, (byte) 0x61});
- public static final MemoryAddress kIOCFPlugInInterfaceID = UUID.CreateCFUUID(
- new byte[]{(byte) 0xC2, (byte) 0x44, (byte) 0xE8, (byte) 0x58, (byte) 0x10, (byte) 0x9C, (byte) 0x11,
- (byte) 0xD4, (byte) 0x91, (byte) 0xD4, (byte) 0x00, (byte) 0x50, (byte) 0xE4, (byte) 0xC6,
- (byte) 0x42, (byte) 0x6F});
+class IoKitHelper {
+
+ private IoKitHelper() {
+ }
+
+ static final MemorySegment kIOUSBDeviceUserClientTypeID = UUID.createCFUUID(new byte[]{(byte) 0x9d,
+ (byte) 0xc7, (byte) 0xb7, (byte) 0x80, (byte) 0x9e, (byte) 0xc0, (byte) 0x11, (byte) 0xD4, (byte) 0xa5,
+ (byte) 0x4f, (byte) 0x00, (byte) 0x0a, (byte) 0x27, (byte) 0x05, (byte) 0x28, (byte) 0x61});
+ static final MemorySegment kIOUSBInterfaceUserClientTypeID = UUID.createCFUUID(new byte[]{(byte) 0x2d,
+ (byte) 0x97, (byte) 0x86, (byte) 0xc6, (byte) 0x9e, (byte) 0xf3, (byte) 0x11, (byte) 0xD4, (byte) 0xad,
+ (byte) 0x51, (byte) 0x00, (byte) 0x0a, (byte) 0x27, (byte) 0x05, (byte) 0x28, (byte) 0x61});
+ static final MemorySegment kIOUSBDeviceInterfaceID187 = UUID.createCFUUID(new byte[]{(byte) 0x3c,
+ (byte) 0x9e, (byte) 0xe1, (byte) 0xeb, (byte) 0x24, (byte) 0x02, (byte) 0x11, (byte) 0xb2, (byte) 0x8e,
+ (byte) 0x7e, (byte) 0x00, (byte) 0x0a, (byte) 0x27, (byte) 0x80, (byte) 0x1e, (byte) 0x86});
+ static final MemorySegment kIOUSBInterfaceInterfaceID190 = UUID.createCFUUID(new byte[]{(byte) 0x8f,
+ (byte) 0xdb, (byte) 0x84, (byte) 0x55, (byte) 0x74, (byte) 0xa6, (byte) 0x11, (byte) 0xD6, (byte) 0x97,
+ (byte) 0xb1, (byte) 0x00, (byte) 0x30, (byte) 0x65, (byte) 0xd3, (byte) 0x60, (byte) 0x8e});
+ static final MemorySegment kIOCFPlugInInterfaceID = UUID.createCFUUID(new byte[]{(byte) 0xC2, (byte) 0x44,
+ (byte) 0xE8, (byte) 0x58, (byte) 0x10, (byte) 0x9C, (byte) 0x11, (byte) 0xD4, (byte) 0x91, (byte) 0xD4,
+ (byte) 0x00, (byte) 0x50, (byte) 0xE4, (byte) 0xC6, (byte) 0x42, (byte) 0x6F});
+
+ /**
+ * Layout of COM object.
+ *
+ * Parts of I/O Kit use a plug-in architecture following the Component Object Model (COM).
+ * This layout is the basis for calling object methods (through the vtable) and
+ * accessing the reference count (for debugging purposes).
+ *
+ */
+ static final StructLayout COM_OBJECT =
+ MemoryLayout.structLayout(
+ ADDRESS.withTargetLayout(
+ MemoryLayout.structLayout(
+ // up to 100 function pointers
+ MemoryLayout.sequenceLayout(100, ADDRESS)
+ )
+ ).withName("vtable"),
+ ADDRESS.withTargetLayout(
+ MemoryLayout.structLayout(
+ ADDRESS.withName("unknown"),
+ JAVA_INT.withName("refCount")
+ )
+ ).withName("data")
+ );
+
+ /**
+ * Var handle for accessing the vtable.
+ *
+ * The vtable is an array of function pointers.
+ *
+ */
+ static final VarHandle vtable$VH = COM_OBJECT.varHandle(PathElement.groupElement("vtable"));
/**
- * Get an interface of the specified service.
+ * Var handle for accessing the reference count.
+ */
+ static final VarHandle refCount$VH = COM_OBJECT.varHandle(
+ PathElement.groupElement("data"),
+ PathElement.dereferenceElement(),
+ PathElement.groupElement("refCount")
+ );
+
+ /**
+ * Get the vtable of the specified object instance.
+ * @param self object instance
+ * @return vtable
+ */
+ static MemorySegment getVtable(MemorySegment self) {
+ return (MemorySegment) vtable$VH.get(self, 0);
+ }
+
+ /**
+ * Gets an object instance implementing the specified service.
*
- * This method first request the specified plugin interfaces and then
+ * This method first requests the specified plugin interface and then
* queries for the specified interface.
*
+ * Each USB device and interface must register its event source with this task.
+ *
+ *
+ * The task assigns a consecutive number to transfers. It is used in the
+ * {@code refcon} argument to match callbacks with the submitted transfer.
+ *
+ */
+@SuppressWarnings("java:S6548")
+class MacosAsyncTask {
+
+ enum TaskState {
+ NOT_STARTED,
+ STARTING,
+ RUNNING
+ }
+
+ /**
+ * Singleton instance of background task.
+ */
+ static final MacosAsyncTask INSTANCE = new MacosAsyncTask();
+
+ private static final System.Logger LOG = System.getLogger(MacosAsyncTask.class.getName());
+
+ private final ReentrantLock asyncIoLock = new ReentrantLock();
+ private final Condition asyncIoReady = asyncIoLock.newCondition();
+ private TaskState state = TaskState.NOT_STARTED;
+ private MemorySegment asyncIoRunLoop;
+ private MemorySegment completionUpcallStub;
+ private MemorySegment messagePort;
+ private long lastTransferId;
+ private final Map transfersById = new HashMap<>();
+
+ /**
+ * Adds an event source to this background.
+ *
+ * @param source event source
+ */
+ void addEventSource(MemorySegment source) {
+ try {
+ asyncIoLock.lock();
+
+ if (state != TaskState.RUNNING) {
+ if (state == TaskState.NOT_STARTED)
+ startAsyncIOThread();
+ waitForRunLoopReady();
+ }
+
+ CoreFoundation.CFRunLoopAddSource(asyncIoRunLoop, source, IOKit.kCFRunLoopDefaultMode());
+
+ } finally {
+ asyncIoLock.unlock();
+ }
+ }
+
+ private void waitForRunLoopReady() {
+ while (state != TaskState.RUNNING)
+ asyncIoReady.awaitUninterruptibly();
+ }
+
+ /**
+ * Removes an event source from this background task.
+ *
+ * The event source is not immediately removed. Instead, it is posted to a message queue
+ * processed by the same background thread processing the completion callbacks. This ensures
+ * that the events from releasing interfaces and closing devices are processed.
+ *
+ * @param source event source
+ */
+ void removeEventSource(MemorySegment source) {
+ try (var arena = Arena.ofConfined()) {
+ var eventSourceRef = arena.allocate(JAVA_LONG, 1);
+ eventSourceRef.set(JAVA_LONG, 0, source.address());
+ var dataRef = CoreFoundation.CFDataCreate(NULL, eventSourceRef, eventSourceRef.byteSize());
+ CoreFoundation.CFMessagePortSendRequest(messagePort, 0, dataRef, 0, 0, NULL, NULL);
+ CoreFoundation.CFRelease(dataRef);
+ }
+ }
+
+ /**
+ * Starts the background thread.
+ */
+ @SuppressWarnings("java:S125")
+ private void startAsyncIOThread() {
+ MemorySegment messagePortSource = NULL;
+ MemorySegment localPort = NULL;
+
+ try {
+ state = TaskState.STARTING;
+
+ // create descriptor for completion callback function
+ var completionHandlerFuncDesc = FunctionDescriptor.ofVoid(ADDRESS, JAVA_INT, ADDRESS);
+ var asyncIOCompletedMH = MethodHandles.lookup().findVirtual(MacosAsyncTask.class, "asyncIOCompleted",
+ MethodType.methodType(void.class, MemorySegment.class, int.class, MemorySegment.class));
+ var methodHandle = asyncIOCompletedMH.bindTo(this);
+ completionUpcallStub = Linker.nativeLinker().upcallStub(methodHandle, completionHandlerFuncDesc, Arena.global());
+
+ // create local and remote message ports (all three CF creations can return NULL,
+ // e.g. if bootstrap port registration fails in a sandboxed process)
+ var pid = ProcessHandle.current().pid();
+ var portName = CoreFoundationHelper.createCFStringRef("net.codecrete.usb.macos.eventsource." + pid, Arena.global());
+ var messagePortCallback = CFMessagePortCreateLocal$callout.allocate(this::messagePortCallback, Arena.global());
+ localPort = CoreFoundation.CFMessagePortCreateLocal(NULL, portName, messagePortCallback, NULL, NULL);
+ if (localPort.address() == 0)
+ throw new UsbException("internal error (CFMessagePortCreateLocal failed)");
+ messagePortSource = CoreFoundation.CFMessagePortCreateRunLoopSource(NULL, localPort, 0);
+ if (messagePortSource.address() == 0)
+ throw new UsbException("internal error (CFMessagePortCreateRunLoopSource failed)");
+ var remotePort = CoreFoundation.CFMessagePortCreateRemote(NULL, portName);
+ if (remotePort.address() == 0)
+ throw new UsbException("internal error (CFMessagePortCreateRemote failed)");
+ messagePort = remotePort;
+
+ } catch (Exception e) {
+ // release partially created ports and reset the state; otherwise the task would be
+ // stuck in STARTING and all later addEventSource() calls would wait forever
+ if (messagePortSource.address() != 0)
+ CoreFoundation.CFRelease(messagePortSource);
+ if (localPort.address() != 0)
+ CoreFoundation.CFRelease(localPort);
+ state = TaskState.NOT_STARTED;
+ if (e instanceof RuntimeException runtimeException)
+ throw runtimeException;
+ throw new UsbException("internal error (creating method handle)", e);
+ }
+
+ var source = messagePortSource;
+ var thread = new Thread(() -> asyncIOCompletionTask(source), "USB async IO");
+ thread.setDaemon(true);
+ thread.start();
+ }
+
+ /**
+ * Background task calling the completion handlers.
+ *
+ * Without an initial event source, the run loop will immediately exit.
+ * Later it has no problems if the number of event sources drops to 0.
+ *
+ *
+ * @param firstSource first event source
+ */
+ private void asyncIOCompletionTask(MemorySegment firstSource) {
+ try {
+ asyncIoLock.lock();
+ asyncIoRunLoop = CoreFoundation.CFRunLoopGetCurrent();
+ CoreFoundation.CFRunLoopAddSource(asyncIoRunLoop, firstSource, IOKit.kCFRunLoopDefaultMode());
+ state = TaskState.RUNNING;
+ asyncIoReady.signalAll();
+ } finally {
+ asyncIoLock.unlock();
+ }
+
+ // loop forever
+ CoreFoundation.CFRunLoopRun();
+ LOG.log(WARNING, "unexpected end of CFRunLoopRun");
+ }
+
+ /**
+ * Prepare a transfer for submission by assigning it an ID
+ * and remembering the association to the transfer.
+ *
+ * Each submission needs to be prepared separately.
+ *
+ *
+ * @param transfer transfer
+ */
+ synchronized void prepareForSubmission(MacosTransfer transfer) {
+ lastTransferId += 1;
+ transfer.setId(lastTransferId);
+ transfer.setResultSize(-1);
+ transfersById.put(lastTransferId, transfer);
+ }
+
+ /**
+ * Undoes the registration performed by {@link #prepareForSubmission(MacosTransfer)}.
+ *
+ * Must be called if the native submission of a prepared transfer fails. In that case,
+ * no completion callback will ever fire for the transfer, so its map entry would leak
+ * unless it is removed here.
+ *
+ *
+ * @param transfer transfer whose submission failed
+ */
+ synchronized void submissionFailed(MacosTransfer transfer) {
+ transfersById.remove(transfer.id());
+ }
+
+ /**
+ * Callback function called when an asynchronous transfer has completed.
+ *
+ * @param refcon contains transfer ID
+ * @param result contains result code
+ * @param arg0 contains actual length of transferred data
+ */
+ @SuppressWarnings("java:S1144")
+ private void asyncIOCompleted(MemorySegment refcon, int result, MemorySegment arg0) {
+
+ try {
+ MacosTransfer transfer;
+ synchronized (this) {
+ transfer = transfersById.remove(refcon.address());
+ }
+
+ if (transfer == null) {
+ // A completion for an unknown transfer ID (e.g. a duplicate or spurious
+ // callback). Ignore it rather than dereferencing null.
+ LOG.log(WARNING, "Ignoring async IO completion for unknown transfer ID {0}", refcon.address());
+ return;
+ }
+
+ transfer.setResultCode(result);
+ transfer.setResultSize((int) arg0.address());
+ transfer.completion().completed(transfer);
+
+ } catch (Exception e) {
+ // This method is a native upcall running on the process-wide async IO thread.
+ // Any exception escaping into CFRunLoopRun() would kill that thread and hang
+ // all async transfers for the entire library, so nothing must escape here.
+ LOG.log(ERROR, "Unexpected exception while handling async IO completion", e);
+ }
+ }
+
+ /**
+ * Callback function called when a message is received on the message port.
+ *
+ * All messages are related to removing event sources. They just contain the run loop source reference.
+ *
+ */
+ @SuppressWarnings({"java:S1144", "unused"})
+ private MemorySegment messagePortCallback(MemorySegment local, int msgid, MemorySegment data, MemorySegment info) {
+ var runloopSourceRefPtr = CoreFoundation.CFDataGetBytePtr(data);
+ var runloopSourceRef = MemorySegment.ofAddress(runloopSourceRefPtr.get(JAVA_LONG_UNALIGNED, 0));
+ CoreFoundation.CFRunLoopRemoveSource(asyncIoRunLoop, runloopSourceRef, IOKit.kCFRunLoopDefaultMode());
+ return NULL;
+ }
+
+ /**
+ * Gets the native IO completion callback function for asynchronous transfers
+ * to be handled by this background task.
+ *
+ * @return function pointer
+ */
+ MemorySegment nativeCompletionCallback() {
+ return completionUpcallStub;
+ }
+}
diff --git a/java-does-usb/src/main/java/net/codecrete/usb/macos/MacosEndpointInputStream.java b/java-does-usb/src/main/java/net/codecrete/usb/macos/MacosEndpointInputStream.java
new file mode 100644
index 00000000..06dff333
--- /dev/null
+++ b/java-does-usb/src/main/java/net/codecrete/usb/macos/MacosEndpointInputStream.java
@@ -0,0 +1,23 @@
+//
+// Java Does USB
+// Copyright (c) 2023 Manuel Bleichenbacher
+// Licensed under MIT License
+// https://opensource.org/licenses/MIT
+//
+
+package net.codecrete.usb.macos;
+
+import net.codecrete.usb.common.EndpointInputStream;
+import net.codecrete.usb.common.Transfer;
+
+public class MacosEndpointInputStream extends EndpointInputStream {
+
+ MacosEndpointInputStream(MacosUsbDevice device, int endpointNumber, int bufferSize) {
+ super(device, endpointNumber, bufferSize);
+ }
+
+ @Override
+ protected void submitTransferIn(Transfer transfer) {
+ ((MacosUsbDevice) device).submitTransferIn(endpointNumber, (MacosTransfer) transfer, 0);
+ }
+}
diff --git a/java-does-usb/src/main/java/net/codecrete/usb/macos/MacosEndpointOutputStream.java b/java-does-usb/src/main/java/net/codecrete/usb/macos/MacosEndpointOutputStream.java
new file mode 100644
index 00000000..898ab5e1
--- /dev/null
+++ b/java-does-usb/src/main/java/net/codecrete/usb/macos/MacosEndpointOutputStream.java
@@ -0,0 +1,23 @@
+//
+// Java Does USB
+// Copyright (c) 2023 Manuel Bleichenbacher
+// Licensed under MIT License
+// https://opensource.org/licenses/MIT
+//
+
+package net.codecrete.usb.macos;
+
+import net.codecrete.usb.common.EndpointOutputStream;
+import net.codecrete.usb.common.Transfer;
+
+public class MacosEndpointOutputStream extends EndpointOutputStream {
+
+ MacosEndpointOutputStream(MacosUsbDevice device, int endpointNumber, int bufferSize) {
+ super(device, endpointNumber, bufferSize);
+ }
+
+ @Override
+ protected void submitTransferOut(Transfer request) {
+ ((MacosUsbDevice) device).submitTransferOut(endpointNumber, (MacosTransfer) request, 0);
+ }
+}
diff --git a/java-does-usb/src/main/java/net/codecrete/usb/macos/MacosTransfer.java b/java-does-usb/src/main/java/net/codecrete/usb/macos/MacosTransfer.java
new file mode 100644
index 00000000..6610234d
--- /dev/null
+++ b/java-does-usb/src/main/java/net/codecrete/usb/macos/MacosTransfer.java
@@ -0,0 +1,22 @@
+//
+// Java Does USB
+// Copyright (c) 2023 Manuel Bleichenbacher
+// Licensed under MIT License
+// https://opensource.org/licenses/MIT
+//
+
+package net.codecrete.usb.macos;
+
+import net.codecrete.usb.common.Transfer;
+
+class MacosTransfer extends Transfer {
+ private long id;
+
+ public long id() {
+ return id;
+ }
+
+ public void setId(long id) {
+ this.id = id;
+ }
+}
diff --git a/java-does-usb/src/main/java/net/codecrete/usb/macos/MacosUSBDevice.java b/java-does-usb/src/main/java/net/codecrete/usb/macos/MacosUSBDevice.java
deleted file mode 100644
index af2b69fb..00000000
--- a/java-does-usb/src/main/java/net/codecrete/usb/macos/MacosUSBDevice.java
+++ /dev/null
@@ -1,373 +0,0 @@
-//
-// Java Does USB
-// Copyright (c) 2022 Manuel Bleichenbacher
-// Licensed under MIT License
-// https://opensource.org/licenses/MIT
-//
-
-package net.codecrete.usb.macos;
-
-import net.codecrete.usb.USBControlTransfer;
-import net.codecrete.usb.USBDirection;
-import net.codecrete.usb.USBException;
-import net.codecrete.usb.USBTransferType;
-import net.codecrete.usb.common.DescriptorParser;
-import net.codecrete.usb.common.USBDescriptors;
-import net.codecrete.usb.common.USBDeviceImpl;
-import net.codecrete.usb.macos.gen.iokit.IOKit;
-import net.codecrete.usb.macos.gen.iokit.IOUSBDevRequest;
-import net.codecrete.usb.macos.gen.iokit.IOUSBFindInterfaceRequest;
-
-import java.lang.foreign.MemoryAddress;
-import java.lang.foreign.MemorySegment;
-import java.lang.foreign.MemorySession;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
-import static java.lang.foreign.MemoryAddress.ofLong;
-import static java.lang.foreign.ValueLayout.*;
-
-public class MacosUSBDevice extends USBDeviceImpl {
-
- private final MemoryAddress device;
- private int configurationValue;
- private List claimedInterfaces;
- private Map endpoints;
-
- MacosUSBDevice(MemoryAddress device, Object id, int vendorId, int productId, String manufacturer, String product, String serial) {
- super(id, vendorId, productId, manufacturer, product, serial);
- this.device = device;
-
- loadDescription();
-
- IoKitUSB.AddRef(device);
- }
-
- @Override
- public boolean isOpen() {
- return claimedInterfaces != null;
- }
-
- @Override
- public void open() {
- if (isOpen())
- throw new USBException("the device is already open");
-
- // open device
- int ret = IoKitUSB.USBDeviceOpen(device);
- if (ret != 0)
- throw new MacosUSBException("unable to open USB device", ret);
-
- // set configuration
- ret = IoKitUSB.SetConfiguration(device, (byte) configurationValue);
- if (ret != 0)
- throw new MacosUSBException("failed to set configuration", ret);
-
- claimedInterfaces = new ArrayList<>();
- updateEndpointList();
- }
-
- @Override
- public void close() {
- if (!isOpen())
- return;
-
- for (InterfaceInfo interfaceInfo : claimedInterfaces) {
- IoKitUSB.USBInterfaceClose(interfaceInfo.asAddress());
- IoKitUSB.Release(interfaceInfo.asAddress());
- setClaimed(interfaceInfo.interfaceNumber, false);
- }
-
- claimedInterfaces = null;
- endpoints = null;
- IoKitUSB.USBDeviceClose(device);
- }
-
- void closeFully() {
- close();
- IoKitUSB.Release(device);
- }
-
- private void loadDescription() {
- try (var session = MemorySession.openConfined()) {
-
- try {
- // retrieve information of first configuration
- var descPtrHolder = session.allocate(ADDRESS);
- int ret = IoKitUSB.GetConfigurationDescriptorPtr(device, (byte) 0, descPtrHolder.address());
- if (ret != 0)
- throw new MacosUSBException("failed to query first configuration", ret);
-
- // get value of first configuration
- var configDescHeader = MemorySegment.ofAddress(descPtrHolder.get(ADDRESS, 0),
- USBDescriptors.Configuration.byteSize(), session);
- int totalLength = (short) USBDescriptors.Configuration_wTotalLength.get(configDescHeader);
- var configDesc = MemorySegment.ofAddress(descPtrHolder.get(ADDRESS, 0),
- totalLength, session);
-
- var configuration = DescriptorParser.parseConfigurationDescriptor(configDesc, vendorId(), productId());
-
- configurationValue = 255 & configuration.configValue;
- setInterfaces(configuration.interfaces);
-
- } catch (Throwable e) {
- configurationValue = 0;
- throw e;
- }
- }
- }
-
- private InterfaceInfo findInterface(int interfaceNumber) {
-
- try (var outerSession = MemorySession.openConfined()) {
- var request = outerSession.allocate(IOUSBFindInterfaceRequest.$LAYOUT());
- IOUSBFindInterfaceRequest.bInterfaceClass$set(request, (short) IOKit.kIOUSBFindInterfaceDontCare());
- IOUSBFindInterfaceRequest.bInterfaceSubClass$set(request, (short) IOKit.kIOUSBFindInterfaceDontCare());
- IOUSBFindInterfaceRequest.bInterfaceProtocol$set(request, (short) IOKit.kIOUSBFindInterfaceDontCare());
- IOUSBFindInterfaceRequest.bAlternateSetting$set(request, (short) IOKit.kIOUSBFindInterfaceDontCare());
-
- var iterHolder = outerSession.allocate(JAVA_INT);
- int ret = IoKitUSB.CreateInterfaceIterator(device, request.address(), iterHolder.address());
- final var iter = iterHolder.get(JAVA_INT, 0);
- if (ret != 0) throw new RuntimeException("CreateInterfaceIterator failed");
- outerSession.addCloseAction(() -> IOKit.IOObjectRelease(iter));
-
- int service;
- while ((service = IOKit.IOIteratorNext(iter)) != 0) {
- try (var session = MemorySession.openConfined()) {
-
- final int service_final = service;
- session.addCloseAction(() -> IOKit.IOObjectRelease(service_final));
-
- final MemoryAddress intf = IoKitHelper.getInterface(service,
- IoKitHelper.kIOUSBInterfaceUserClientTypeID, IoKitHelper.kIOUSBInterfaceInterfaceID100);
- if (intf == null) continue;
-
- var intfNumberHolder = session.allocate(JAVA_INT);
- IoKitUSB.GetInterfaceNumber(intf, intfNumberHolder.address());
- if (intfNumberHolder.get(JAVA_INT, 0) != interfaceNumber) {
- IoKitUSB.Release(intf);
- continue;
- }
-
- return new InterfaceInfo(intf.toRawLongValue(), interfaceNumber);
- }
- }
- }
-
- throw new MacosUSBException(String.format("Invalid interface number: %d", interfaceNumber));
- }
-
- public void claimInterface(int interfaceNumber) {
- checkIsOpen();
-
- var interfaceInfo = findInterface(interfaceNumber);
-
- try {
- var ret = IoKitUSB.USBInterfaceOpen(interfaceInfo.asAddress());
- if (ret != 0)
- throw new MacosUSBException("Failed to claim interface", ret);
- setClaimed(interfaceNumber, true);
-
- } catch (Throwable t) {
- IoKitUSB.Release(interfaceInfo.asAddress());
- throw t;
- }
-
- claimedInterfaces.add(interfaceInfo);
-
- updateEndpointList();
- }
-
- public void releaseInterface(int interfaceNumber) {
- checkIsOpen();
-
- var interfaceInfoOptional =
- claimedInterfaces.stream().filter(info -> info.interfaceNumber == interfaceNumber).findFirst();
- if (interfaceInfoOptional.isEmpty())
- throw new MacosUSBException(String.format("Invalid interface number: %d", interfaceNumber));
-
- var interfaceInfo = interfaceInfoOptional.get();
-
- int ret = IoKitUSB.USBInterfaceClose(interfaceInfo.asAddress());
- if (ret != 0) throw new MacosUSBException("Failed to release interface", ret);
-
- claimedInterfaces.remove(interfaceInfo);
- IoKitUSB.Release(interfaceInfo.asAddress());
- setClaimed(interfaceNumber, false);
-
- updateEndpointList();
- }
-
- /**
- * Update the map of active endpoints.
- *
- * MacOS uses a pipe index to refer to endpoints. This method
- * builds a map from endpoint address to pipe index.
- *
- */
- private void updateEndpointList() {
- endpoints = new HashMap<>();
-
- for (InterfaceInfo interfaceInfo : claimedInterfaces) {
- try (var session = MemorySession.openConfined()) {
-
- var intf = interfaceInfo.asAddress();
- var numEndpointsHolder = session.allocate(JAVA_BYTE);
- int ret = IoKitUSB.GetNumEndpoints(intf, numEndpointsHolder.address());
- if (ret != 0)
- throw new MacosUSBException("Failed to get number of endpoints", ret);
- int numEndpoints = numEndpointsHolder.get(JAVA_BYTE, 0) & 255;
-
- for (int pipeIndex = 1; pipeIndex <= numEndpoints; pipeIndex++) {
-
- var directionHolder = session.allocate(JAVA_BYTE);
- var numberHolder = session.allocate(JAVA_BYTE);
- var transferTypeHolder = session.allocate(JAVA_BYTE);
- var maxPacketSizeHolder = session.allocate(JAVA_SHORT);
- var intervalHolder = session.allocate(JAVA_BYTE);
-
- ret = IoKitUSB.GetPipeProperties(intf, (byte) pipeIndex, directionHolder.address(),
- numberHolder.address(), transferTypeHolder.address(), maxPacketSizeHolder.address(),
- intervalHolder.address());
- if (ret != 0) throw new MacosUSBException("Failed to get pipe properties", ret);
-
- int endpointNumber = numberHolder.get(JAVA_BYTE, 0) & 255;
- int direction = directionHolder.get(JAVA_BYTE, 0) & 255;
- byte endpointAddress = (byte) (endpointNumber | (direction << 7));
- byte transferType = transferTypeHolder.get(JAVA_BYTE, 0);
- var endpointInfo = new EndpointInfo(interfaceInfo.addr, (byte) pipeIndex, getTransferType(transferType));
- endpoints.put(endpointAddress, endpointInfo);
- }
- }
- }
- }
-
- private EndpointInfo getEndpointInfo(int endpointNumber, USBDirection direction,
- USBTransferType transferType1, USBTransferType transferType2) {
- if (endpoints != null) {
- byte endpointAddress = (byte) (endpointNumber | (direction == USBDirection.IN ? 0x80 : 0));
- var endpointInfo = endpoints.get(endpointAddress);
- if (endpointInfo != null
- && (endpointInfo.transferType == transferType1 || endpointInfo.transferType == transferType2))
- return endpointInfo;
- }
-
- String transferTypeDesc;
- if (transferType2 == null)
- transferTypeDesc = transferType1.name();
- else
- transferTypeDesc = String.format("%s or %s", transferType1.name(), transferType2.name());
- throw new USBException(String.format("Endpoint number %d does not exist, is not part of a claimed interface " +
- "or is not valid for %s transfer in %s direction", endpointNumber, transferTypeDesc,
- direction.name()));
- }
-
- private static MemorySegment createDeviceRequest(MemorySession session, USBDirection direction,
- USBControlTransfer setup, MemorySegment data) {
- var deviceRequest = session.allocate(IOUSBDevRequest.$LAYOUT());
- var bmRequestType =
- (direction == USBDirection.IN ? 0x80 : 0x00) | (setup.requestType().ordinal() << 5) | setup.recipient().ordinal();
- IOUSBDevRequest.bmRequestType$set(deviceRequest, (byte) bmRequestType);
- IOUSBDevRequest.bRequest$set(deviceRequest, setup.request());
- IOUSBDevRequest.wValue$set(deviceRequest, setup.value());
- IOUSBDevRequest.wIndex$set(deviceRequest, setup.index());
- IOUSBDevRequest.wLength$set(deviceRequest, (short) data.byteSize());
- IOUSBDevRequest.pData$set(deviceRequest, data.address());
- return deviceRequest;
- }
-
- @Override
- public byte[] controlTransferIn(USBControlTransfer setup, int length) {
- checkIsOpen();
-
- try (var session = MemorySession.openConfined()) {
- var data = session.allocate(length);
- var deviceRequest = createDeviceRequest(session, USBDirection.IN, setup, data);
-
- int ret = IoKitUSB.DeviceRequest(device, deviceRequest.address());
- if (ret != 0) throw new MacosUSBException("Control IN transfer failed", ret);
-
- int lenDone = IOUSBDevRequest.wLenDone$get(deviceRequest);
- return data.asSlice(0, lenDone).toArray(JAVA_BYTE);
- }
- }
-
- @Override
- public void controlTransferOut(USBControlTransfer setup, byte[] data) {
- checkIsOpen();
-
- try (var session = MemorySession.openConfined()) {
- int dataLength = data != null ? data.length : 0;
- var dataSegment = session.allocate(dataLength);
- if (dataLength > 0) dataSegment.copyFrom(MemorySegment.ofArray(data));
- var deviceRequest = createDeviceRequest(session, USBDirection.OUT, setup, dataSegment);
-
- int ret = IoKitUSB.DeviceRequest(device, deviceRequest.address());
- if (ret != 0) throw new MacosUSBException("Control IN transfer failed", ret);
- }
- }
-
- @Override
- public void transferOut(int endpointNumber, byte[] data) {
-
- var endpointInfo = getEndpointInfo(endpointNumber, USBDirection.OUT,
- USBTransferType.BULK, USBTransferType.INTERRUPT);
-
- try (var session = MemorySession.openConfined()) {
- var nativeData = session.allocateArray(JAVA_BYTE, data.length);
- nativeData.copyFrom(MemorySegment.ofArray(data));
- int ret = IoKitUSB.WritePipe(endpointInfo.interfacAddress(), endpointInfo.pipeIndex,
- nativeData.address(), data.length);
- if (ret != 0)
- throw new MacosUSBException(String.format("Sending data to endpoint %d failed", endpointNumber), ret);
- }
- }
-
- @Override
- public byte[] transferIn(int endpointNumber, int maxLength) {
-
- var endpointInfo = getEndpointInfo(endpointNumber, USBDirection.IN,
- USBTransferType.BULK, USBTransferType.INTERRUPT);
-
- try (var session = MemorySession.openConfined()) {
- var nativeData = session.allocateArray(JAVA_BYTE, maxLength);
- var sizeHolder = session.allocate(JAVA_INT, maxLength);
- int ret = IoKitUSB.ReadPipe(endpointInfo.interfacAddress(), endpointInfo.pipeIndex,
- nativeData.address(), sizeHolder.address());
- if (ret != 0)
- throw new MacosUSBException(String.format("Receiving data from endpoint %d failed", endpointNumber),
- ret);
-
- int size = sizeHolder.get(JAVA_INT, 0);
- var result = new byte[size];
- var resultSegment = MemorySegment.ofArray(result);
- resultSegment.copyFrom(nativeData.asSlice(0, size));
-
- return result;
- }
- }
-
- private static USBTransferType getTransferType(byte macosTransferType) {
- return switch (macosTransferType) {
- case 1 -> USBTransferType.ISOCHRONOUS;
- case 2 -> USBTransferType.BULK;
- case 3 -> USBTransferType.INTERRUPT;
- default -> null;
- };
- }
-
- record InterfaceInfo(long addr, int interfaceNumber) {
- MemoryAddress asAddress() {
- return ofLong(addr);
- }
- }
-
- record EndpointInfo(long interfaceAddr, byte pipeIndex, USBTransferType transferType) {
- MemoryAddress interfacAddress() {
- return ofLong(interfaceAddr);
- }
- }
-}
diff --git a/java-does-usb/src/main/java/net/codecrete/usb/macos/MacosUSBDeviceRegistry.java b/java-does-usb/src/main/java/net/codecrete/usb/macos/MacosUSBDeviceRegistry.java
deleted file mode 100644
index dc8b5d75..00000000
--- a/java-does-usb/src/main/java/net/codecrete/usb/macos/MacosUSBDeviceRegistry.java
+++ /dev/null
@@ -1,247 +0,0 @@
-//
-// Java Does USB
-// Copyright (c) 2022 Manuel Bleichenbacher
-// Licensed under MIT License
-// https://opensource.org/licenses/MIT
-//
-
-package net.codecrete.usb.macos;
-
-import net.codecrete.usb.USBDevice;
-import net.codecrete.usb.common.USBDeviceRegistry;
-import net.codecrete.usb.macos.gen.corefoundation.CoreFoundation;
-import net.codecrete.usb.macos.gen.iokit.IOKit;
-
-import java.lang.foreign.*;
-import java.lang.invoke.MethodHandle;
-import java.lang.invoke.MethodHandles;
-import java.lang.invoke.MethodType;
-import java.util.ArrayList;
-import java.util.function.Consumer;
-
-import static java.lang.foreign.MemoryAddress.NULL;
-import static java.lang.foreign.ValueLayout.*;
-
-/**
- * MacOS implementation of USB device registry.
- */
-public class MacosUSBDeviceRegistry extends USBDeviceRegistry {
-
- /**
- * Monitors the USB devices.
- *
- * This method is the core of the background thread. It runs forever and does not terminate.
- *
- */
- @Override
- protected void monitorDevices() {
-
- // as the method runs forever, there is no need to clean up one-time allocations
- try (var session = MemorySession.openConfined()) {
-
- try {
-
- // setup run loop, run loop source and notification port
- var notifyPort = IOKit.IONotificationPortCreate(IOKit.kIOMasterPortDefault$get());
- var runLoopSource = IOKit.IONotificationPortGetRunLoopSource(notifyPort);
- var runLoop = CoreFoundation.CFRunLoopGetCurrent();
- CoreFoundation.CFRunLoopAddSource(runLoop, runLoopSource, IOKit.kCFRunLoopDefaultMode$get());
-
- // setup notification for connected devices
- var onDeviceConnectedMH = MethodHandles.lookup().findVirtual(MacosUSBDeviceRegistry.class,
- "onDevicesConnected", MethodType.methodType(void.class, MemoryAddress.class, int.class));
- int deviceConnectedIter = setupNotification(session, notifyPort, IOKit.kIOFirstMatchNotification(),
- onDeviceConnectedMH);
-
- // iterate current devices in order to arm the notifications (and build initial device list)
- var deviceList = new ArrayList();
- iterateDevices(deviceConnectedIter, (device) -> deviceList.add(device));
- setInitialDeviceList(deviceList);
-
- // setup notification for disconnected devices
- var onDeviceDisconnectedMH = MethodHandles.lookup().findVirtual(MacosUSBDeviceRegistry.class,
- "onDevicesDisconnected", MethodType.methodType(void.class, MemoryAddress.class, int.class));
- int deviceDisconnectedIter = setupNotification(session, notifyPort, IOKit.kIOTerminatedNotification(),
- onDeviceDisconnectedMH);
-
- // iterate current devices in order to arm the notifications
- onDevicesDisconnected(NULL, deviceDisconnectedIter);
-
- } catch (Throwable e) {
- enumerationFailed(e);
- return;
- }
-
- // loop forever
- CoreFoundation.CFRunLoopRun();
- }
- }
-
- /**
- * Process the devices resulting from the iterator
- *
- * @param iterator the iterator
- * @param consumer a consumer that will be called for each device with the entry ID and the service
- */
- private void iterateDevices(int iterator, IOKitDeviceConsumer consumer) {
-
- int svc;
- while ((svc = IOKit.IOIteratorNext(iterator)) != 0) {
- try (var session = MemorySession.openConfined()) {
-
- final int service = svc;
- session.addCloseAction(() -> IOKit.IOObjectRelease(service));
-
- var device = IoKitHelper.getInterface(service, IoKitHelper.kIOUSBDeviceUserClientTypeID,
- IoKitHelper.kIOUSBDeviceInterfaceID100);
-
- if (device != null)
- session.addCloseAction(() -> IoKitUSB.Release(device));
-
- // get entry ID (as unique ID)
- var entryIdHolder = session.allocate(JAVA_LONG);
- int ret = IOKit.IORegistryEntryGetRegistryEntryID(service, entryIdHolder);
- if (ret != 0)
- throw new MacosUSBException("IORegistryEntryGetRegistryEntryID failed", ret);
- var entryId = entryIdHolder.get(JAVA_LONG, 0);
-
- // call consumer to process device
- consumer.accept(entryId, service, device);
- }
- }
- }
-
- /**
- * Calls the consumer for all devices produced by the iterator.
- *
- * This method tries to create a {@link USBDevice} instance.
- * If it fails, an information is printed, but the consumer is not called.
- *
- *
- * @param iterator the iterator
- * @param consumer the consumer
- */
- private void iterateDevices(int iterator, Consumer consumer) {
- iterateDevices(iterator, (entryId, service, deviceIntf) -> {
-
- var deviceInfo = new VidPid();
- try {
- var device = createDevice(entryId, service, deviceIntf, deviceInfo);
- if (device != null)
- consumer.accept(device);
-
- } catch (Throwable e) {
- System.err.printf("Info: [JavaDoesUSB] failed to retrieve information about device 0x%04x/0x%04x - " + "ignoring device%n", deviceInfo.vid, deviceInfo.pid);
- e.printStackTrace(System.err);
- }
- });
- }
-
- private USBDevice createDevice(Long entryID, int service, MemoryAddress deviceIntf, VidPid info) {
-
- if (deviceIntf == null)
- return null;
-
- Integer vendorId = IoKitHelper.getPropertyInt(service, "idVendor");
- Integer productId = IoKitHelper.getPropertyInt(service, "idProduct");
- if (vendorId == null || productId == null)
- return null;
-
- info.vid = vendorId;
- info.pid = productId;
-
- String manufacturer = IoKitHelper.getPropertyString(service, "kUSBVendorString");
- String product = IoKitHelper.getPropertyString(service, "kUSBProductString");
- String serial = IoKitHelper.getPropertyString(service, "kUSBSerialNumberString");
-
- var device = new MacosUSBDevice(deviceIntf, entryID, vendorId, productId, manufacturer, product, serial);
-
- Integer classCode = IoKitHelper.getPropertyInt(service, "bDeviceClass");
- Integer subclassCode = IoKitHelper.getPropertyInt(service, "bDeviceSubClass");
- Integer protocolCode = IoKitHelper.getPropertyInt(service, "bDeviceProtocol");
-
- device.setClassCodes(classCode != null ? classCode : 0, subclassCode != null ? subclassCode : 0,
- protocolCode != null ? protocolCode : 0);
-
- Integer usbVersion = IoKitHelper.getPropertyInt(service, "bcdUSB");
- Integer deviceVersion = IoKitHelper.getPropertyInt(service, "bcdDevice");
- //noinspection ConstantConditions
- device.setVersions(usbVersion, deviceVersion);
-
- return device;
- }
-
- private int setupNotification(MemorySession session, MemoryAddress notifyPort, MemorySegment notificationType,
- MethodHandle callback) {
-
- // new matching dictionary for (dis)connected device notifications
- MemoryAddress matchingDict = IOKit.IOServiceMatching(IOKit.kIOUSBDeviceClassName());
-
- // create callback stub
- var onDeviceCallbackStub = Linker.nativeLinker().upcallStub(callback.bindTo(this),
- FunctionDescriptor.ofVoid(ADDRESS, JAVA_INT), session);
-
- // Set up a notification to be called when a device is first matched / terminated by I/O Kit.
- // This method consumes the matchingDict reference.
- var deviceIterHolder = session.allocate(JAVA_INT);
- int ret = IOKit.IOServiceAddMatchingNotification(notifyPort, notificationType, matchingDict,
- onDeviceCallbackStub, NULL, deviceIterHolder);
- if (ret != 0)
- throw new MacosUSBException("IOServiceAddMatchingNotification failed", ret);
-
- return deviceIterHolder.get(JAVA_INT, 0);
- }
-
- /**
- * Callback function for monitoring connected USB devices.
- *
- * This method is used in an upcall from native code.
- *
- *
- * @param ignoredRefCon ignored parameter
- * @param iterator device iterator
- */
- private void onDevicesConnected(MemoryAddress ignoredRefCon, int iterator) {
-
- // process device iterator for connected devices
- iterateDevices(iterator, this::addDevice);
- }
-
- /**
- * Callback function for monitoring disconnected USB devices.
- *
- * This method is used in an upcall from native code.
- *
+ * All read and write operations on endpoints are submitted through synchronized methods in order to control
+ * concurrency. If it wasn't controlled, the danger is that device and interface pointers are used, which have
+ * just been closed and thus deallocated by another thread, likely leading to crashes.
+ *
+ *
+ * As a consequence of the synchronized submission, blocking operations consists of submitting an
+ * asynchronous transfer and waiting for the completion.
+ *
+ */
+@SuppressWarnings({"SynchronizationOnLocalVariableOrMethodParameter", "java:S2160", "java:S3077"})
+public class MacosUsbDevice extends UsbDeviceImpl {
+
+ private final MacosAsyncTask asyncTask;
+ // Native USB device interface (IOUSBDeviceInterface**)
+ private MemorySegment device;
+ // Currently selected configuration
+ private int configurationValue;
+ // Details about interfaces that have been claimed
+ // (volatile: written under the device monitor, read unlocked via isOpened();
+ // the list contents are only accessed while holding the device monitor)
+ private volatile List claimedInterfaces;
+ // Details about endpoints of current alternate settings (for claimed interfaces)
+ private Map endpoints;
+
+ private final long discoveryTime;
+
+ MacosUsbDevice(MemorySegment device, Object id, int vendorId, int productId) {
+ super(id, vendorId, productId);
+ discoveryTime = System.currentTimeMillis();
+ asyncTask = MacosAsyncTask.INSTANCE;
+
+ loadDescription(device);
+
+ this.device = device;
+ IoKitUsb.AddRef(device);
+ }
+
+ @Override
+ public synchronized void detachStandardDrivers() {
+ checkIsClosed("detachStandardDrivers() must not be called while the device is open");
+ var ret = IoKitUsb.USBDeviceReEnumerate(device, IOKit.kUSBReEnumerateCaptureDeviceMask());
+ if (ret != 0)
+ throwException(ret, "detaching standard drivers failed");
+ }
+
+ @Override
+ public synchronized void attachStandardDrivers() {
+ checkIsClosed("attachStandardDrivers() must not be called while the device is open");
+ var ret = IoKitUsb.USBDeviceReEnumerate(device, IOKit.kUSBReEnumerateReleaseDeviceMask());
+ if (ret != 0)
+ throwException(ret, "attaching standard drivers failed");
+ }
+
+ @Override
+ public boolean isOpened() {
+ return claimedInterfaces != null;
+ }
+
+ @SuppressWarnings({"java:S2276", "java:S2142"})
+ @Override
+ public synchronized void open() {
+ checkIsClosed("device is already open");
+
+ // open device (several retries if device has just been connected/discovered)
+ var duration = System.currentTimeMillis() - discoveryTime;
+ var numTries = duration < 1000 ? 4 : 1;
+ var ret = 0;
+ // Defer interruption: keep a local flag instead of re-asserting the interrupt
+ // (which would make the remaining backoff sleeps throw immediately and defeat
+ // the retry delay). Re-assert once the retries are done.
+ var wasInterrupted = false;
+ while (numTries > 0) {
+ numTries -= 1;
+ ret = IoKitUsb.USBDeviceOpenSeize(device);
+ if (ret != IOKit.kIOReturnExclusiveAccess())
+ break;
+
+ // sleep and retry
+ try {
+ Thread.sleep(90);
+ } catch (InterruptedException _) {
+ wasInterrupted = true;
+ }
+ }
+ if (wasInterrupted)
+ Thread.currentThread().interrupt();
+ if (ret != 0)
+ throwException(ret, "opening USB device failed");
+
+ claimedInterfaces = new ArrayList<>();
+ addDeviceEventSource();
+
+ // set configuration
+ ret = IoKitUsb.SetConfiguration(device, (byte) configurationValue);
+ if (ret != 0)
+ throwException(ret, "setting configuration failed");
+
+ updateEndpointList();
+ }
+
+ @Override
+ public synchronized void close() {
+ if (!isOpened())
+ return;
+
+ for (var interfaceInfo : claimedInterfaces) {
+ setClaimed(interfaceInfo.interfaceNumber, false);
+ var source = IoKitUsb.GetInterfaceAsyncEventSource(interfaceInfo.iokitInterface());
+ IoKitUsb.USBInterfaceClose(interfaceInfo.iokitInterface);
+ IoKitUsb.Release(interfaceInfo.iokitInterface);
+ if (source.address() != 0)
+ asyncTask.removeEventSource(source);
+ }
+
+ claimedInterfaces = null;
+ endpoints = null;
+
+ var source = IoKitUsb.GetDeviceAsyncEventSource(device);
+ IoKitUsb.USBDeviceClose(device);
+ if (source.address() != 0)
+ asyncTask.removeEventSource(source);
+ }
+
+ @Override
+ protected synchronized void disconnect() {
+ super.disconnect();
+ IoKitUsb.Release(device);
+ device = null;
+ }
+
+ private void loadDescription(MemorySegment device) {
+ try (var arena = Arena.ofConfined()) {
+
+ // retrieve device descriptor using synchronous control transfer
+ var data = arena.allocate(255);
+ var deviceRequest = createDeviceRequest(arena, UsbDirection.IN, new UsbControlTransfer(
+ UsbRequestType.STANDARD,
+ UsbRecipient.DEVICE,
+ 6, // get descriptor
+ Constants.DEVICE_DESCRIPTOR_TYPE << 8,
+ 0
+ ), data);
+ var ret = IoKitUsb.DeviceRequest(device, deviceRequest);
+ if (ret != 0)
+ throwException(ret, "querying device descriptor failed");
+
+ var len = IOUSBDevRequest.wLenDone(deviceRequest);
+ rawDeviceDescriptor = data.asSlice(0, len).toArray(JAVA_BYTE);
+
+ configurationValue = 0;
+
+ // retrieve information of first configuration
+ var descPtrHolder = arena.allocate(ADDRESS);
+ ret = IoKitUsb.GetConfigurationDescriptorPtr(device, (byte) 0, descPtrHolder);
+ if (ret != 0)
+ throwException(ret, "querying first configuration failed");
+
+ // read the descriptor header with a minimally sized view first, then resize to the
+ // total length the header reports (the kernel buffer was sized from the same field)
+ var headerSize = ConfigurationDescriptor.LAYOUT.byteSize();
+ var configDescHeader = new ConfigurationDescriptor(dereference(descPtrHolder).reinterpret(headerSize));
+ var totalLength = configDescHeader.totalLength();
+ if (totalLength < headerSize)
+ throwException("invalid configuration descriptor (wTotalLength: %d)", totalLength);
+ var configDesc = dereference(descPtrHolder).reinterpret(totalLength);
+
+ var configuration = setConfigurationDescriptor(configDesc);
+ configurationValue = 255 & configuration.configValue();
+ }
+ }
+
+ @SuppressWarnings("java:S135")
+ private InterfaceInfo findInterfaceInfo(int interfaceNumber) {
+
+ try (var arena = Arena.ofConfined(); var outerCleanup = new ScopeCleanup()) {
+ var request = IOUSBFindInterfaceRequest.allocate(arena);
+ IOUSBFindInterfaceRequest.bInterfaceClass(request, (short) IOKit.kIOUSBFindInterfaceDontCare());
+ IOUSBFindInterfaceRequest.bInterfaceSubClass(request, (short) IOKit.kIOUSBFindInterfaceDontCare());
+ IOUSBFindInterfaceRequest.bInterfaceProtocol(request, (short) IOKit.kIOUSBFindInterfaceDontCare());
+ IOUSBFindInterfaceRequest.bAlternateSetting(request, (short) IOKit.kIOUSBFindInterfaceDontCare());
+
+ var iterHolder = arena.allocate(JAVA_INT);
+ var ret = IoKitUsb.CreateInterfaceIterator(device, request, iterHolder);
+ if (ret != 0)
+ throwException("internal error (CreateInterfaceIterator)");
+
+ final var iter = iterHolder.get(JAVA_INT, 0);
+ outerCleanup.add(() -> IOKit.IOObjectRelease(iter));
+
+ var intfNumberHolder = arena.allocate(JAVA_INT);
+
+ int service;
+ while ((service = IOKit.IOIteratorNext(iter)) != 0) {
+ try (var cleanup = new ScopeCleanup()) {
+
+ final var service_final = service;
+ cleanup.add(() -> IOKit.IOObjectRelease(service_final));
+
+ final var intf = IoKitHelper.getInterface(service, IoKitHelper.kIOUSBInterfaceUserClientTypeID,
+ IoKitHelper.kIOUSBInterfaceInterfaceID190);
+ if (intf == null)
+ continue;
+
+ cleanup.add(() -> IoKitUsb.Release(intf));
+
+ IoKitUsb.GetInterfaceNumber(intf, intfNumberHolder);
+ if (intfNumberHolder.get(JAVA_INT, 0) != interfaceNumber)
+ continue;
+
+ IoKitUsb.AddRef(intf);
+ return new InterfaceInfo(intf, interfaceNumber);
+ }
+ }
+ }
+
+ throwException("invalid interface number: %d", interfaceNumber);
+ throw new AssertionError("not reached");
+ }
+
+ public synchronized void claimInterface(int interfaceNumber) {
+ checkIsOpen();
+ getInterfaceWithCheck(interfaceNumber, false);
+
+ try (var cleanup = new ScopeCleanup()) {
+
+ var interfaceInfo = findInterfaceInfo(interfaceNumber);
+ cleanup.add(() -> IoKitUsb.Release(interfaceInfo.iokitInterface()));
+
+ var ret = IoKitUsb.USBInterfaceOpen(interfaceInfo.iokitInterface());
+ if (ret != 0)
+ throwException(ret, "claiming interface failed");
+
+ IoKitUsb.AddRef(interfaceInfo.iokitInterface());
+ claimedInterfaces.add(interfaceInfo);
+ setClaimed(interfaceNumber, true);
+ addInterfaceEventSource(interfaceInfo);
+ }
+
+ updateEndpointList();
+ }
+
+ @SuppressWarnings({"OptionalGetWithoutIsPresent", "java:S3655"})
+ public synchronized void selectAlternateSetting(int interfaceNumber, int alternateNumber) {
+ // check interface
+ var intf = getInterfaceWithCheck(interfaceNumber, true);
+
+ // check alternate setting
+ var altSetting = intf.getAlternate(alternateNumber);
+ var intfInfo =
+ claimedInterfaces.stream().filter(interf -> interf.interfaceNumber() == interfaceNumber).findFirst().get();
+
+ var ret = IoKitUsb.SetAlternateInterface(intfInfo.iokitInterface(), (byte) alternateNumber);
+ if (ret != 0)
+ throwException(ret, "setting alternate interface failed");
+
+ intf.setAlternate(altSetting);
+ updateEndpointList();
+ }
+
+ @SuppressWarnings({"OptionalGetWithoutIsPresent", "java:S3655"})
+ public synchronized void releaseInterface(int interfaceNumber) {
+ checkIsOpen();
+ getInterfaceWithCheck(interfaceNumber, true);
+
+ @SuppressWarnings("OptionalGetWithoutIsPresent")
+ var interfaceInfo =
+ claimedInterfaces.stream().filter(info -> info.interfaceNumber == interfaceNumber).findFirst().get();
+
+ var source = IoKitUsb.GetInterfaceAsyncEventSource(interfaceInfo.iokitInterface());
+ if (source.address() != 0)
+ asyncTask.removeEventSource(source);
+
+ var ret = IoKitUsb.USBInterfaceClose(interfaceInfo.iokitInterface());
+ if (ret != 0)
+ throwException(ret, "releasing interface failed");
+
+ claimedInterfaces.remove(interfaceInfo);
+ IoKitUsb.Release(interfaceInfo.iokitInterface());
+ setClaimed(interfaceNumber, false);
+
+ updateEndpointList();
+ }
+
+ /**
+ * Update the map of active endpoints.
+ *
+ * MacOS uses a pipe index to refer to endpoints. This method
+ * builds a map from endpoint address to pipe index.
+ *
+ */
+ private void updateEndpointList() {
+ endpoints = new HashMap<>();
+
+ try (var arena = Arena.ofConfined()) {
+
+ var directionHolder = arena.allocate(JAVA_BYTE);
+ var numberHolder = arena.allocate(JAVA_BYTE);
+ var transferTypeHolder = arena.allocate(JAVA_BYTE);
+ var maxPacketSizeHolder = arena.allocate(JAVA_SHORT);
+ var intervalHolder = arena.allocate(JAVA_BYTE);
+
+ for (var interfaceInfo : claimedInterfaces) {
+
+ var intf = interfaceInfo.iokitInterface();
+ var numEndpointsHolder = arena.allocate(JAVA_BYTE);
+ var ret = IoKitUsb.GetNumEndpoints(intf, numEndpointsHolder);
+ if (ret != 0)
+ throwException(ret, "internal error (GetNumEndpoints)");
+ var numEndpoints = numEndpointsHolder.get(JAVA_BYTE, 0) & 255;
+
+ for (var pipeIndex = 1; pipeIndex <= numEndpoints; pipeIndex++) {
+
+ ret = IoKitUsb.GetPipeProperties(intf, (byte) pipeIndex, directionHolder, numberHolder,
+ transferTypeHolder, maxPacketSizeHolder, intervalHolder);
+ if (ret != 0)
+ throwException(ret, "internal error (GetPipeProperties)");
+
+ var endpointNumber = numberHolder.get(JAVA_BYTE, 0) & 0xff;
+ var direction = directionHolder.get(JAVA_BYTE, 0) & 0xff;
+ var endpointAddress = (byte) (endpointNumber | (direction << 7));
+ var transferType = transferTypeHolder.get(JAVA_BYTE, 0);
+ var maxPacketSize = maxPacketSizeHolder.get(JAVA_SHORT, 0) & 0xffff;
+ var endpointInfo = new EndpointInfo(interfaceInfo.iokitInterface(), (byte) pipeIndex,
+ getTransferType(transferType), maxPacketSize);
+ endpoints.put(endpointAddress, endpointInfo);
+ }
+ }
+ }
+ }
+
+ @SuppressWarnings("SameParameterValue")
+ private synchronized EndpointInfo getEndpointInfo(int endpointNumber, UsbDirection direction,
+ UsbTransferType transferType1, UsbTransferType transferType2) {
+ if (endpoints != null) {
+ var endpointAddress = (byte) (endpointNumber | (direction == UsbDirection.IN ? 0x80 : 0));
+ var endpointInfo = endpoints.get(endpointAddress);
+ if (endpointInfo != null && (endpointInfo.transferType == transferType1 || endpointInfo.transferType == transferType2))
+ return endpointInfo;
+ }
+
+ String transferTypeDesc;
+ if (transferType2 == null)
+ transferTypeDesc = transferType1.name();
+ else
+ transferTypeDesc = String.format("%s or %s", transferType1.name(), transferType2.name());
+
+ throwException(
+ "endpoint number %d does not exist, is not part of a claimed interface or is not valid for %s transfer in %s direction",
+ endpointNumber, transferTypeDesc, direction.name());
+ throw new AssertionError("not reached");
+ }
+
+ private static MemorySegment createDeviceRequest(Arena arena, UsbDirection direction, UsbControlTransfer setup,
+ MemorySegment data) {
+ var deviceRequest = IOUSBDevRequest.allocate(arena);
+ var bmRequestType =
+ (direction == UsbDirection.IN ? 0x80 : 0x00) | (setup.requestType().ordinal() << 5) | setup.recipient().ordinal();
+ IOUSBDevRequest.bmRequestType(deviceRequest, (byte) bmRequestType);
+ IOUSBDevRequest.bRequest(deviceRequest, (byte) setup.request());
+ IOUSBDevRequest.wValue(deviceRequest, (short) setup.value());
+ IOUSBDevRequest.wIndex(deviceRequest, (short) setup.index());
+ IOUSBDevRequest.wLength(deviceRequest, (short) data.byteSize());
+ IOUSBDevRequest.pData(deviceRequest, data);
+ return deviceRequest;
+ }
+
+ @Override
+ public byte @NotNull [] controlTransferIn(@NotNull UsbControlTransfer setup, int length) {
+ try (var arena = Arena.ofConfined()) {
+ var data = arena.allocate(length);
+ var deviceRequest = createDeviceRequest(arena, UsbDirection.IN, setup, data);
+
+ var transfer = new MacosTransfer();
+ transfer.setCompletion(UsbDeviceImpl::onSyncTransferCompleted);
+
+ synchronized (transfer) {
+ submitControlTransfer(deviceRequest, transfer);
+ waitForTransfer(transfer, 0, UsbDirection.IN, 0);
+ }
+
+ return data.asSlice(0, transfer.resultSize()).toArray(JAVA_BYTE);
+ }
+ }
+
+ @Override
+ public void controlTransferOut(@NotNull UsbControlTransfer setup, byte[] data) {
+ try (var arena = Arena.ofConfined()) {
+ var dataLength = data != null ? data.length : 0;
+ var dataSegment = arena.allocate(dataLength);
+ if (dataLength > 0)
+ dataSegment.copyFrom(MemorySegment.ofArray(data));
+ var deviceRequest = createDeviceRequest(arena, UsbDirection.OUT, setup, dataSegment);
+
+ var transfer = new MacosTransfer();
+ transfer.setCompletion(UsbDeviceImpl::onSyncTransferCompleted);
+
+ synchronized (transfer) {
+ submitControlTransfer(deviceRequest, transfer);
+ waitForTransfer(transfer, 0, UsbDirection.OUT, 0);
+ }
+ }
+ }
+
+ @Override
+ public void transferOut(int endpointNumber, byte @NotNull [] data, int offset, int length, int timeout) {
+
+ var epInfo = getEndpointInfo(endpointNumber, UsbDirection.OUT, UsbTransferType.BULK,
+ UsbTransferType.INTERRUPT);
+
+ // Auto arena: a transfer that times out may be abandoned (see UsbDeviceImpl.waitForTransfer),
+ // so the buffer must outlive a possible late completion instead of being freed deterministically.
+ var arena = Arena.ofAuto();
+ var nativeData = arena.allocate(JAVA_BYTE, length);
+ nativeData.copyFrom(MemorySegment.ofArray(data).asSlice(offset, length));
+
+ var transfer = new MacosTransfer();
+ transfer.setData(nativeData);
+ transfer.setDataSize(length);
+ transfer.setCompletion(UsbDeviceImpl::onSyncTransferCompleted);
+
+ synchronized (transfer) {
+ if (timeout <= 0 || epInfo.transferType() == UsbTransferType.BULK) {
+ // no timeout or timeout handled by operating system
+ submitTransferOut(endpointNumber, transfer, timeout);
+ waitForTransfer(transfer, 0, UsbDirection.OUT, endpointNumber);
+
+ } else {
+ // interrupt transfer with timeout
+ submitTransferOut(endpointNumber, transfer, 0);
+ waitForTransfer(transfer, timeout, UsbDirection.OUT, endpointNumber);
+ }
+ }
+ }
+
+ @Override
+ public byte @NotNull [] transferIn(int endpointNumber, int timeout) {
+
+ var epInfo = getEndpointInfo(endpointNumber, UsbDirection.IN, UsbTransferType.BULK,
+ UsbTransferType.INTERRUPT);
+
+ // Auto arena: a transfer that times out may be abandoned (see UsbDeviceImpl.waitForTransfer),
+ // so the buffer must outlive a possible late completion instead of being freed deterministically.
+ var arena = Arena.ofAuto();
+ var nativeData = arena.allocate(JAVA_BYTE, epInfo.packetSize());
+
+ var transfer = new MacosTransfer();
+ transfer.setData(nativeData);
+ transfer.setDataSize(epInfo.packetSize());
+ transfer.setCompletion(UsbDeviceImpl::onSyncTransferCompleted);
+
+ synchronized (transfer) {
+ if (timeout <= 0 || epInfo.transferType() == UsbTransferType.BULK) {
+ // no timeout, or timeout handled by operating system
+ submitTransferIn(endpointNumber, transfer, timeout);
+ waitForTransfer(transfer, 0, UsbDirection.IN, endpointNumber);
+
+ } else {
+ // interrupt transfer with timeout
+ submitTransferIn(endpointNumber, transfer, 0);
+ waitForTransfer(transfer, timeout, UsbDirection.IN, endpointNumber);
+ }
+ }
+
+ return nativeData.asSlice(0, transfer.resultSize()).toArray(JAVA_BYTE);
+ }
+
+ /**
+ * Submits a transfer IN to the specified BULK or INTERRUPT endpoint.
+ *
+ * A timeout may only be specified for BULK endpoints.
+ *
+ *
+ * @param endpointNumber endpoint number
+ * @param transfer transfer to execute
+ * @param timeout the timeout, in milliseconds, or 0 for no timeout
+ */
+ synchronized void submitTransferIn(int endpointNumber, MacosTransfer transfer, int timeout) {
+
+ var epInfo = getEndpointInfo(endpointNumber, UsbDirection.IN, UsbTransferType.BULK,
+ UsbTransferType.INTERRUPT);
+ asyncTask.prepareForSubmission(transfer);
+
+ // submit transfer
+ int ret;
+ if (timeout <= 0)
+ ret = IoKitUsb.ReadPipeAsync(epInfo.iokitInterface(), epInfo.pipeIndex(), transfer.data(),
+ transfer.dataSize(), asyncTask.nativeCompletionCallback(), MemorySegment.ofAddress(transfer.id()));
+ else
+ ret = IoKitUsb.ReadPipeAsyncTO(epInfo.iokitInterface(), epInfo.pipeIndex(), transfer.data(),
+ transfer.dataSize(), timeout, timeout, asyncTask.nativeCompletionCallback(),
+ MemorySegment.ofAddress(transfer.id()));
+
+ if (ret != 0) {
+ asyncTask.submissionFailed(transfer);
+ throwException(ret, "error occurred while reading from endpoint %d", endpointNumber);
+ }
+ }
+
+ /**
+ * Submits a transfer OUT to the specified BULK or INTERRUPT endpoint.
+ *
+ * A timeout may only be specified for BULK endpoints.
+ *
+ *
+ * @param endpointNumber endpoint number
+ * @param transfer transfer request to execute
+ * @param timeout the timeout, in milliseconds, or 0 for no timeout
+ */
+ synchronized void submitTransferOut(int endpointNumber, MacosTransfer transfer, int timeout) {
+
+ var epInfo = getEndpointInfo(endpointNumber, UsbDirection.OUT, UsbTransferType.BULK,
+ UsbTransferType.INTERRUPT);
+ asyncTask.prepareForSubmission(transfer);
+
+ // submit transfer
+ int ret;
+ if (timeout <= 0)
+ ret = IoKitUsb.WritePipeAsync(epInfo.iokitInterface(), epInfo.pipeIndex(), transfer.data(),
+ transfer.dataSize(), asyncTask.nativeCompletionCallback(), MemorySegment.ofAddress(transfer.id()));
+ else
+ ret = IoKitUsb.WritePipeAsyncTO(epInfo.iokitInterface(), epInfo.pipeIndex(), transfer.data(),
+ transfer.dataSize(), timeout, timeout, asyncTask.nativeCompletionCallback(),
+ MemorySegment.ofAddress(transfer.id()));
+
+ if (ret != 0) {
+ asyncTask.submissionFailed(transfer);
+ throwException(ret, "error occurred while transmitting to endpoint %d", endpointNumber);
+ }
+ }
+
+ /**
+ * Submits a control transfer.
+ *
+ * @param deviceRequest control transfer request
+ * @param transfer transfer request (for completion handling)
+ */
+ synchronized void submitControlTransfer(MemorySegment deviceRequest, MacosTransfer transfer) {
+
+ checkIsOpen();
+ asyncTask.prepareForSubmission(transfer);
+
+ // submit transfer
+ var ret = IoKitUsb.DeviceRequestAsync(device, deviceRequest, asyncTask.nativeCompletionCallback(),
+ MemorySegment.ofAddress(transfer.id()));
+
+ if (ret != 0) {
+ asyncTask.submissionFailed(transfer);
+ throwException(ret, "control transfer failed");
+ }
+ }
+
+ @Override
+ protected Transfer createTransfer() {
+ return new MacosTransfer();
+ }
+
+ @Override
+ public synchronized void abortTransfers(UsbDirection direction, int endpointNumber) {
+ var epInfo = getEndpointInfo(endpointNumber, direction, UsbTransferType.BULK,
+ UsbTransferType.INTERRUPT);
+
+ var ret = IoKitUsb.AbortPipe(epInfo.iokitInterface(), epInfo.pipeIndex());
+ if (ret != 0)
+ throwException(ret, "aborting transfers failed");
+ }
+
+ @Override
+ public synchronized void clearHalt(UsbDirection direction, int endpointNumber) {
+ var epInfo = getEndpointInfo(endpointNumber, direction, UsbTransferType.BULK,
+ UsbTransferType.INTERRUPT);
+
+ var ret = IoKitUsb.ClearPipeStallBothEnds(epInfo.iokitInterface(), epInfo.pipeIndex());
+ if (ret != 0)
+ throwException(ret, "clearing halt condition failed");
+ }
+
+ @Override
+ public synchronized @NotNull InputStream openInputStream(int endpointNumber, int bufferSize) {
+ // check that endpoint number is valid
+ getEndpointInfo(endpointNumber, UsbDirection.IN, UsbTransferType.BULK, null);
+
+ return new MacosEndpointInputStream(this, endpointNumber, bufferSize);
+ }
+
+ @Override
+ public synchronized @NotNull OutputStream openOutputStream(int endpointNumber, int bufferSize) {
+ // check that endpoint number is valid
+ getEndpointInfo(endpointNumber, UsbDirection.OUT, UsbTransferType.BULK, null);
+
+ return new MacosEndpointOutputStream(this, endpointNumber, bufferSize);
+ }
+
+ @Override
+ protected void throwOSException(int errorCode, String message, Object... args) {
+ throwException(errorCode, message, args);
+ }
+
+ private static UsbTransferType getTransferType(byte macosTransferType) {
+ return switch (macosTransferType) {
+ case 1 -> UsbTransferType.ISOCHRONOUS;
+ case 2 -> UsbTransferType.BULK;
+ case 3 -> UsbTransferType.INTERRUPT;
+ default -> null;
+ };
+ }
+
+ private synchronized void addDeviceEventSource() {
+ try (var innerArena = Arena.ofConfined()) {
+ var sourceHolder = innerArena.allocate(ADDRESS);
+ var ret = IoKitUsb.CreateDeviceAsyncEventSource(device, sourceHolder);
+ if (ret != 0)
+ throwException(ret, "internal error (CreateDeviceAsyncEventSource)");
+ var source = dereference(sourceHolder);
+ asyncTask.addEventSource(source);
+ }
+ }
+
+ private synchronized void addInterfaceEventSource(InterfaceInfo interfaceInfo) {
+ try (var innerArena = Arena.ofConfined()) {
+ var sourceHolder = innerArena.allocate(ADDRESS);
+ var ret = IoKitUsb.CreateInterfaceAsyncEventSource(interfaceInfo.iokitInterface(), sourceHolder);
+ if (ret != 0)
+ throwException(ret, "internal error (CreateInterfaceAsyncEventSource)");
+ var source = dereference(sourceHolder);
+ asyncTask.addEventSource(source);
+ }
+ }
+
+ record InterfaceInfo(MemorySegment iokitInterface, int interfaceNumber) {
+ }
+
+ record EndpointInfo(MemorySegment iokitInterface, byte pipeIndex, UsbTransferType transferType, int packetSize) {
+ }
+}
diff --git a/java-does-usb/src/main/java/net/codecrete/usb/macos/MacosUsbDeviceRegistry.java b/java-does-usb/src/main/java/net/codecrete/usb/macos/MacosUsbDeviceRegistry.java
new file mode 100644
index 00000000..b2126246
--- /dev/null
+++ b/java-does-usb/src/main/java/net/codecrete/usb/macos/MacosUsbDeviceRegistry.java
@@ -0,0 +1,274 @@
+//
+// Java Does USB
+// Copyright (c) 2022 Manuel Bleichenbacher
+// Licensed under MIT License
+// https://opensource.org/licenses/MIT
+//
+
+package net.codecrete.usb.macos;
+
+import net.codecrete.usb.UsbDevice;
+import net.codecrete.usb.common.ScopeCleanup;
+import net.codecrete.usb.common.UsbDeviceRegistry;
+import net.codecrete.usb.macos.gen.corefoundation.CoreFoundation;
+import net.codecrete.usb.macos.gen.iokit.IOKit;
+import net.codecrete.usb.macos.gen.iokit.IOServiceAddMatchingNotification$callback;
+
+import java.lang.foreign.Arena;
+import java.lang.foreign.MemorySegment;
+import java.lang.foreign.SegmentAllocator;
+import java.util.ArrayList;
+import java.util.function.Consumer;
+
+import static java.lang.System.Logger.Level.INFO;
+import static java.lang.System.Logger.Level.WARNING;
+import static java.lang.foreign.MemorySegment.NULL;
+import static java.lang.foreign.ValueLayout.JAVA_INT;
+import static java.lang.foreign.ValueLayout.JAVA_LONG;
+import static net.codecrete.usb.macos.CoreFoundationHelper.createCFStringRef;
+import static net.codecrete.usb.macos.MacosUsbException.throwException;
+
+/**
+ * MacOS implementation of USB device registry.
+ */
+@SuppressWarnings("java:S116")
+public class MacosUsbDeviceRegistry extends UsbDeviceRegistry {
+
+ private static final System.Logger LOG = System.getLogger(MacosUsbDeviceRegistry.class.getName());
+
+ private static final MemorySegment KEY_ID_VENDOR;
+ private static final MemorySegment KEY_ID_PRODUCT;
+ private static final MemorySegment KEY_VENDOR;
+ private static final MemorySegment KEY_PRODUCT;
+ private static final MemorySegment KEY_SERIAL_NUM;
+ private static final MemorySegment KEY_DEVICE_CLASS;
+ private static final MemorySegment KEY_DEVICE_SUBCLASS;
+ private static final MemorySegment KEY_DEVICE_PROTOCOL;
+ private static final MemorySegment KEY_USB_BCD;
+ private static final MemorySegment KEY_DEVICE_BCD;
+
+ static {
+ SegmentAllocator global = Arena.global();
+ KEY_ID_VENDOR = createCFStringRef("idVendor", global);
+ KEY_ID_PRODUCT = createCFStringRef("idProduct", global);
+ KEY_VENDOR = createCFStringRef("kUSBVendorString", global);
+ KEY_PRODUCT = createCFStringRef("kUSBProductString", global);
+ KEY_SERIAL_NUM = createCFStringRef("kUSBSerialNumberString", global);
+ KEY_DEVICE_CLASS = createCFStringRef("bDeviceClass", global);
+ KEY_DEVICE_SUBCLASS = createCFStringRef("bDeviceSubClass", global);
+ KEY_DEVICE_PROTOCOL = createCFStringRef("bDeviceProtocol", global);
+ KEY_USB_BCD = createCFStringRef("bcdUSB", global);
+ KEY_DEVICE_BCD = createCFStringRef("bcdDevice", global);
+ }
+
+ /**
+ * Monitors the USB devices.
+ *
+ * This method is the core of the background thread. It runs forever and does not terminate.
+ *
+ */
+ @Override
+ protected void monitorDevices() {
+
+ // as the method runs forever, there is no need to clean up one-time allocations
+ try (var arena = Arena.ofConfined()) {
+
+ try {
+
+ // setup run loop, run loop source and notification port
+ var notifyPort = IOKit.IONotificationPortCreate(IOKit.kIOMasterPortDefault());
+ var runLoopSource = IOKit.IONotificationPortGetRunLoopSource(notifyPort);
+ var runLoop = CoreFoundation.CFRunLoopGetCurrent();
+ CoreFoundation.CFRunLoopAddSource(runLoop, runLoopSource, IOKit.kCFRunLoopDefaultMode());
+
+ // setup notification for connected devices
+ var deviceConnectedIter = setupNotification(arena, notifyPort, IOKit.kIOFirstMatchNotification(),
+ this::onDevicesConnected);
+
+ // iterate current devices in order to arm the notifications (and build initial device list)
+ var deviceList = new ArrayList();
+ iterateDevices(deviceConnectedIter, device -> deviceList.add(device)); // NOSONAR
+ setInitialDeviceList(deviceList);
+
+ // setup notification for disconnected devices
+ var deviceDisconnectedIter = setupNotification(arena, notifyPort, IOKit.kIOTerminatedNotification(),
+ this::onDevicesDisconnected);
+
+ // iterate current devices in order to arm the notifications
+ onDevicesDisconnected(NULL, deviceDisconnectedIter);
+
+ } catch (Exception e) {
+ enumerationFailed(e);
+ return;
+ }
+
+ // loop forever
+ CoreFoundation.CFRunLoopRun();
+ LOG.log(WARNING, "unexpected end of CFRunLoopRun");
+ }
+ }
+
+ /**
+ * Process the devices resulting from the iterator
+ *
+ * @param iterator the iterator
+ * @param consumer a consumer that will be called for each device with the entry ID and the service
+ */
+ private void iterateDevices(int iterator, IOKitDeviceConsumer consumer) {
+
+ try (var arena = Arena.ofConfined()) {
+ var entryIdHolder = arena.allocate(JAVA_LONG);
+
+ int svc;
+ while ((svc = IOKit.IOIteratorNext(iterator)) != 0) {
+ try (var cleanup = new ScopeCleanup()) {
+
+ final var service = svc;
+ cleanup.add(() -> IOKit.IOObjectRelease(service));
+
+ var device = IoKitHelper.getInterface(service, IoKitHelper.kIOUSBDeviceUserClientTypeID,
+ IoKitHelper.kIOUSBDeviceInterfaceID187);
+ if (device != null)
+ cleanup.add(() -> IoKitUsb.Release(device));
+
+ // get entry ID (as unique ID)
+ var ret = IOKit.IORegistryEntryGetRegistryEntryID(service, entryIdHolder);
+ if (ret != 0)
+ throwException(ret, "internal error (IORegistryEntryGetRegistryEntryID)");
+ var entryId = entryIdHolder.get(JAVA_LONG, 0);
+
+ // call consumer to process device
+ consumer.accept(entryId, service, device);
+ }
+ }
+ }
+ }
+
+ /**
+ * Calls the consumer for all devices produced by the iterator.
+ *
+ * This method tries to create a {@link UsbDevice} instance.
+ * If it fails, an information is printed, but the consumer is not called.
+ *
+ *
+ * @param iterator the iterator
+ * @param consumer the consumer
+ */
+ @SuppressWarnings("java:S106")
+ private void iterateDevices(int iterator, Consumer consumer) {
+ iterateDevices(iterator, (entryId, service, deviceIntf) -> {
+
+ var deviceInfo = new VidPid();
+ try {
+ var device = createDevice(entryId, service, deviceIntf, deviceInfo);
+ if (device != null)
+ consumer.accept(device);
+
+ } catch (Exception e) {
+ LOG.log(INFO, String.format("failed to retrieve information about device 0x%04x/0x%04x - ignoring device",
+ deviceInfo.vid, deviceInfo.pid), e);
+ }
+ });
+ }
+
+ private UsbDevice createDevice(Long entryID, int service, MemorySegment deviceIntf, VidPid info) {
+
+ if (deviceIntf == null)
+ return null;
+
+ try (var arena = Arena.ofConfined()) {
+
+ var vendorId = IoKitHelper.getPropertyInt(service, KEY_ID_VENDOR, arena);
+ var productId = IoKitHelper.getPropertyInt(service, KEY_ID_PRODUCT, arena);
+ if (vendorId == null || productId == null)
+ return null;
+
+ info.vid = vendorId;
+ info.pid = productId;
+
+ var device = new MacosUsbDevice(deviceIntf, entryID, vendorId, productId);
+
+ var manufacturer = IoKitHelper.getPropertyString(service, KEY_VENDOR, arena);
+ var product = IoKitHelper.getPropertyString(service, KEY_PRODUCT, arena);
+ var serial = IoKitHelper.getPropertyString(service, KEY_SERIAL_NUM, arena);
+
+ device.setProductStrings(manufacturer, product, serial);
+
+ var classCode = IoKitHelper.getPropertyInt(service, KEY_DEVICE_CLASS, arena);
+ var subclassCode = IoKitHelper.getPropertyInt(service, KEY_DEVICE_SUBCLASS, arena);
+ var protocolCode = IoKitHelper.getPropertyInt(service, KEY_DEVICE_PROTOCOL, arena);
+
+ device.setClassCodes(classCode != null ? classCode : 0, subclassCode != null ? subclassCode : 0,
+ protocolCode != null ? protocolCode : 0);
+
+ var usbVersion = IoKitHelper.getPropertyInt(service, KEY_USB_BCD, arena);
+ var deviceVersion = IoKitHelper.getPropertyInt(service, KEY_DEVICE_BCD, arena);
+ //noinspection DataFlowIssue
+ device.setVersions(usbVersion, deviceVersion != null ? deviceVersion : 0);
+
+ return device;
+ }
+ }
+
+ private int setupNotification(Arena arena, MemorySegment notifyPort, MemorySegment notificationType,
+ IOServiceAddMatchingNotification$callback.Function callback) {
+
+ // new matching dictionary for (dis)connected device notifications (NOSONAR)
+ var matchingDict = IOKit.IOServiceMatching(IOKit.kIOUSBDeviceClassName());
+
+ // create callback stub
+ var onDeviceCallbackStub = IOServiceAddMatchingNotification$callback.allocate(callback, Arena.global());
+
+ // Set up a notification to be called when a device is first matched / terminated by I/O Kit.
+ // This method consumes the matchingDict reference.
+ var deviceIterHolder = arena.allocate(JAVA_INT);
+ var ret = IOKit.IOServiceAddMatchingNotification(notifyPort, notificationType, matchingDict,
+ onDeviceCallbackStub, NULL, deviceIterHolder);
+ if (ret != 0)
+ throwException(ret, "internal error (IOServiceAddMatchingNotification)");
+
+ return deviceIterHolder.get(JAVA_INT, 0);
+ }
+
+ /**
+ * Callback function for monitoring connected USB devices.
+ *
+ * This method is used in an upcall from native code.
+ *
+ *
+ * @param ignoredRefCon ignored parameter
+ * @param iterator device iterator
+ */
+ @SuppressWarnings("java:S1172")
+ private void onDevicesConnected(MemorySegment ignoredRefCon, int iterator) {
+
+ // process device iterator for connected devices
+ iterateDevices(iterator, this::addDevice);
+ }
+
+ /**
+ * Callback function for monitoring disconnected USB devices.
+ *
+ * This method is used in an upcall from native code.
+ *
+ *
+ * @param ignoredRefCon ignored parameter
+ * @param iterator device iterator
+ */
+ @SuppressWarnings({"SameParameterValue", "java:S1172", "java:S106"})
+ private void onDevicesDisconnected(MemorySegment ignoredRefCon, int iterator) {
+
+ // process device iterator for disconnected devices
+ iterateDevices(iterator, (entryId, _, _) -> closeAndRemoveDevice(entryId));
+ }
+
+ @FunctionalInterface
+ interface IOKitDeviceConsumer {
+ void accept(long entryId, int service, MemorySegment deviceIntf);
+ }
+
+ static class VidPid {
+ int vid;
+ int pid;
+ }
+}
diff --git a/java-does-usb/src/main/java/net/codecrete/usb/macos/MacosUsbException.java b/java-does-usb/src/main/java/net/codecrete/usb/macos/MacosUsbException.java
new file mode 100644
index 00000000..8731c61f
--- /dev/null
+++ b/java-does-usb/src/main/java/net/codecrete/usb/macos/MacosUsbException.java
@@ -0,0 +1,69 @@
+//
+// Java Does USB
+// Copyright (c) 2022 Manuel Bleichenbacher
+// Licensed under MIT License
+// https://opensource.org/licenses/MIT
+//
+package net.codecrete.usb.macos;
+
+import net.codecrete.usb.UsbException;
+import net.codecrete.usb.UsbStallException;
+import net.codecrete.usb.UsbTimeoutException;
+import net.codecrete.usb.macos.gen.iokit.IOKit;
+import net.codecrete.usb.macos.gen.mach.mach;
+
+/**
+ * Exception thrown if a macOS specific error occurs.
+ */
+public class MacosUsbException extends UsbException {
+
+ /**
+ * Creates a new instance.
+ *
+ * The message for the macOS error code is looked up and appended to the message.
+ *
+ *
+ * @param message exception message
+ * @param errorCode macOS error code (usually returned by macOS functions)
+ */
+ public MacosUsbException(String message, int errorCode) {
+ super(String.format("%s: %s", message, machErrorMessage(errorCode)), errorCode);
+ }
+
+ private static String machErrorMessage(int errorCode) {
+ var msg = mach.mach_error_string(errorCode);
+ return msg.getString(0);
+ }
+
+ /**
+ * Throws an exception for the specified macOS error code.
+ *
+ * The message for the macOS error code is looked up and appended to the message.
+ *
+ * Invalid string descriptors might be missing the header,
+ * have a descriptor type that is not a string descriptor,
+ * indicate an incorrect length or have incomplete UTF-16 code units.
+ *
+ * @return if this descriptor is valid
+ */
+ public boolean isValid() {
+ return descriptor.byteSize() >= 2
+ && descriptor.get(JAVA_BYTE, bDescriptorType$OFFSET) == 3
+ && length() == descriptor.byteSize()
+ && (descriptor.byteSize() & 1) == 0;
+ }
+
+ public int length() {
+ return 0xff & descriptor.get(JAVA_BYTE, bLength$OFFSET);
+ }
+
+ /**
+ * Returns the string of this string descriptor.
+ *
+ * Invalid UTF-16 code units are replaced with the Unicode replacement character.
+ * Trailing 0s (UTF-16 code unit with value 0) are truncated.
+ *
+ * @throws UsbException if the string descriptor is invalid
+ * @return the string value
+ */
+ public String string() {
+ if (!isValid())
+ throw new UsbException("String descriptor is invalid");
+ var len = (int) (length() - 2L);
+ var bytes = descriptor.asSlice(string$OFFSET, len).toArray(JAVA_BYTE);
+
+ // truncate trailing 0s
+ while (len > 0 && bytes[len - 2] == 0 && bytes[len - 1] == 0)
+ len--;
+
+ return new String(bytes, 0, len, StandardCharsets.UTF_16LE);
+ }
+
+ // struct USBStringDescriptor {
+ // uint8_t bLength;
+ // uint8_t bDescriptorType;
+ // uint16_t string[1];
+ // } __attribute__((packed));
+ public static final GroupLayout LAYOUT = MemoryLayout.structLayout(
+ JAVA_BYTE.withName("bLength"),
+ JAVA_BYTE.withName("bDescriptorType"),
+ JAVA_SHORT.withName("string")
+ );
+
+ private static final long bLength$OFFSET = 0;
+ private static final long bDescriptorType$OFFSET = 1;
+ private static final long string$OFFSET = 2;
+}
diff --git a/java-does-usb/src/main/java/net/codecrete/usb/windows/CompositeFunction.java b/java-does-usb/src/main/java/net/codecrete/usb/windows/CompositeFunction.java
deleted file mode 100644
index acf7326c..00000000
--- a/java-does-usb/src/main/java/net/codecrete/usb/windows/CompositeFunction.java
+++ /dev/null
@@ -1,81 +0,0 @@
-//
-// Java Does USB
-// Copyright (c) 2022 Manuel Bleichenbacher
-// Licensed under MIT License
-// https://opensource.org/licenses/MIT
-//
-
-package net.codecrete.usb.windows;
-
-import java.lang.foreign.MemoryAddress;
-
-/**
- * Describes the function of an interface of a composite USB device.
- *
- * A composite USB device can have multiple functions, e.g. a mass
- * storage function and a virtual serial port function. Each function
- * will appear as a separate device in Window.
- *
- *
- * A function consists of one or more interfaces. Functions with
- * multiple interfaces must have consecutive interface numbers. The
- * interfaces after the first one are called associated interfaces.
- *
- *
- * On Windows, each function has a separate device path. Each device
- * path must be opened and each interface must be opened.
- * Furthermore, the first and the associated interfaces are
- * treated differently.
- *
- */
-public class CompositeFunction {
- // TODO: implement associated interfaces
-
- // TODO: implement devices without interfaces
-
- private final int firstInterfaceNumber_;
- private final int numInterfaces_;
- private final String devicePath_;
- private MemoryAddress deviceHandle_;
- private MemoryAddress firstInterfaceHandle_;
-
- /**
- * Creates a new instance with a single interface.
- *
- * @param interfaceNumber the interface number
- * @param devicePath the device path
- */
- public CompositeFunction(int interfaceNumber, String devicePath) {
- firstInterfaceNumber_ = interfaceNumber;
- numInterfaces_ = 1;
- devicePath_ = devicePath;
- }
-
- public int firstInterfaceNumber() {
- return firstInterfaceNumber_;
- }
-
- public int numInterfaces() {
- return numInterfaces_;
- }
-
- public String devicePath() {
- return devicePath_;
- }
-
- public MemoryAddress deviceHandle() {
- return deviceHandle_;
- }
-
- public void setDeviceHandle(MemoryAddress deviceHandle) {
- deviceHandle_ = deviceHandle;
- }
-
- public MemoryAddress firstInterfaceHandle() {
- return firstInterfaceHandle_;
- }
-
- public void setFirstInterfaceHandle(MemoryAddress firstInterfaceHandle) {
- firstInterfaceHandle_ = firstInterfaceHandle;
- }
-}
diff --git a/java-does-usb/src/main/java/net/codecrete/usb/windows/CustomApis.java b/java-does-usb/src/main/java/net/codecrete/usb/windows/CustomApis.java
new file mode 100644
index 00000000..34ecb727
--- /dev/null
+++ b/java-does-usb/src/main/java/net/codecrete/usb/windows/CustomApis.java
@@ -0,0 +1,58 @@
+package net.codecrete.usb.windows;
+
+import net.codecrete.usb.usbstandard.SetupPacket;
+
+import java.lang.foreign.FunctionDescriptor;
+import java.lang.foreign.Linker;
+import java.lang.foreign.MemorySegment;
+import java.lang.foreign.SymbolLookup;
+import java.lang.invoke.MethodHandle;
+
+import static java.lang.foreign.ValueLayout.ADDRESS;
+import static java.lang.foreign.ValueLayout.JAVA_INT;
+
+@SuppressWarnings({"java:S100", "java:S101", "java:S112", "java:S117"})
+public class CustomApis {
+ private CustomApis() {
+ }
+
+ static {
+ System.loadLibrary("KERNEL32");
+ System.loadLibrary("WINUSB");
+ }
+
+ private static final SymbolLookup SYMBOL_LOOKUP = SymbolLookup.loaderLookup();
+ private static final Linker LINKER = Linker.nativeLinker();
+ private static final Linker.Option LAST_ERROR_STATE = Linker.Option.captureCallState("GetLastError");
+
+ // Custom implementation of WinUsb_ControlTransfer as FFM cannot deal with the
+ // WINUSB_SETUP_PACKET being passed by value as it uses unaligned fields.
+ // SetupPacket does not use unaligned fields.
+ private static class WinUsb_ControlTransfer$IMPL {
+ private static final FunctionDescriptor DESC = FunctionDescriptor.of(JAVA_INT, ADDRESS, SetupPacket.LAYOUT, ADDRESS, JAVA_INT, ADDRESS, ADDRESS);
+ private static final MethodHandle HANDLE = LINKER.downcallHandle(SYMBOL_LOOKUP.findOrThrow("WinUsb_ControlTransfer"), DESC, LAST_ERROR_STATE);
+ }
+
+ public static int WinUsb_ControlTransfer(MemorySegment lastErrorState, MemorySegment InterfaceHandle, MemorySegment SetupPacket, MemorySegment Buffer, int BufferLength, MemorySegment LengthTransferred, MemorySegment Overlapped) {
+ try {
+ return (int) WinUsb_ControlTransfer$IMPL.HANDLE.invokeExact(lastErrorState, InterfaceHandle, SetupPacket, Buffer, BufferLength, LengthTransferred, Overlapped);
+ } catch (Throwable ex) {
+ throw new RuntimeException(ex);
+ }
+ }
+
+ // CloseHandle implementation without error state
+ private static class CloseHandle$IMPL {
+ private static final FunctionDescriptor DESC = FunctionDescriptor.of(JAVA_INT, ADDRESS);
+ private static final MethodHandle HANDLE = LINKER.downcallHandle(SYMBOL_LOOKUP.findOrThrow("CloseHandle"), DESC);
+ }
+
+ public static int CloseHandle(MemorySegment hObject) {
+ try {
+ return (int) CloseHandle$IMPL.HANDLE.invokeExact(hObject);
+ } catch (Throwable ex) {
+ throw new RuntimeException(ex);
+ }
+ }
+
+}
diff --git a/java-does-usb/src/main/java/net/codecrete/usb/windows/DeviceInfoSet.java b/java-does-usb/src/main/java/net/codecrete/usb/windows/DeviceInfoSet.java
new file mode 100644
index 00000000..e9d3bfb8
--- /dev/null
+++ b/java-does-usb/src/main/java/net/codecrete/usb/windows/DeviceInfoSet.java
@@ -0,0 +1,395 @@
+package net.codecrete.usb.windows;
+
+import net.codecrete.usb.common.ScopeCleanup;
+import system.Guid;
+import windows.win32.devices.deviceanddriverinstallation.SP_DEVICE_INTERFACE_DATA;
+import windows.win32.devices.deviceanddriverinstallation.SP_DEVICE_INTERFACE_DETAIL_DATA_W;
+import windows.win32.devices.deviceanddriverinstallation.SP_DEVINFO_DATA;
+
+import java.lang.foreign.Arena;
+import java.lang.foreign.MemorySegment;
+import java.util.List;
+
+import static java.lang.foreign.MemorySegment.NULL;
+import static java.lang.foreign.ValueLayout.JAVA_CHAR;
+import static java.lang.foreign.ValueLayout.JAVA_INT;
+import static java.nio.charset.StandardCharsets.UTF_16LE;
+import static net.codecrete.usb.windows.Win.allocateErrorState;
+import static net.codecrete.usb.windows.WindowsUsbException.throwException;
+import static net.codecrete.usb.windows.WindowsUsbException.throwLastError;
+import static windows.win32.devices.deviceanddriverinstallation.Apis.SetupDiDeleteDeviceInterfaceData;
+import static windows.win32.devices.deviceanddriverinstallation.Apis.SetupDiDestroyDeviceInfoList;
+import static windows.win32.devices.deviceanddriverinstallation.Apis.SetupDiEnumDeviceInfo;
+import static windows.win32.devices.deviceanddriverinstallation.Apis.SetupDiEnumDeviceInterfaces;
+import static windows.win32.devices.deviceanddriverinstallation.Apis.SetupDiGetClassDevsW;
+import static windows.win32.devices.deviceanddriverinstallation.Apis.SetupDiCreateDeviceInfoList;
+import static windows.win32.devices.deviceanddriverinstallation.Apis.SetupDiGetDeviceInterfaceDetailW;
+import static windows.win32.devices.deviceanddriverinstallation.Apis.SetupDiGetDevicePropertyW;
+import static windows.win32.devices.deviceanddriverinstallation.Apis.SetupDiOpenDevRegKey;
+import static windows.win32.devices.deviceanddriverinstallation.Apis.SetupDiOpenDeviceInfoW;
+import static windows.win32.devices.deviceanddriverinstallation.Apis.SetupDiOpenDeviceInterfaceW;
+import static windows.win32.devices.deviceanddriverinstallation.Constants.DIREG_DEV;
+import static windows.win32.devices.deviceanddriverinstallation.SETUP_DI_GET_CLASS_DEVS_FLAGS.DIGCF_DEVICEINTERFACE;
+import static windows.win32.devices.deviceanddriverinstallation.SETUP_DI_GET_CLASS_DEVS_FLAGS.DIGCF_PRESENT;
+import static windows.win32.devices.deviceanddriverinstallation.SETUP_DI_PROPERTY_CHANGE_SCOPE.DICS_FLAG_GLOBAL;
+import static windows.win32.devices.properties.Constants.DEVPKEY_Device_Service;
+import static windows.win32.devices.properties.DEVPROPTYPE.DEVPROP_TYPEMOD_LIST;
+import static windows.win32.devices.properties.DEVPROPTYPE.DEVPROP_TYPE_STRING;
+import static windows.win32.devices.properties.DEVPROPTYPE.DEVPROP_TYPE_UINT32;
+import static windows.win32.foundation.WIN32_ERROR.ERROR_FILE_NOT_FOUND;
+import static windows.win32.foundation.WIN32_ERROR.ERROR_INSUFFICIENT_BUFFER;
+import static windows.win32.foundation.WIN32_ERROR.ERROR_MORE_DATA;
+import static windows.win32.foundation.WIN32_ERROR.ERROR_NOT_FOUND;
+import static windows.win32.foundation.WIN32_ERROR.ERROR_NO_MORE_ITEMS;
+import static windows.win32.system.com.Apis.CLSIDFromString;
+import static windows.win32.system.registry.Apis.RegCloseKey;
+import static windows.win32.system.registry.Apis.RegQueryValueExW;
+import static windows.win32.system.registry.REG_SAM_FLAGS.KEY_READ;
+
+/**
+ * Device information set (of Windows Setup API).
+ *
+ *
+ * An instance of this class represents a device information set ({@code HDEVINFO})
+ * and a current element within the set.
+ *
+ */
+public class DeviceInfoSet implements AutoCloseable {
+
+ @FunctionalInterface
+ interface InfoSetCreator {
+ long create(Arena arena, MemorySegment errorState);
+ }
+
+ private final Arena arena;
+ private final MemorySegment errorState;
+ private final long devInfoSet;
+ private final MemorySegment devInfoData;
+ private MemorySegment devIntfData;
+ private int iterationIndex = -1;
+
+ /**
+ * Creates a new device info set containing the present devices of the specified device class and
+ * optionally device instance ID.
+ *
+ *
+ * After creation, there is no current element. {@link #next()} should be called to iterate the first
+ * and all subsequent elements.
+ *
+ *
+ * @param interfaceGuid device interface class GUID
+ * @param instanceId device instance ID
+ * @return device info set
+ */
+ static DeviceInfoSet ofPresentDevices(MemorySegment interfaceGuid, String instanceId) {
+ return new DeviceInfoSet((arena, errorState) -> {
+ var instanceIdSegment = instanceId != null ? arena.allocateFrom(instanceId, UTF_16LE) : NULL;
+ return SetupDiGetClassDevsW(errorState, interfaceGuid, instanceIdSegment, NULL,
+ DIGCF_PRESENT | DIGCF_DEVICEINTERFACE);
+ });
+ }
+
+ /**
+ * Creates a new device info set containing a single device with the specified instance ID.
+ *
+ *
+ * The device becomes the current element. The set cannot be iterated.
+ *
+ *
+ * @param instanceId instance ID
+ */
+ static DeviceInfoSet ofInstance(String instanceId) {
+ var devInfoSet = ofEmpty();
+ try {
+ devInfoSet.addInstanceId(instanceId);
+ } catch (Exception t) {
+ devInfoSet.close();
+ throw t;
+ }
+ return devInfoSet;
+ }
+
+ /**
+ * Creates a new device info set containing a single device with the specified path.
+ *
+ *
+ * The device becomes the current element. The set cannot be iterated.
+ *
+ *
+ * @param devicePath device path
+ */
+ static DeviceInfoSet ofPath(String devicePath) {
+ var devInfoSet = ofEmpty();
+ try {
+ devInfoSet.addDevicePath(devicePath);
+ } catch (Exception t) {
+ devInfoSet.close();
+ throw t;
+ }
+ return devInfoSet;
+ }
+
+ /**
+ * Creates a new empty device info set.
+ *
+ * @return device info set
+ */
+ private static DeviceInfoSet ofEmpty() {
+ return new DeviceInfoSet((_, errorState) -> SetupDiCreateDeviceInfoList(errorState, NULL, NULL));
+ }
+
+ private DeviceInfoSet(InfoSetCreator creator) {
+ arena = Arena.ofConfined();
+ try {
+ errorState = allocateErrorState(arena);
+
+ devInfoSet = creator.create(arena, errorState);
+ if (Win.isInvalidHandle(devInfoSet))
+ throwLastError(errorState, "internal error (creating device info set)");
+
+ // allocate SP_DEVINFO_DATA (will receive device details)
+ devInfoData = SP_DEVINFO_DATA.allocate(arena);
+
+ } catch (Exception e) {
+ arena.close();
+ throw e;
+ }
+ }
+
+ @Override
+ public void close() {
+ if (devIntfData != null)
+ SetupDiDeleteDeviceInterfaceData(errorState, devInfoSet, devIntfData);
+ SetupDiDestroyDeviceInfoList(errorState, devInfoSet);
+ arena.close();
+ }
+
+ private void addInstanceId(String instanceId) {
+ var instanceIdSegment = arena.allocateFrom(instanceId, UTF_16LE);
+ if (SetupDiOpenDeviceInfoW(errorState, devInfoSet, instanceIdSegment, NULL, 0, devInfoData) == 0)
+ throwLastError(errorState, "internal error (SetupDiOpenDeviceInfoW)");
+ }
+
+ private void addDevicePath(String devicePath) {
+ if (devIntfData != null)
+ throw new AssertionError("calling addDevice() multiple times is not implemented");
+
+ // load device information into dev info set
+ var intfData = SP_DEVICE_INTERFACE_DATA.allocate(arena);
+ var devicePathSegment = arena.allocateFrom(devicePath, UTF_16LE);
+ if (SetupDiOpenDeviceInterfaceW(errorState, devInfoSet, devicePathSegment, 0, intfData) == 0)
+ throwLastError(errorState, "internal error (SetupDiOpenDeviceInterfaceW)");
+
+ devIntfData = intfData; // for later cleanup
+
+ if (SetupDiGetDeviceInterfaceDetailW(errorState, devInfoSet, intfData, NULL, 0, NULL, devInfoData) == 0) {
+ var err = Win.getLastError(errorState);
+ if (err != ERROR_INSUFFICIENT_BUFFER)
+ throwException(err, "internal error (SetupDiGetDeviceInterfaceDetailW)");
+ }
+ }
+
+ /**
+ * Iterates to the next element in this set.
+ *
+ * @return {@code true} if there is a current element, {@code false} if the iteration moved beyond the last element
+ */
+ boolean next() {
+ iterationIndex += 1;
+ if (SetupDiEnumDeviceInfo(errorState, devInfoSet, iterationIndex, devInfoData) == 0) {
+ var err = Win.getLastError(errorState);
+ if (err == ERROR_NO_MORE_ITEMS)
+ return false;
+ throwLastError(errorState, "internal error (SetupDiEnumDeviceInfo)");
+ }
+
+ return true;
+ }
+
+ /**
+ * Checks if the current element is a composite USB device
+ *
+ * @return {@code true} if it is a composite device
+ */
+ boolean isCompositeDevice() {
+ var deviceService = getStringProperty(DEVPKEY_Device_Service());
+
+ // usbccgp is the USB Generic Parent Driver used for composite devices
+ return "usbccgp".equalsIgnoreCase(deviceService);
+ }
+
+ /**
+ * Gets the device path for the device with the given instance ID.
+ *
+ * The device path is looked up by checking the GUIDs associated with the current element.
+ *
+ *
+ * @param instanceId device instance ID
+ * @return the device path, {@code null} if not found
+ */
+ String getDevicePathByGUID(String instanceId) {
+ var guids = findDeviceInterfaceGUIDs(arena);
+
+ for (var guid : guids) {
+ // check for class GUID
+ var guidSegment = arena.allocateFrom(guid, UTF_16LE);
+ var clsid = Guid.allocate(arena);
+ if (CLSIDFromString(guidSegment, clsid) != 0)
+ continue;
+
+ try {
+ return getDevicePath(instanceId, clsid);
+ } catch (Exception _) {
+ // ignore and try next one
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Gets a list of {@code DeviceInterfaceGUIDs} from the current element's device configuration information
+ * in the registry.
+ *
+ * @param arena arena for allocating memory
+ * @return list of GUIDs
+ */
+ private List findDeviceInterfaceGUIDs(Arena arena) {
+
+ try (var cleanup = new ScopeCleanup()) {
+ // open device registry key
+ var regKey = SetupDiOpenDevRegKey(errorState, devInfoSet, devInfoData, DICS_FLAG_GLOBAL, 0,
+ DIREG_DEV, KEY_READ);
+ if (Win.isInvalidHandle(regKey))
+ throwLastError(errorState, "internal error (SetupDiOpenDevRegKey)");
+ cleanup.add(() -> RegCloseKey(regKey));
+
+ // read registry value (without buffer, to query length)
+ var keyNameSegment = arena.allocateFrom("DeviceInterfaceGUIDs", UTF_16LE);
+ var valueTypeHolder = arena.allocate(JAVA_INT);
+ var valueSizeHolder = arena.allocate(JAVA_INT);
+ var res = RegQueryValueExW(regKey, keyNameSegment, NULL, valueTypeHolder, NULL, valueSizeHolder);
+ if (res == ERROR_FILE_NOT_FOUND)
+ return List.of(); // no device interface GUIDs
+ if (res != 0 && res != ERROR_MORE_DATA)
+ throwException(res, "internal error (RegQueryValueExW)");
+
+ // read registry value (with buffer)
+ var valueSize = valueSizeHolder.get(JAVA_INT, 0);
+ var value = arena.allocate(valueSize);
+ res = RegQueryValueExW(regKey, keyNameSegment, NULL, valueTypeHolder, value, valueSizeHolder);
+ if (res != 0)
+ throwException(res, "internal error (RegQueryValueExW)");
+
+ return Win.createStringListFromSegment(value);
+ }
+ }
+
+ /**
+ * Gets the integer device property of the current element.
+ *
+ * @param propertyKey property key (of type {@code DEVPKEY})
+ * @return property value
+ */
+ @SuppressWarnings("SameParameterValue")
+ int getIntProperty(MemorySegment propertyKey) {
+ var propertyTypeHolder = arena.allocate(JAVA_INT);
+ var propertyValueHolder = arena.allocate(JAVA_INT);
+ if (SetupDiGetDevicePropertyW(errorState, devInfoSet, devInfoData, propertyKey, propertyTypeHolder,
+ propertyValueHolder, (int) propertyValueHolder.byteSize(), NULL, 0) == 0)
+ throwLastError(errorState, "internal error (SetupDiGetDevicePropertyW - A)");
+
+ if (propertyTypeHolder.get(JAVA_INT, 0) != DEVPROP_TYPE_UINT32)
+ throwException("internal error (expected property type UINT32)");
+
+ return propertyValueHolder.get(JAVA_INT, 0);
+ }
+
+ /**
+ * Gets the string device property of the current element.
+ *
+ * @param propertyKey property key (of type {@code DEVPKEY})
+ * @return property value
+ */
+ String getStringProperty(MemorySegment propertyKey) {
+ var propertyValue = getVariableLengthProperty(propertyKey, DEVPROP_TYPE_STRING, arena);
+ if (propertyValue == null)
+ return null;
+ return propertyValue.getString(0, UTF_16LE);
+ }
+
+ /**
+ * Gets the string list device property of the current element.
+ *
+ * @param propertyKey property key (of type {@code DEVPKEY})
+ * @return property value
+ */
+ @SuppressWarnings("java:S1168")
+ List getStringListProperty(MemorySegment propertyKey) {
+ var propertyValue = getVariableLengthProperty(propertyKey,
+ DEVPROP_TYPE_STRING | DEVPROP_TYPEMOD_LIST, arena);
+ if (propertyValue == null)
+ return null;
+
+ return Win.createStringListFromSegment(propertyValue);
+ }
+
+ private MemorySegment getVariableLengthProperty(MemorySegment propertyKey, int propertyType, Arena arena) {
+
+ // query length (thus no buffer)
+ var propertyTypeHolder = arena.allocate(JAVA_INT);
+ var requiredSizeHolder = arena.allocate(JAVA_INT);
+ if (SetupDiGetDevicePropertyW(errorState, devInfoSet, devInfoData, propertyKey, propertyTypeHolder, NULL, 0,
+ requiredSizeHolder, 0) == 0) {
+ var err = Win.getLastError(errorState);
+ if (err == ERROR_NOT_FOUND)
+ return null;
+ if (err != ERROR_INSUFFICIENT_BUFFER)
+ throwException(err, "internal error (SetupDiGetDevicePropertyW - B)");
+ }
+
+ if (propertyTypeHolder.get(JAVA_INT, 0) != propertyType)
+ throwException("internal error (unexpected property type)");
+
+ var stringLen = (requiredSizeHolder.get(JAVA_INT, 0) + 1) / 2;
+
+ // allocate buffer
+ var propertyValueHolder = arena.allocate(JAVA_CHAR, stringLen);
+
+ // get property value
+ if (SetupDiGetDevicePropertyW(errorState, devInfoSet, devInfoData, propertyKey, propertyTypeHolder,
+ propertyValueHolder, (int) propertyValueHolder.byteSize(), NULL, 0) == 0)
+ throwLastError(errorState, "internal error (SetupDiGetDevicePropertyW - C)");
+
+ return propertyValueHolder;
+ }
+
+ /**
+ * Gets the device path for the device with the given device instance ID and device interface class.
+ *
+ * @param instanceId device instance ID
+ * @param interfaceGuid device interface class GUID
+ * @return the device path
+ */
+ static String getDevicePath(String instanceId, MemorySegment interfaceGuid) {
+ try (var deviceInfoSet = DeviceInfoSet.ofPresentDevices(interfaceGuid, instanceId)) {
+ return deviceInfoSet.getDevicePathForGuid(interfaceGuid);
+ }
+ }
+
+ private String getDevicePathForGuid(MemorySegment interfaceGuid) {
+ // retrieve first element of enumeration
+ devIntfData = SP_DEVICE_INTERFACE_DATA.allocate(arena);
+ if (SetupDiEnumDeviceInterfaces(errorState, devInfoSet, NULL, interfaceGuid, 0, devIntfData) == 0)
+ throwLastError(errorState, "internal error (SetupDiEnumDeviceInterfaces)");
+
+ // get device path
+ var intfDetailData = SP_DEVICE_INTERFACE_DETAIL_DATA_W.allocate(arena, 260);
+ if (SetupDiGetDeviceInterfaceDetailW(errorState, devInfoSet, devIntfData, intfDetailData,
+ (int) intfDetailData.byteSize(), NULL, NULL) == 0)
+ throwLastError(errorState, "Internal error (SetupDiGetDeviceInterfaceDetailW)");
+
+ var devicePath = SP_DEVICE_INTERFACE_DETAIL_DATA_W.DevicePath(intfDetailData);
+ return devicePath.getString(0, UTF_16LE);
+ }
+}
diff --git a/java-does-usb/src/main/java/net/codecrete/usb/windows/DeviceProperty.java b/java-does-usb/src/main/java/net/codecrete/usb/windows/DeviceProperty.java
deleted file mode 100644
index a57aa7b3..00000000
--- a/java-does-usb/src/main/java/net/codecrete/usb/windows/DeviceProperty.java
+++ /dev/null
@@ -1,190 +0,0 @@
-//
-// Java Does USB
-// Copyright (c) 2022 Manuel Bleichenbacher
-// Licensed under MIT License
-// https://opensource.org/licenses/MIT
-//
-
-package net.codecrete.usb.windows;
-
-import net.codecrete.usb.USBException;
-import net.codecrete.usb.windows.gen.advapi32.Advapi32;
-import net.codecrete.usb.windows.gen.kernel32.GUID;
-import net.codecrete.usb.windows.gen.kernel32.Kernel32;
-import net.codecrete.usb.windows.gen.setupapi.SP_DEVICE_INTERFACE_DATA;
-import net.codecrete.usb.windows.gen.setupapi.SP_DEVICE_INTERFACE_DETAIL_DATA_W;
-import net.codecrete.usb.windows.gen.setupapi.SetupAPI;
-
-import java.lang.foreign.Addressable;
-import java.lang.foreign.MemoryAddress;
-import java.lang.foreign.MemorySegment;
-import java.lang.foreign.MemorySession;
-import java.util.List;
-
-import static java.lang.foreign.MemoryAddress.NULL;
-import static java.lang.foreign.ValueLayout.JAVA_CHAR;
-import static java.lang.foreign.ValueLayout.JAVA_INT;
-
-/**
- * Device property GUIDs and functions
- */
-public class DeviceProperty {
-
- public static final MemorySegment DEVPKEY_Device_Address = createDEVPROPKEY(0xa45c254e, (short) 0xdf1c,
- (short) 0x4efd, (byte) 0x80, (byte) 0x20, (byte) 0x67, (byte) 0xd1, (byte) 0x46, (byte) 0xa8, (byte) 0x50
- , (byte) 0xe0, 30);
-
- public static final MemorySegment DEVPKEY_Device_InstanceId = createDEVPROPKEY(0x78c34fc8, (short) 0x104a,
- (short) 0x4aca, (byte) 0x9e, (byte) 0xa4, (byte) 0x52, (byte) 0x4d, (byte) 0x52, (byte) 0x99, (byte) 0x6e
- , (byte) 0x57, 256);
-
- public static final MemorySegment DEVPKEY_Device_Parent = createDEVPROPKEY(0x4340a6c5, (short) 0x93fa,
- (short) 0x4706, (byte) 0x97, (byte) 0x2c, (byte) 0x7b, (byte) 0x64, (byte) 0x80, (byte) 0x08, (byte) 0xa5
- , (byte) 0xa7, 8);
-
- public static final MemorySegment DEVPKEY_Device_Service = createDEVPROPKEY(0xa45c254e, (short) 0xdf1c,
- (short) 0x4efd, (byte) 0x80, (byte) 0x20, (byte) 0x67, (byte) 0xd1, (byte) 0x46, (byte) 0xa8, (byte) 0x50,
- (byte) 0xe0, 6);
-
- public static final MemorySegment DEVPKEY_Device_Children = createDEVPROPKEY(0x4340a6c5, (short) 0x93fa,
- (short) 0x4706, (byte) 0x97, (byte) 0x2c, (byte) 0x7b, (byte) 0x64, (byte) 0x80, (byte) 0x08, (byte) 0xa5,
- (byte) 0xa7, 9);
-
- public static final MemorySegment DEVPKEY_Device_HardwareIds = createDEVPROPKEY(0xa45c254e, (short) 0xdf1c,
- (short) 0x4efd, (byte) 0x80, (byte) 0x20, (byte) 0x67, (byte) 0xd1, (byte) 0x46, (byte) 0xa8, (byte) 0x50,
- (byte) 0xe0, 3);
-
- public static int getDeviceIntProperty(Addressable devInfo, Addressable devInfoData, Addressable propertyKey) {
- try (var session = MemorySession.openConfined()) {
- var propertyTypeHolder = session.allocate(JAVA_INT);
- var propertyValueHolder = session.allocate(JAVA_INT);
- if (SetupAPI.SetupDiGetDevicePropertyW(devInfo, devInfoData, propertyKey, propertyTypeHolder,
- propertyValueHolder, (int) propertyValueHolder.byteSize(), NULL, 0) == 0)
- throw new WindowsUSBException("Internal error (SetupDiGetDevicePropertyW)", Kernel32.GetLastError());
-
- if (propertyTypeHolder.get(JAVA_INT, 0) != SetupAPI.DEVPROP_TYPE_UINT32())
- throw new USBException("Internal error (expected property type UINT32)");
-
- return propertyValueHolder.get(JAVA_INT, 0);
- }
- }
-
- public static String getDeviceStringProperty(Addressable devInfo, Addressable devInfoData,
- Addressable propertyKey) {
- try (var session = MemorySession.openConfined()) {
- var propertyValue = getProperty(devInfo, devInfoData, propertyKey,
- SetupAPI.DEVPROP_TYPE_STRING(), session);
- return Win.createStringFromSegment(propertyValue);
- }
- }
-
- public static List getDeviceStringListProperty(Addressable devInfo, Addressable devInfoData,
- Addressable propertyKey) {
- try (var session = MemorySession.openConfined()) {
- var propertyValue = getProperty(devInfo, devInfoData, propertyKey,
- SetupAPI.DEVPROP_TYPE_STRING() | SetupAPI.DEVPROP_TYPEMOD_LIST(), session);
-
- return Win.createStringListFromSegment(propertyValue);
- }
- }
-
- private static MemorySegment getProperty(Addressable devInfo, Addressable devInfoData, Addressable propertyKey,
- int propertyType, MemorySession session) {
- var propertyTypeHolder = session.allocate(JAVA_INT);
- var requiredSizeHolder = session.allocate(JAVA_INT);
- if (SetupAPI.SetupDiGetDevicePropertyW(devInfo, devInfoData, propertyKey, propertyTypeHolder, NULL, 0,
- requiredSizeHolder, 0) == 0) {
- // TODO: Reactivate when proper GetLastError() handling is available
- // int err = Kernel32.GetLastError();
- // if (err != Kernel32.ERROR_INSUFFICIENT_BUFFER())
- // throw new WindowsUSBException("Internal error (SetupDiGetDevicePropertyW)", Kernel32
- // .GetLastError());
- }
-
- if (propertyTypeHolder.get(JAVA_INT, 0) != propertyType)
- throw new USBException("Internal error (unexpected property type)");
-
- int stringLen = requiredSizeHolder.get(JAVA_INT, 0) / 2 - 1;
-
- var propertyValueHolder = session.allocateArray(JAVA_CHAR, stringLen + 1);
- if (SetupAPI.SetupDiGetDevicePropertyW(devInfo, devInfoData, propertyKey, propertyTypeHolder,
- propertyValueHolder, (int) propertyValueHolder.byteSize(), NULL, 0) == 0)
- throw new WindowsUSBException("Internal error (SetupDiGetDevicePropertyW)", Kernel32.GetLastError());
-
- return propertyValueHolder;
- }
-
- public static List findDeviceInterfaceGUIDs(MemoryAddress devInfoSetHandle, MemorySegment devInfo, MemorySession session) {
-
- // open device registry key
- var regKey = SetupAPI.SetupDiOpenDevRegKey(devInfoSetHandle, devInfo, SetupAPI.DICS_FLAG_GLOBAL(),
- 0, SetupAPI.DIREG_DEV(), Advapi32.KEY_READ());
- if (Win.IsInvalidHandle(regKey))
- throw new WindowsUSBException("Cannot open device registry key", Kernel32.GetLastError());
- session.addCloseAction(() -> Advapi32.RegCloseKey(regKey));
-
- // read registry value (without buffer, to query length)
- var keyNameSegment = Win.createSegmentFromString("DeviceInterfaceGUIDs", session);
- var valueTypeHolder = session.allocate(JAVA_INT);
- var valueSizeHolder = session.allocate(JAVA_INT);
- var res = Advapi32.RegQueryValueExW(regKey, keyNameSegment, NULL, valueTypeHolder, NULL, valueSizeHolder);
- if (res == Kernel32.ERROR_FILE_NOT_FOUND())
- return List.of(); // no device interface GUIDs
- if (res != 0 && res != Kernel32.ERROR_MORE_DATA())
- throw new WindowsUSBException("Internal error (RegQueryValueExW)", res);
-
- // read registry value (with buffer)
- var valueSize = valueSizeHolder.get(JAVA_INT, 0);
- var value = session.allocate(valueSize);
- res = Advapi32.RegQueryValueExW(regKey, keyNameSegment, NULL, valueTypeHolder, value, valueSizeHolder);
- if (res != 0)
- throw new WindowsUSBException("Internal error (RegQueryValueExW)", res);
-
- return Win.createStringListFromSegment(value);
- }
-
- public static String getDevicePath(String instanceID, Addressable interfaceGuid) {
- try (var session = MemorySession.openConfined()) {
- // get device info set for instance
- var instanceIDSegment = Win.createSegmentFromString(instanceID, session);
- final var devInfoSetHandle = SetupAPI.SetupDiGetClassDevsW(interfaceGuid, instanceIDSegment, NULL,
- SetupAPI.DIGCF_PRESENT() | SetupAPI.DIGCF_DEVICEINTERFACE());
- if (Win.IsInvalidHandle(devInfoSetHandle))
- throw new USBException("internal error (SetupDiGetClassDevsW)");
-
- // ensure the result is destroyed when the scope is left
- session.addCloseAction(() -> SetupAPI.SetupDiDestroyDeviceInfoList(devInfoSetHandle));
-
- // retrieve first element of enumeration
- var devIntfData = session.allocate(SP_DEVICE_INTERFACE_DATA.$LAYOUT());
- SP_DEVICE_INTERFACE_DATA.cbSize$set(devIntfData, (int) devIntfData.byteSize());
- if (SetupAPI.SetupDiEnumDeviceInterfaces(devInfoSetHandle, NULL, interfaceGuid, 0, devIntfData) == 0)
- throw new USBException("internal error (SetupDiEnumDeviceInterfaces)");
-
- // get device path
- // (SP_DEVICE_INTERFACE_DETAIL_DATA_W is of variable length and requires a bigger allocation so
- // the device path fits)
- final int devicePathOffset = 4;
- var intfDetailData = session.allocate(4 + 260 * 2);
- SP_DEVICE_INTERFACE_DETAIL_DATA_W.cbSize$set(intfDetailData,
- (int) SP_DEVICE_INTERFACE_DETAIL_DATA_W.sizeof());
- int intfDetailDataSize = (int) intfDetailData.byteSize();
- if (SetupAPI.SetupDiGetDeviceInterfaceDetailW(devInfoSetHandle, devIntfData, intfDetailData,
- intfDetailDataSize, NULL, NULL) == 0)
- throw new WindowsUSBException("Internal error (SetupDiGetDeviceInterfaceDetailW)", Kernel32.GetLastError());
-
- return Win.createStringFromSegment(intfDetailData.asSlice(devicePathOffset));
- }
- }
-
- public static MemorySegment createDEVPROPKEY(int data1, short data2, short data3, byte data4_0, byte data4_1,
- byte data4_2, byte data4_3, byte data4_4, byte data4_5, byte data4_6
- , byte data4_7, int pid) {
- var propKey = MemorySession.global().allocate(GUID.sizeof() + JAVA_INT.byteSize());
- Win.setGUID(propKey, data1, data2, data3, data4_0, data4_1, data4_2, data4_3, data4_4, data4_5, data4_6,
- data4_7);
- propKey.set(JAVA_INT, 16, pid);
- return propKey;
- }
-
-}
diff --git a/java-does-usb/src/main/java/net/codecrete/usb/windows/InterfaceHandle.java b/java-does-usb/src/main/java/net/codecrete/usb/windows/InterfaceHandle.java
new file mode 100644
index 00000000..c3d56a74
--- /dev/null
+++ b/java-does-usb/src/main/java/net/codecrete/usb/windows/InterfaceHandle.java
@@ -0,0 +1,45 @@
+//
+// Java Does USB
+// Copyright (c) 2022 Manuel Bleichenbacher
+// Licensed under MIT License
+// https://opensource.org/licenses/MIT
+//
+
+package net.codecrete.usb.windows;
+
+import java.lang.foreign.MemorySegment;
+
+/**
+ * Handles for WinUSB devices and interfaces
+ */
+class InterfaceHandle {
+ InterfaceHandle(int interfaceNumber, int firstInterfaceNumber) {
+ this.interfaceNumber = interfaceNumber;
+ this.firstInterfaceNumber = firstInterfaceNumber;
+ }
+
+ /**
+ * The number of this interface.
+ */
+ final int interfaceNumber;
+ /**
+ * The number of the first interface in the same composite function.
+ */
+ final int firstInterfaceNumber;
+ /**
+ * The file handle of the device.
+ *
+ * This is only used for the first interface in a composite function.
+ *
+ */
+ MemorySegment deviceHandle;
+ /**
+ * The WinUSB handle of the interface.
+ */
+ @SuppressWarnings("java:S1700")
+ MemorySegment winusbHandle;
+ /**
+ * Count indicating how many interface depend on the device being open.
+ */
+ int deviceOpenCount;
+}
diff --git a/java-does-usb/src/main/java/net/codecrete/usb/windows/USBHelper.java b/java-does-usb/src/main/java/net/codecrete/usb/windows/USBHelper.java
deleted file mode 100644
index c0c84fdc..00000000
--- a/java-does-usb/src/main/java/net/codecrete/usb/windows/USBHelper.java
+++ /dev/null
@@ -1,112 +0,0 @@
-//
-// Java Does USB
-// Copyright (c) 2022 Manuel Bleichenbacher
-// Licensed under MIT License
-// https://opensource.org/licenses/MIT
-//
-
-package net.codecrete.usb.windows;
-
-import net.codecrete.usb.common.USBDescriptors;
-import net.codecrete.usb.common.USBStructs;
-
-import java.lang.foreign.GroupLayout;
-import java.lang.foreign.MemorySegment;
-import java.lang.invoke.VarHandle;
-
-import static java.lang.foreign.MemoryLayout.PathElement.groupElement;
-import static java.lang.foreign.MemoryLayout.structLayout;
-import static java.lang.foreign.ValueLayout.*;
-
-/**
- * USB constants and struct
- *
- * Mainly used to work around an alignment problem with the generated
- * USB_NODE_CONNECTION_INFORMATION_EX struct.
- *
- */
-public class USBHelper {
-
- public static final byte USB_REQUEST_GET_DESCRIPTOR = 0x06;
-
-
- // typedef struct _USB_NODE_CONNECTION_INFORMATION_EX {
- // ULONG ConnectionIndex; /* INPUT */
- // /* usb device descriptor returned by this device
- // during enumeration */
- // USB_DEVICE_DESCRIPTOR DeviceDescriptor;/* OUTPUT */
- // UCHAR CurrentConfigurationValue;/* OUTPUT */
- // /* values for the speed field are defined in USB200.h */
- // UCHAR Speed;/* OUTPUT */
- // BOOLEAN DeviceIsHub;/* OUTPUT */
- // USHORT DeviceAddress;/* OUTPUT */
- // ULONG NumberOfOpenPipes;/* OUTPUT */
- // USB_CONNECTION_STATUS ConnectionStatus;/* OUTPUT */
- // USB_PIPE_INFO PipeList[0];/* OUTPUT */
- //} USB_NODE_CONNECTION_INFORMATION_EX, *PUSB_NODE_CONNECTION_INFORMATION_EX;
- public static final GroupLayout USB_NODE_CONNECTION_INFORMATION_EX$Struct = structLayout(JAVA_INT.withName(
- "ConnectionIndex"), USBDescriptors.Device$Struct.withName("DeviceDescriptor"), JAVA_BYTE.withName(
- "CurrentConfigurationValue"), JAVA_BYTE.withName("Speed"), JAVA_BYTE.withName("DeviceIsHub"),
- JAVA_SHORT.withName("DeviceAddress"), JAVA_INT.withName("NumberOfOpenPipes"), JAVA_INT.withName(
- "ConnectionStatus")
- // USB_PIPE_INFO PipeList[0]
- );
- public static final VarHandle USB_NODE_CONNECTION_INFORMATION_EX_ConnectionIndex =
- USB_NODE_CONNECTION_INFORMATION_EX$Struct.varHandle(groupElement("ConnectionIndex"));
- public static final long USB_NODE_CONNECTION_INFORMATION_EX_DeviceDescriptor$Offset =
- USB_NODE_CONNECTION_INFORMATION_EX$Struct.byteOffset(groupElement("DeviceDescriptor"));
-
- public static MemorySegment USB_NODE_CONNECTION_INFORMATION_EX_DeviceDescriptor$slice(MemorySegment seg) {
- return seg.asSlice(USB_NODE_CONNECTION_INFORMATION_EX_DeviceDescriptor$Offset,
- USBDescriptors.Device$Struct.byteSize());
- }
-
- public static final VarHandle USB_NODE_CONNECTION_INFORMATION_EX_CurrentConfigurationValue =
- USB_NODE_CONNECTION_INFORMATION_EX$Struct.varHandle(groupElement("CurrentConfigurationValue"));
-
- // typedef struct _USB_DESCRIPTOR_REQUEST {
- // ULONG ConnectionIndex;
- // struct {
- // UCHAR bmRequest;
- // UCHAR bRequest;
- // USHORT wValue;
- // USHORT wIndex;
- // USHORT wLength;
- // } SetupPacket;
- // UCHAR Data[0];
- //} USB_DESCRIPTOR_REQUEST, *PUSB_DESCRIPTOR_REQUEST;
- public static final GroupLayout USB_DESCRIPTOR_REQUEST$Struct = structLayout(JAVA_INT.withName("ConnectionIndex")
- , USBStructs.SetupPacket$Struct.withName("SetupPacket"));
- public static final VarHandle USB_DESCRIPTOR_REQUEST_ConnectionIndex =
- USB_DESCRIPTOR_REQUEST$Struct.varHandle(groupElement("ConnectionIndex"));
- public static final long USB_DESCRIPTOR_REQUEST_SetupPacket$Offset =
- USB_DESCRIPTOR_REQUEST$Struct.byteOffset(groupElement("SetupPacket"));
- public static final long USB_DESCRIPTOR_REQUEST_Data$Offset = USB_DESCRIPTOR_REQUEST$Struct.byteSize();
-
- // typedef struct _USB_STRING_DESCRIPTOR {
- // UCHAR bLength;
- // UCHAR bDescriptorType;
- // WCHAR bString[1];
- //} USB_STRING_DESCRIPTOR, *PUSB_STRING_DESCRIPTOR;
- public static final GroupLayout USB_STRING_DESCRIPTOR$Struct = structLayout(JAVA_BYTE.withName("bLength"),
- JAVA_BYTE.withName("bDescriptorType"));
- public static final VarHandle USB_STRING_DESCRIPTOR_bLength =
- USB_STRING_DESCRIPTOR$Struct.varHandle(groupElement("bLength"));
- public static final VarHandle USB_STRING_DESCRIPTOR_bDescriptorType =
- USB_STRING_DESCRIPTOR$Struct.varHandle(groupElement("bDescriptorType"));
- public static final long USB_STRING_DESCRIPTOR_bString$Offset = USB_STRING_DESCRIPTOR$Struct.byteSize();
-
- // A5DCBF10-6530-11D2-901F-00C04FB951ED
- public static final MemorySegment GUID_DEVINTERFACE_USB_DEVICE = Win.CreateGUID(0xA5DCBF10, (short) 0x6530,
- (short) 0x11D2, (byte) 0x90, (byte) 0x1F, (byte) 0x00, (byte) 0xC0, (byte) 0x4F, (byte) 0xB9, (byte) 0x51
- , (byte) 0xED);
-
- // f18a0e88-c30c-11d0-8815-00a0c906bed8
- public static final MemorySegment GUID_DEVINTERFACE_USB_HUB = Win.CreateGUID(0xf18a0e88, (short) 0xc30c,
- (short) 0x11d0, (byte) 0x88, (byte) 0x15, (byte) 0x00, (byte) 0xa0, (byte) 0xc9, (byte) 0x06, (byte) 0xbe
- , (byte) 0xd8);
-
- static {
- assert USB_NODE_CONNECTION_INFORMATION_EX$Struct.byteSize() == 35;
- }
-}
diff --git a/java-does-usb/src/main/java/net/codecrete/usb/windows/Win.java b/java-does-usb/src/main/java/net/codecrete/usb/windows/Win.java
index ac2e51a0..6f5791bf 100644
--- a/java-does-usb/src/main/java/net/codecrete/usb/windows/Win.java
+++ b/java-does-usb/src/main/java/net/codecrete/usb/windows/Win.java
@@ -7,70 +7,72 @@
package net.codecrete.usb.windows;
-import net.codecrete.usb.windows.gen.kernel32.GUID;
-import net.codecrete.usb.windows.gen.stdlib.StdLib;
-
-import java.lang.foreign.MemoryAddress;
+import java.lang.foreign.Arena;
+import java.lang.foreign.Linker;
+import java.lang.foreign.MemoryLayout.PathElement;
import java.lang.foreign.MemorySegment;
-import java.lang.foreign.MemorySession;
-import java.lang.foreign.ValueLayout;
+import java.lang.foreign.StructLayout;
+import java.lang.invoke.VarHandle;
import java.util.ArrayList;
import java.util.List;
-import static java.lang.foreign.ValueLayout.JAVA_BYTE;
import static java.lang.foreign.ValueLayout.JAVA_CHAR;
+import static java.nio.charset.StandardCharsets.UTF_16LE;
/**
* Windows helpers.
*/
public class Win {
+ private Win() {
+ }
/**
- * Checks if a Windows handle is invalid.
+ * Call state for capturing the {@code GetLastError()} value.
+ */
+ public static final Linker.Option LAST_ERROR_STATE = Linker.Option.captureCallState("GetLastError");
+ private static final StructLayout LAST_ERROR_STATE_LAYOUT = Linker.Option.captureStateLayout();
+
+ private static final VarHandle callState_GetLastError$VH =
+ LAST_ERROR_STATE_LAYOUT.varHandle(PathElement.groupElement("GetLastError"));
+
+ static MemorySegment allocateErrorState(Arena arena) {
+ return arena.allocate(LAST_ERROR_STATE_LAYOUT);
+ }
+
+ /**
+ * Returns the error code captured using the call state {@link #LAST_ERROR_STATE}.
*
- * @param handle Windows handle
- * @return {@code true} if the handle is invalid, {@code false} otherwise
+ * @param callState the call state
+ * @return the error code
*/
- public static boolean IsInvalidHandle(MemoryAddress handle) {
- return handle.toRawLongValue() == -1L;
+ public static int getLastError(MemorySegment callState) {
+ return (int) callState_GetLastError$VH.get(callState, 0);
}
/**
- * Creates a memory segment as a copy of a Java string.
- *
- * The memory segment contains a copy of the string (null-terminated, UTF-16/wide characters).
- *
+ * Checks if a Windows handle is invalid.
*
- * @param str the string to copy
- * @param session the memory session for the memory segment
- * @return the resulting memory segment
+ * @param handle Windows handle
+ * @return {@code true} if the handle is invalid, {@code false} otherwise
*/
- public static MemorySegment createSegmentFromString(String str, MemorySession session) {
- // allocate segment (including space for terminating null)
- var segment = session.allocateArray(ValueLayout.JAVA_CHAR, str.length() + 1);
- // copy characters
- segment.copyFrom(MemorySegment.ofArray(str.toCharArray()));
- return segment;
+ public static boolean isInvalidHandle(MemorySegment handle) {
+ return handle.address() == -1L;
}
/**
- * Creates a copy of the string in the memory segment.
- *
- * The string must be a null-terminated UTF-16 (wide character) string.
- *
+ * Checks if a Windows handle is invalid.
*
- * @param segment the memory segment
- * @return copied string
+ * @param handle Windows handle
+ * @return {@code true} if the handle is invalid, {@code false} otherwise
*/
- public static String createStringFromSegment(MemorySegment segment) {
- long strLen = StdLib.wcslen(segment);
- return new String(segment.asSlice(0, 2L * strLen).toArray(JAVA_CHAR));
+ public static boolean isInvalidHandle(long handle) {
+ return handle == -1L;
}
/**
* Creates a copy of the string list in the memory segment.
*
- * The string list a a series of null-terminated UTF-16 (wide character) strings.
+ * The string list is a series of null-terminated UTF-16 (wide character) strings.
* The list is terminated with yet another null character.
*
*
@@ -79,52 +81,12 @@ public static String createStringFromSegment(MemorySegment segment) {
*/
public static List createStringListFromSegment(MemorySegment segment) {
var stringList = new ArrayList();
- int offset = 0;
+ var offset = 0;
while (segment.get(JAVA_CHAR, offset) != '\0') {
- var str = Win.createStringFromSegment(segment.asSlice(offset));
+ var str = segment.getString(offset, UTF_16LE);
offset += str.length() * 2 + 2;
stringList.add(str);
}
return stringList;
}
-
- /**
- * Creates a GUID in native memory.
- *
- * @param data1 Group 1 (4 bytes).
- * @param data2 Group 2 (2 bytes).
- * @param data3 Group 3 (2 bytes).
- * @param data4_0 Byte 0 of group 4
- * @param data4_1 Byte 1 of group 4
- * @param data4_2 Byte 2 of group 4
- * @param data4_3 Byte 3 of group 4
- * @param data4_4 Byte 4 of group 4
- * @param data4_5 Byte 5 of group 4
- * @param data4_6 Byte 6 of group 4
- * @param data4_7 Byte 7 of group 4
- * @return GUID as memory segment
- */
- public static MemorySegment CreateGUID(int data1, short data2, short data3, byte data4_0, byte data4_1,
- byte data4_2, byte data4_3, byte data4_4, byte data4_5, byte data4_6,
- byte data4_7) {
- var guid = MemorySegment.allocateNative(GUID.$LAYOUT(), MemorySession.global());
- setGUID(guid, data1, data2, data3, data4_0, data4_1, data4_2, data4_3, data4_4, data4_5, data4_6, data4_7);
- return guid;
- }
-
- public static void setGUID(MemorySegment guid, int data1, short data2, short data3, byte data4_0, byte data4_1,
- byte data4_2, byte data4_3, byte data4_4, byte data4_5, byte data4_6, byte data4_7) {
- GUID.Data1$set(guid, data1);
- GUID.Data2$set(guid, data2);
- GUID.Data3$set(guid, data3);
- var data4 = GUID.Data4$slice(guid);
- data4.set(JAVA_BYTE, 0, data4_0);
- data4.set(JAVA_BYTE, 1, data4_1);
- data4.set(JAVA_BYTE, 2, data4_2);
- data4.set(JAVA_BYTE, 3, data4_3);
- data4.set(JAVA_BYTE, 4, data4_4);
- data4.set(JAVA_BYTE, 5, data4_5);
- data4.set(JAVA_BYTE, 6, data4_6);
- data4.set(JAVA_BYTE, 7, data4_7);
- }
}
diff --git a/java-does-usb/src/main/java/net/codecrete/usb/windows/WindowsAsyncTask.java b/java-does-usb/src/main/java/net/codecrete/usb/windows/WindowsAsyncTask.java
new file mode 100644
index 00000000..fa31dad2
--- /dev/null
+++ b/java-does-usb/src/main/java/net/codecrete/usb/windows/WindowsAsyncTask.java
@@ -0,0 +1,258 @@
+//
+// Java Does USB
+// Copyright (c) 2023 Manuel Bleichenbacher
+// Licensed under MIT License
+// https://opensource.org/licenses/MIT
+//
+
+package net.codecrete.usb.windows;
+
+import net.codecrete.usb.UsbException;
+import windows.win32.system.io.OVERLAPPED;
+
+import java.lang.foreign.Arena;
+import java.lang.foreign.MemorySegment;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import static java.lang.System.Logger.Level.ERROR;
+import static java.lang.foreign.MemorySegment.NULL;
+import static java.lang.foreign.ValueLayout.ADDRESS;
+import static java.lang.foreign.ValueLayout.JAVA_INT;
+import static java.lang.foreign.ValueLayout.JAVA_LONG;
+import static net.codecrete.usb.windows.Win.allocateErrorState;
+import static net.codecrete.usb.windows.WindowsUsbException.throwLastError;
+import static windows.win32.foundation.WIN32_ERROR.ERROR_OPERATION_ABORTED;
+import static windows.win32.system.io.Apis.CreateIoCompletionPort;
+import static windows.win32.system.io.Apis.GetQueuedCompletionStatus;
+import static windows.win32.system.threading.Constants.INFINITE;
+
+/**
+ * Background task for handling asynchronous transfers.
+ *
+ * Each USB device must register its handle with this task.
+ *
+ *
+ * The task keeps track of the submitted transfers by indexing them
+ * by OVERLAPPED struct address.
+ *
+ *
+ * OVERLAPPED structs are allocated but never freed. To limit the memory usage,
+ * OVERLAPPED structs are reused. So the maximum number of outstanding transfers
+ * determines the number of allocated OVERLAPPED structs.
+ *
+ */
+@SuppressWarnings("java:S6548")
+class WindowsAsyncTask {
+
+ private static final System.Logger LOG = System.getLogger(WindowsAsyncTask.class.getName());
+
+ /**
+ * Singleton instance of background task.
+ */
+ static final WindowsAsyncTask INSTANCE = new WindowsAsyncTask();
+
+ // Currently outstanding transfer requests,
+ // indexed by OVERLAPPED address.
+ private Map requestsByOverlapped;
+ // available OVERLAPPED data structures
+ private List availableOverlappedStructs;
+ // Arena used to allocate OVERLAPPED data structures
+ private Arena overlappedArena;
+
+ /**
+ * Windows completion port for asynchronous/overlapped IO
+ */
+ private MemorySegment asyncIoCompletionPort = NULL;
+
+ /**
+ * Indicates that the background task has terminated due to an unrecoverable error.
+ */
+ private boolean taskTerminated;
+
+ /**
+ * Background task for handling asynchronous IO completions.
+ */
+ @SuppressWarnings("java:S2189")
+ private void asyncCompletionTask() {
+
+ try (var arena = Arena.ofConfined()) {
+
+ var overlappedHolder = arena.allocate(ADDRESS);
+ var numBytesHolder = arena.allocate(JAVA_INT);
+ var completionKeyHolder = arena.allocate(JAVA_LONG);
+ var errorState = allocateErrorState(arena);
+
+ while (true) {
+ try {
+ overlappedHolder.set(ADDRESS, 0, NULL);
+ completionKeyHolder.set(JAVA_LONG, 0, 0);
+
+ var res = GetQueuedCompletionStatus(errorState, asyncIoCompletionPort, numBytesHolder,
+ completionKeyHolder, overlappedHolder, INFINITE);
+ var overlappedAddr = overlappedHolder.get(JAVA_LONG, 0);
+
+ // A null OVERLAPPED means no completion packet was dequeued (nothing posts
+ // packets without an OVERLAPPED): the completion port itself has failed,
+ // and no further completions will ever be delivered.
+ if (overlappedAddr == 0) {
+ var success = res != 0;
+ throwLastError(errorState, "internal error (GetQueuedCompletionStatus, success: %s)", success);
+ }
+
+ completeTransfer(overlappedAddr);
+
+ } catch (Exception e) {
+ LOG.log(ERROR, "USB async IO thread failed and is terminating; "
+ + "all outstanding transfers will fail, and no further transfers are possible", e);
+ failAllPendingTransfers();
+ return;
+ }
+ }
+ }
+ }
+
+ /**
+ * Fails all outstanding transfers and marks this task as terminated.
+ *
+ * Called when the background task can no longer dispatch completions. Waiters blocked
+ * on the failed transfers wake up with an error result instead of hanging forever,
+ * and future submissions are rejected.
+ *
+ */
+ private void failAllPendingTransfers() {
+ List pendingTransfers;
+ synchronized (this) {
+ taskTerminated = true;
+ pendingTransfers = new ArrayList<>(requestsByOverlapped.values());
+ requestsByOverlapped.clear();
+ availableOverlappedStructs.clear();
+ for (var transfer : pendingTransfers) {
+ transfer.setResultCode(ERROR_OPERATION_ABORTED);
+ transfer.setResultSize(0);
+ transfer.setOverlapped(null);
+ }
+ }
+
+ for (var transfer : pendingTransfers) {
+ try {
+ transfer.completion().completed(transfer);
+ } catch (Exception e) {
+ LOG.log(ERROR, "Unexpected exception while handling async IO completion", e);
+ }
+ }
+ }
+
+ /**
+ * Add a Windows handle (of a USB device) to the completion port.
+ *
+ * The handle is removed by closing it.
+ *
+ *
+ * @param handle Windows handle
+ */
+ synchronized void addDevice(MemorySegment handle) {
+
+ try (var arena = Arena.ofConfined()) {
+ var errorState = allocateErrorState(arena);
+
+ // Creates a new port if it doesn't exist; adds handle to existing port if it exists
+ var portHandle = CreateIoCompletionPort(errorState, handle, asyncIoCompletionPort,
+ handle.address(), 0);
+ if (portHandle == MemorySegment.NULL)
+ throwLastError(errorState, "internal error (CreateIoCompletionPort)");
+
+ if (asyncIoCompletionPort == MemorySegment.NULL) {
+ asyncIoCompletionPort = portHandle;
+ startAsyncIOTask();
+ }
+ }
+ }
+
+ private void startAsyncIOTask() {
+ availableOverlappedStructs = new ArrayList<>();
+ overlappedArena = Arena.ofAuto();
+ requestsByOverlapped = new HashMap<>();
+
+ // start background thread for handling IO completion
+ var thread = new Thread(this::asyncCompletionTask, "USB async IO");
+ thread.setDaemon(true);
+ thread.start();
+ }
+
+ /**
+ * Prepare a transfer for submission by adding the OVERLAPPED struct.
+ *
+ * @param transfer transfer to prepare
+ */
+ synchronized void prepareForSubmission(WindowsTransfer transfer) {
+ if (taskTerminated)
+ throw new UsbException("USB async IO background thread has terminated due to an unrecoverable error; "
+ + "USB transfers are no longer possible");
+
+ MemorySegment overlapped;
+ var size = availableOverlappedStructs.size();
+ if (size == 0) {
+ overlapped = OVERLAPPED.allocate(overlappedArena);
+ } else {
+ overlapped = availableOverlappedStructs.remove(size - 1);
+ }
+
+ transfer.setOverlapped(overlapped);
+ transfer.setResultSize(-1);
+ requestsByOverlapped.put(overlapped.address(), transfer);
+ }
+
+ /**
+ * Undoes the registration performed by {@link #prepareForSubmission(WindowsTransfer)}.
+ *
+ * Must be called if the native submission of a prepared transfer fails synchronously.
+ * In that case, no completion packet will ever be posted for the transfer, so its map
+ * entry would leak and the OVERLAPPED struct would never return to the pool unless
+ * they are cleaned up here.
+ *
+ *
+ * @param transfer transfer whose submission failed
+ */
+ synchronized void submissionFailed(WindowsTransfer transfer) {
+ requestsByOverlapped.remove(transfer.overlapped().address());
+ availableOverlappedStructs.add(transfer.overlapped());
+ transfer.setOverlapped(null);
+ }
+
+ /**
+ * Completes the transfer by calling the completion handler.
+ *
+ * @param overlappedAddr address of OVERLAPPED struct
+ */
+ private void completeTransfer(long overlappedAddr) {
+ WindowsTransfer transfer;
+ synchronized (this) {
+ transfer = requestsByOverlapped.remove(overlappedAddr);
+ if (transfer == null)
+ return;
+
+ // the results must be read from the OVERLAPPED struct before it is
+ // returned to the pool and possibly reused by another submission
+ transfer.setResultCode((int) OVERLAPPED.Internal(transfer.overlapped()));
+ transfer.setResultSize((int) OVERLAPPED.InternalHigh(transfer.overlapped()));
+
+ availableOverlappedStructs.add(transfer.overlapped());
+ transfer.setOverlapped(null);
+ }
+
+ // The completion handler must be called without holding the lock: handlers acquire
+ // other monitors (transfer, device), and threads submitting transfers acquire this
+ // task's lock while holding those monitors, so calling handlers under the lock can
+ // deadlock.
+ try {
+ transfer.completion().completed(transfer);
+ } catch (Exception e) {
+ // This method runs on the process-wide async IO thread. Any exception escaping
+ // would kill that thread and hang all async transfers for the entire library.
+ LOG.log(ERROR, "Unexpected exception while handling async IO completion", e);
+ }
+ }
+}
diff --git a/java-does-usb/src/main/java/net/codecrete/usb/windows/WindowsEndpointInputStream.java b/java-does-usb/src/main/java/net/codecrete/usb/windows/WindowsEndpointInputStream.java
new file mode 100644
index 00000000..0c96fc94
--- /dev/null
+++ b/java-does-usb/src/main/java/net/codecrete/usb/windows/WindowsEndpointInputStream.java
@@ -0,0 +1,29 @@
+//
+// Java Does USB
+// Copyright (c) 2023 Manuel Bleichenbacher
+// Licensed under MIT License
+// https://opensource.org/licenses/MIT
+//
+
+package net.codecrete.usb.windows;
+
+import net.codecrete.usb.UsbDirection;
+import net.codecrete.usb.common.EndpointInputStream;
+import net.codecrete.usb.common.Transfer;
+
+public class WindowsEndpointInputStream extends EndpointInputStream {
+
+ WindowsEndpointInputStream(WindowsUsbDevice device, int endpointNumber, int bufferSize) {
+ super(device, endpointNumber, bufferSize);
+ }
+
+ @Override
+ protected void submitTransferIn(Transfer transfer) {
+ ((WindowsUsbDevice) device).submitTransferIn(endpointNumber, (WindowsTransfer) transfer);
+ }
+
+ @Override
+ protected void configureEndpoint() {
+ ((WindowsUsbDevice) device).configureForAsyncIo(UsbDirection.IN, endpointNumber);
+ }
+}
diff --git a/java-does-usb/src/main/java/net/codecrete/usb/windows/WindowsEndpointOutputStream.java b/java-does-usb/src/main/java/net/codecrete/usb/windows/WindowsEndpointOutputStream.java
new file mode 100644
index 00000000..8125dd5a
--- /dev/null
+++ b/java-does-usb/src/main/java/net/codecrete/usb/windows/WindowsEndpointOutputStream.java
@@ -0,0 +1,29 @@
+//
+// Java Does USB
+// Copyright (c) 2023 Manuel Bleichenbacher
+// Licensed under MIT License
+// https://opensource.org/licenses/MIT
+//
+
+package net.codecrete.usb.windows;
+
+import net.codecrete.usb.UsbDirection;
+import net.codecrete.usb.common.EndpointOutputStream;
+import net.codecrete.usb.common.Transfer;
+
+public class WindowsEndpointOutputStream extends EndpointOutputStream {
+
+ WindowsEndpointOutputStream(WindowsUsbDevice device, int endpointNumber, int bufferSize) {
+ super(device, endpointNumber, bufferSize);
+ }
+
+ @Override
+ protected void submitTransferOut(Transfer request) {
+ ((WindowsUsbDevice) device).submitTransferOut(endpointNumber, (WindowsTransfer) request);
+ }
+
+ @Override
+ protected void configureEndpoint() {
+ ((WindowsUsbDevice) device).configureForAsyncIo(UsbDirection.OUT, endpointNumber);
+ }
+}
diff --git a/java-does-usb/src/main/java/net/codecrete/usb/windows/WindowsTransfer.java b/java-does-usb/src/main/java/net/codecrete/usb/windows/WindowsTransfer.java
new file mode 100644
index 00000000..9b45f4bb
--- /dev/null
+++ b/java-does-usb/src/main/java/net/codecrete/usb/windows/WindowsTransfer.java
@@ -0,0 +1,24 @@
+//
+// Java Does USB
+// Copyright (c) 2023 Manuel Bleichenbacher
+// Licensed under MIT License
+// https://opensource.org/licenses/MIT
+//
+
+package net.codecrete.usb.windows;
+
+import net.codecrete.usb.common.Transfer;
+
+import java.lang.foreign.MemorySegment;
+
+class WindowsTransfer extends Transfer {
+ private MemorySegment overlapped;
+
+ public MemorySegment overlapped() {
+ return overlapped;
+ }
+
+ public void setOverlapped(MemorySegment overlapped) {
+ this.overlapped = overlapped;
+ }
+}
diff --git a/java-does-usb/src/main/java/net/codecrete/usb/windows/WindowsUSBDevice.java b/java-does-usb/src/main/java/net/codecrete/usb/windows/WindowsUSBDevice.java
deleted file mode 100644
index f4fa3c7a..00000000
--- a/java-does-usb/src/main/java/net/codecrete/usb/windows/WindowsUSBDevice.java
+++ /dev/null
@@ -1,269 +0,0 @@
-//
-// Java Does USB
-// Copyright (c) 2022 Manuel Bleichenbacher
-// Licensed under MIT License
-// https://opensource.org/licenses/MIT
-//
-
-package net.codecrete.usb.windows;
-
-import net.codecrete.usb.*;
-import net.codecrete.usb.common.DescriptorParser;
-import net.codecrete.usb.common.USBDeviceImpl;
-import net.codecrete.usb.common.USBStructs;
-import net.codecrete.usb.windows.gen.kernel32.Kernel32;
-import net.codecrete.usb.windows.gen.winusb.WinUSB;
-
-import java.lang.foreign.MemorySegment;
-import java.lang.foreign.MemorySession;
-import java.util.List;
-
-import static java.lang.foreign.MemoryAddress.NULL;
-import static java.lang.foreign.ValueLayout.*;
-
-/**
- * Windows implementation for USB device.
- */
-public class WindowsUSBDevice extends USBDeviceImpl {
-
- private final List functions;
- private boolean isOpen_;
-
- WindowsUSBDevice(String devicePath, List functions, int vendorId, int productId, String manufacturer, String product, String serial,
- MemorySegment configDesc) {
- super(devicePath, vendorId, productId, manufacturer, product, serial);
- this.functions = functions;
- readDescription(configDesc);
- }
-
- @Override
- public boolean isOpen() {
- return isOpen_;
- }
-
- @Override
- public void open() {
- if (isOpen())
- throw new USBException("the device is already open");
-
- isOpen_ = true;
- }
-
- private void readDescription(MemorySegment configDesc) {
- var configuration = DescriptorParser.parseConfigurationDescriptor(configDesc, vendorId(), productId());
- setInterfaces(configuration.interfaces);
- }
-
- @Override
- public void close() {
- if (!isOpen())
- return;
-
- for (var intf : interfaces_) {
- if (intf.isClaimed())
- releaseInterface(intf.number());
- }
-
- isOpen_ = false;
- }
-
- private CompositeFunction findFunction(int interfaceNumber) {
- return functions.stream()
- .filter((func) -> func.firstInterfaceNumber() == interfaceNumber).findFirst().orElse(null);
- }
-
- private CompositeFunction findAnyOpenFunction() {
- return functions.stream()
- .filter((func) -> func.firstInterfaceHandle() != null).findFirst().orElse(null);
- }
-
- private CompositeFunction findControlTransferFunction(USBControlTransfer setup) {
-
- int interfaceNumber = -1;
- int endpointNumber;
-
- if (setup.recipient() == USBRecipient.INTERFACE) {
-
- interfaceNumber = setup.index() & 0xff;
-
- } else if (setup.recipient() == USBRecipient.ENDPOINT) {
-
- endpointNumber = setup.index() & 0xff;
- if (endpointNumber != 0) {
- interfaceNumber = getInterfaceNumber(endpointNumber);
- if (interfaceNumber == -1)
- interfaceNumber = -2;
- }
- }
-
- CompositeFunction function = null;
- if (interfaceNumber >= 0) {
- function = findFunction(interfaceNumber);
- } else if (interfaceNumber == -1) {
- function = findAnyOpenFunction();
- }
-
- if (function == null || function.firstInterfaceHandle() == null)
- throw new USBException("Interface not claimed for control transfer");
-
- return function;
- }
-
- public void claimInterface(int interfaceNumber) {
- checkIsOpen();
-
- var intf = getInterface(interfaceNumber);
- if (intf == null)
- throw new USBException(String.format("Invalid interface number: %d", interfaceNumber));
- if (intf.isClaimed())
- throw new USBException(String.format("Interface %d has already been claimed", interfaceNumber));
- var function = findFunction(interfaceNumber);
- if (function == null)
- throw new USBException(String.format("Interface number %d cannot be claimed (no DeviceInterfaceGUID?)", interfaceNumber));
-
- try (var session = MemorySession.openConfined()) {
-
- // open Windows device
- var pathSegment = Win.createSegmentFromString(function.devicePath(), session);
- var deviceHandle = Kernel32.CreateFileW(pathSegment, Kernel32.GENERIC_WRITE() | Kernel32.GENERIC_READ(),
- Kernel32.FILE_SHARE_WRITE() | Kernel32.FILE_SHARE_READ(), NULL, Kernel32.OPEN_EXISTING(),
- Kernel32.FILE_ATTRIBUTE_NORMAL() | Kernel32.FILE_FLAG_OVERLAPPED(), NULL);
-
- if (Win.IsInvalidHandle(deviceHandle))
- throw new WindowsUSBException(
- String.format("Cannot open USB device %s", function.devicePath()), Kernel32.GetLastError());
-
- try {
- // open interface
- var interfaceHandleHolder = session.allocate(ADDRESS);
- if (WinUSB.WinUsb_Initialize(deviceHandle, interfaceHandleHolder) == 0)
- throw new WindowsUSBException("Cannot open WinUSB device", Kernel32.GetLastError());
- var interfaceHandle = interfaceHandleHolder.get(ADDRESS, 0);
-
- function.setDeviceHandle(deviceHandle);
- function.setFirstInterfaceHandle(interfaceHandle);
-
- } catch (Throwable e) {
- Kernel32.CloseHandle(deviceHandle);
- throw e;
- }
- }
-
- setClaimed(interfaceNumber, true);
- }
-
- public void releaseInterface(int interfaceNumber) {
- checkIsOpen();
-
- var intf = getInterface(interfaceNumber);
- if (intf == null)
- throw new USBException(String.format("Invalid interface number: %d", interfaceNumber));
- if (!intf.isClaimed())
- throw new USBException(String.format("Interface %d has not been claimed", interfaceNumber));
-
- var function = findFunction(interfaceNumber);
- assert function != null;
-
- if (function.deviceHandle() != null) {
- WinUSB.WinUsb_Free(function.firstInterfaceHandle());
- function.setFirstInterfaceHandle(null);
- Kernel32.CloseHandle(function.deviceHandle());
- function.setDeviceHandle(null);
- }
-
- setClaimed(interfaceNumber, false);
- }
-
- private MemorySegment createSetupPacket(MemorySession session, USBDirection direction, USBControlTransfer setup,
- MemorySegment data) {
- var setupPacket = session.allocate(USBStructs.SetupPacket$Struct);
- var bmRequest =
- (direction == USBDirection.IN ? 0x80 : 0) | (setup.requestType().ordinal() << 5) | setup.recipient().ordinal();
- USBStructs.SetupPacket_bmRequest.set(setupPacket, (byte) bmRequest);
- USBStructs.SetupPacket_bRequest.set(setupPacket, setup.request());
- USBStructs.SetupPacket_wValue.set(setupPacket, setup.value());
- USBStructs.SetupPacket_wIndex.set(setupPacket, setup.index());
- USBStructs.SetupPacket_wLength.set(setupPacket, (short) (data != null ? data.byteSize() : 0));
- return setupPacket;
- }
-
- @Override
- public byte[] controlTransferIn(USBControlTransfer setup, int length) {
- checkIsOpen();
- var function = findControlTransferFunction(setup);
-
- try (var session = MemorySession.openConfined()) {
- var buffer = session.allocate(length);
- var setupPacket = createSetupPacket(session, USBDirection.IN, setup, buffer);
- var lengthHolder = session.allocate(JAVA_INT);
-
- if (WinUSB.WinUsb_ControlTransfer(function.firstInterfaceHandle(), setupPacket, buffer, (int) buffer.byteSize(),
- lengthHolder, NULL) == 0)
- throw new WindowsUSBException("Control transfer IN failed", Kernel32.GetLastError());
-
- int rxLength = lengthHolder.get(JAVA_INT, 0);
- return buffer.asSlice(0, rxLength).toArray(JAVA_BYTE);
- }
- }
-
- @Override
- public void controlTransferOut(USBControlTransfer setup, byte[] data) {
- checkIsOpen();
- var function = findControlTransferFunction(setup);
-
- try (var session = MemorySession.openConfined()) {
-
- // copy data to native memory
- int dataLength = data != null ? data.length : 0;
- MemorySegment buffer = session.allocate(dataLength);
- if (dataLength != 0)
- buffer.copyFrom(MemorySegment.ofArray(data));
-
- // create setup packet
- var setupPacket = createSetupPacket(session, USBDirection.OUT, setup, buffer);
- var lengthHolder = session.allocate(JAVA_INT);
-
- if (WinUSB.WinUsb_ControlTransfer(function.firstInterfaceHandle(), setupPacket, buffer, (int) buffer.byteSize(),
- lengthHolder, NULL) == 0)
- throw new WindowsUSBException("Control transfer OUT failed", Kernel32.GetLastError());
- }
- }
-
- @Override
- public void transferOut(int endpointNumber, byte[] data) {
- checkIsOpen();
-
- var endpoint = getEndpoint(endpointNumber, USBDirection.OUT, USBTransferType.BULK, USBTransferType.INTERRUPT);
- var function = findFunction(endpoint.interfaceNumber());
- assert function != null;
-
- try (var session = MemorySession.openConfined()) {
- var buffer = session.allocate(data.length);
- buffer.copyFrom(MemorySegment.ofArray(data));
- var lengthHolder = session.allocate(JAVA_INT);
-
- if (WinUSB.WinUsb_WritePipe(function.firstInterfaceHandle(), endpoint.endpointAddress(), buffer, (int) buffer.byteSize(),
- lengthHolder, NULL) == 0)
- throw new WindowsUSBException("Bulk/interrupt transfer OUT failed", Kernel32.GetLastError());
- }
- }
-
- @Override
- public byte[] transferIn(int endpointNumber, int maxLength) {
- var endpoint = getEndpoint(endpointNumber, USBDirection.IN, USBTransferType.BULK, USBTransferType.INTERRUPT);
- var function = findFunction(endpoint.interfaceNumber());
- assert function != null;
-
- try (var session = MemorySession.openConfined()) {
- var buffer = session.allocate(maxLength);
- var lengthHolder = session.allocate(JAVA_INT);
-
- if (WinUSB.WinUsb_ReadPipe(function.firstInterfaceHandle(), endpoint.endpointAddress(), buffer, (int) buffer.byteSize(), lengthHolder
- , NULL) == 0)
- throw new WindowsUSBException("Bulk/interrupt transfer IN failed", Kernel32.GetLastError());
-
- int len = lengthHolder.get(JAVA_INT, 0);
- return buffer.asSlice(0, len).toArray(JAVA_BYTE);
- }
- }
-}
diff --git a/java-does-usb/src/main/java/net/codecrete/usb/windows/WindowsUSBDeviceRegistry.java b/java-does-usb/src/main/java/net/codecrete/usb/windows/WindowsUSBDeviceRegistry.java
deleted file mode 100644
index 702b83f6..00000000
--- a/java-does-usb/src/main/java/net/codecrete/usb/windows/WindowsUSBDeviceRegistry.java
+++ /dev/null
@@ -1,492 +0,0 @@
-//
-// Java Does USB
-// Copyright (c) 2022 Manuel Bleichenbacher
-// Licensed under MIT License
-// https://opensource.org/licenses/MIT
-//
-
-package net.codecrete.usb.windows;
-
-import net.codecrete.usb.USBDevice;
-import net.codecrete.usb.USBException;
-import net.codecrete.usb.common.*;
-import net.codecrete.usb.windows.gen.kernel32.GUID;
-import net.codecrete.usb.windows.gen.kernel32.Kernel32;
-import net.codecrete.usb.windows.gen.ole32.Ole32;
-import net.codecrete.usb.windows.gen.setupapi.SP_DEVICE_INTERFACE_DATA;
-import net.codecrete.usb.windows.gen.setupapi.SP_DEVINFO_DATA;
-import net.codecrete.usb.windows.gen.setupapi.SetupAPI;
-import net.codecrete.usb.windows.gen.usbioctl.USBIoctl;
-import net.codecrete.usb.windows.gen.user32.*;
-
-import java.lang.foreign.*;
-import java.lang.invoke.MethodHandles;
-import java.lang.invoke.MethodType;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.regex.Pattern;
-
-import static java.lang.foreign.MemoryAddress.NULL;
-import static java.lang.foreign.ValueLayout.*;
-
-/**
- * Windows implementation of USB device registry.
- *
- * To retrieve details of a USB device, this class accesses it indirectly
- * via the parent. To address it the parent's handle (hub handle) and
- * the device's port number is needed.
- *
- */
-public class WindowsUSBDeviceRegistry extends USBDeviceRegistry {
-
- @Override
- protected void monitorDevices() {
- try (var session = MemorySession.openConfined()) {
-
- Addressable hwnd;
-
- try {
- final var className = Win.createSegmentFromString("USB_MONITOR", session);
- final var windowName = Win.createSegmentFromString("USB device monitor", session);
- final var instance = Kernel32.GetModuleHandleW(NULL);
-
- // create upcall for handling window messages
- var handleWindowMessageMH = MethodHandles.lookup().findVirtual(WindowsUSBDeviceRegistry.class,
- "handleWindowMessage", MethodType.methodType(long.class, MemoryAddress.class, int.class,
- long.class, long.class)).bindTo(this);
- var handleWindowMessageStub = Linker.nativeLinker().upcallStub(handleWindowMessageMH,
- FunctionDescriptor.of(JAVA_LONG, ADDRESS, JAVA_INT, JAVA_LONG, JAVA_LONG), session);
-
- // register window class
- var wx = session.allocate(WNDCLASSEXW.$LAYOUT());
- WNDCLASSEXW.cbSize$set(wx, (int) wx.byteSize());
- WNDCLASSEXW.lpfnWndProc$set(wx, handleWindowMessageStub.address());
- WNDCLASSEXW.hInstance$set(wx, instance);
- WNDCLASSEXW.lpszClassName$set(wx, className.address());
- User32.RegisterClassExW(wx);
-
- // create message-only window
- hwnd = User32.CreateWindowExW(0, className, windowName, 0, 0, 0, 0, 0, User32.HWND_MESSAGE(), NULL,
- instance, NULL);
- if (hwnd == NULL)
- throw new WindowsUSBException("internal error (CreateWindowExW)", Kernel32.GetLastError());
-
- // configure notifications
- var notificationFilter = session.allocate(DEV_BROADCAST_DEVICEINTERFACE_W.$LAYOUT());
- DEV_BROADCAST_DEVICEINTERFACE_W.dbcc_size$set(notificationFilter, (int) notificationFilter.byteSize());
- DEV_BROADCAST_DEVICEINTERFACE_W.dbcc_devicetype$set(notificationFilter,
- User32.DBT_DEVTYP_DEVICEINTERFACE());
- DEV_BROADCAST_DEVICEINTERFACE_W.dbcc_classguid$slice(notificationFilter).copyFrom(USBHelper.GUID_DEVINTERFACE_USB_DEVICE);
-
- var notifyHandle = User32.RegisterDeviceNotificationW(hwnd, notificationFilter,
- User32.DEVICE_NOTIFY_WINDOW_HANDLE());
- if (notifyHandle == NULL)
- throw new WindowsUSBException("internal error (RegisterDeviceNotificationW)", Kernel32.GetLastError());
-
- // initial device enumeration
- enumeratePresentDevices();
-
- } catch (Throwable e) {
- enumerationFailed(e);
- return;
- }
-
- // process messages
- var msg = session.allocate(MSG.$LAYOUT());
- //noinspection StatementWithEmptyBody
- while (User32.GetMessageW(msg, hwnd, 0, 0) > 0)
- ; // do nothing
- }
- }
-
- private void enumeratePresentDevices() {
-
- List deviceList = new ArrayList<>();
- try (var outerSession = MemorySession.openConfined()) {
-
- // get device information set of all USB devices present
- final var devInfoSetHandle = SetupAPI.SetupDiGetClassDevsW(USBHelper.GUID_DEVINTERFACE_USB_DEVICE, NULL,
- NULL, SetupAPI.DIGCF_PRESENT() | SetupAPI.DIGCF_DEVICEINTERFACE());
- if (Win.IsInvalidHandle(devInfoSetHandle))
- throw new USBException("internal error (SetupDiGetClassDevsW)");
-
- // ensure the result is destroyed when the scope is left
- outerSession.addCloseAction(() -> SetupAPI.SetupDiDestroyDeviceInfoList(devInfoSetHandle));
-
- var devInfo = MemorySegment.allocateNative(SP_DEVINFO_DATA.$LAYOUT(), outerSession);
- SP_DEVINFO_DATA.cbSize$set(devInfo, (int) SP_DEVINFO_DATA.$LAYOUT().byteSize());
-
- // ensure all hubs are closed later
- final var hubHandles = new HashMap();
- outerSession.addCloseAction(() -> hubHandles.forEach((path, handle) -> Kernel32.CloseHandle(handle)));
-
- // iterate all devices
- for (int i = 0; true; i++) {
-
- if (SetupAPI.SetupDiEnumDeviceInfo(devInfoSetHandle, i, devInfo) == 0) {
- int err = Kernel32.GetLastError();
- // TODO: Remove check for ERROR_SUCCESS if proper GetLastError() handling is available
- if (err == Kernel32.ERROR_NO_MORE_ITEMS() || err == Kernel32.ERROR_SUCCESS())
- break;
- throw new USBException("Internal error (SetupDiEnumDeviceInfo) ");
- }
-
- var instanceID = DeviceProperty.getDeviceStringProperty(devInfoSetHandle, devInfo,
- DeviceProperty.DEVPKEY_Device_InstanceId);
- var devicePath = DeviceProperty.getDevicePath(instanceID, USBHelper.GUID_DEVINTERFACE_USB_DEVICE);
-
- try {
- deviceList.add(createDeviceFromDeviceInfo(devInfoSetHandle, devInfo, devicePath, hubHandles));
-
- } catch (Throwable e) {
- System.err.printf("Info: [JavaDoesUSB] failed to retrieve information about device %s - ignoring "
- + "device%n", devicePath);
- e.printStackTrace(System.err);
- }
- }
-
- setInitialDeviceList(deviceList);
- }
- }
-
- /**
- * Gets the functions of the children of a composite device
- *
- * @param childrenIds the children IDs
- * @return a list of functions (interface number and device path)
- */
- private List getCompositeFunctions(List childrenIds) {
-
- var functions = new ArrayList();
-
- // iterate all children
- for (var instanceId : childrenIds) {
- try (var session = MemorySession.openConfined()) {
- // create device info set
- var devInfoSetHandle = SetupAPI.SetupDiCreateDeviceInfoList(NULL, NULL);
- if (Win.IsInvalidHandle(devInfoSetHandle))
- throw new WindowsUSBException("Cannot create device info list", Kernel32.GetLastError());
- session.addCloseAction(() -> SetupAPI.SetupDiDestroyDeviceInfoList(devInfoSetHandle));
-
- // get device info for child
- var devInfo = session.allocate(SP_DEVINFO_DATA.$LAYOUT());
- SP_DEVINFO_DATA.cbSize$set(devInfo, (int) devInfo.byteSize());
- var instanceIdSegment = Win.createSegmentFromString(instanceId, session);
- if (SetupAPI.SetupDiOpenDeviceInfoW(devInfoSetHandle, instanceIdSegment, NULL, 0, devInfo) == 0)
- throw new WindowsUSBException("Internal error (SetupDiOpenDeviceInfoW)", Kernel32.GetLastError());
-
- // get hardware IDs (to extract interface number)
- var hardwareIds = DeviceProperty.getDeviceStringListProperty(devInfoSetHandle, devInfo,
- DeviceProperty.DEVPKEY_Device_HardwareIds);
- int interfaceNumber = extractInterfaceNumber(hardwareIds);
- if (interfaceNumber == -1)
- continue;
-
- var guids = DeviceProperty.findDeviceInterfaceGUIDs(devInfoSetHandle, devInfo, session);
-
- for (var guid : guids) {
- // check for Class GUID
- var guidSegment = Win.createSegmentFromString(guid, session);
- var clsid = session.allocate(GUID.$LAYOUT());
- if (Ole32.CLSIDFromString(guidSegment, clsid) != 0)
- continue;
-
- try {
- var devicePath = DeviceProperty.getDevicePath(instanceId, clsid);
- functions.add(new CompositeFunction(interfaceNumber, devicePath));
- break;
- } catch (Exception e) {
- // ignore and try next one
- }
- }
- }
-
- }
-
- return functions;
- }
-
- private USBDevice createDeviceFromDeviceInfo(MemoryAddress devInfoSetHandle, MemorySegment devInfo,
- String devicePath, HashMap hubHandles) {
- try (var session = MemorySession.openConfined()) {
-
- var usbPortNum = DeviceProperty.getDeviceIntProperty(devInfoSetHandle, devInfo,
- DeviceProperty.DEVPKEY_Device_Address);
- var parentInstanceID = DeviceProperty.getDeviceStringProperty(devInfoSetHandle, devInfo,
- DeviceProperty.DEVPKEY_Device_Parent);
- var hubPath = DeviceProperty.getDevicePath(parentInstanceID, USBHelper.GUID_DEVINTERFACE_USB_HUB);
-
- // open hub if not open yet
- var hubHandle = hubHandles.get(hubPath);
- if (hubHandle == null) {
- var hubPathSeg = Win.createSegmentFromString(hubPath, session);
- hubHandle = Kernel32.CreateFileW(hubPathSeg, Kernel32.GENERIC_WRITE(), Kernel32.FILE_SHARE_WRITE(),
- NULL, Kernel32.OPEN_EXISTING(), 0, NULL);
- if (Win.IsInvalidHandle(hubHandle))
- throw new USBException("Cannot open USB hub", Kernel32.GetLastError());
- hubHandles.put(hubPath, hubHandle);
- }
-
- // check for composite device
- var deviceService = DeviceProperty.getDeviceStringProperty(devInfoSetHandle, devInfo,
- DeviceProperty.DEVPKEY_Device_Service);
-
- List functions;
- if (isCompositeDevice(deviceService)) {
- functions = getCompositeFunctions(DeviceProperty.getDeviceStringListProperty(devInfoSetHandle, devInfo,
- DeviceProperty.DEVPKEY_Device_Children));
- } else {
- functions = new ArrayList<>();
- functions.add(new CompositeFunction(0, devicePath));
- }
-
- return createDevice(devicePath, functions, hubHandle, usbPortNum);
- }
- }
-
- /**
- * Retrieve device descriptor and create {@code USBDevice} instance
- *
- * @param functions composite functions
- * @param hubHandle the hub handle (parent)
- * @param usbPortNum the USB port number
- * @return the {@code USBDevice} instance
- */
- private USBDevice createDevice(String devicePath, List functions, MemoryAddress hubHandle,
- int usbPortNum) {
-
- try (var session = MemorySession.openConfined()) {
-
- // get device descriptor
- var connInfo = session.allocate(USBHelper.USB_NODE_CONNECTION_INFORMATION_EX$Struct);
- USBHelper.USB_NODE_CONNECTION_INFORMATION_EX_ConnectionIndex.set(connInfo, usbPortNum);
- var sizeHolder = session.allocate(JAVA_INT);
- if (Kernel32.DeviceIoControl(hubHandle, USBIoctl.IOCTL_USB_GET_NODE_CONNECTION_INFORMATION_EX(), connInfo
- , (int) connInfo.byteSize(), connInfo, (int) connInfo.byteSize(), sizeHolder, NULL) == 0)
- throw new WindowsUSBException("Internal error (cannot get device descriptor)", Kernel32.GetLastError());
-
- var deviceDesc = USBHelper.USB_NODE_CONNECTION_INFORMATION_EX_DeviceDescriptor$slice(connInfo);
-
- // extract info from device descriptor
- int manufacturerIndex = 255 & (byte) USBDescriptors.Device_iManufacturer.get(deviceDesc);
- String manufacturer = getStringDescriptor(hubHandle, usbPortNum, manufacturerIndex);
- int productIndex = 255 & (byte) USBDescriptors.Device_iProduct.get(deviceDesc);
- String product = getStringDescriptor(hubHandle, usbPortNum, productIndex);
- int serialNumberIndex = 255 & (byte) USBDescriptors.Device_iSerialNumber.get(deviceDesc);
- String serialNumber = getStringDescriptor(hubHandle, usbPortNum, serialNumberIndex);
-
- int vendorId = 0xffff & (short) USBDescriptors.Device_idVendor.get(deviceDesc);
- int productId = 0xffff & (short) USBDescriptors.Device_idProduct.get(deviceDesc);
-
- var configDesc = getDescriptor(hubHandle, usbPortNum, USBDescriptors.CONFIGURATION_DESCRIPTOR_TYPE, 0,
- (short) 0, session);
-
- var device = new WindowsUSBDevice(devicePath, functions, vendorId, productId, manufacturer, product,
- serialNumber, configDesc);
-
- int classCode = 255 & (byte) USBDescriptors.Device_bDeviceClass.get(deviceDesc);
- int subclassCode = 255 & (byte) USBDescriptors.Device_bDeviceSubClass.get(deviceDesc);
- int protocolCode = 255 & (byte) USBDescriptors.Device_bDeviceProtocol.get(deviceDesc);
- device.setClassCodes(classCode, subclassCode, protocolCode);
-
- var usbVersion = (short) USBDescriptors.Device_bcdUSB.get(deviceDesc);
- var deviceVersion = (short) USBDescriptors.Device_bcdDevice.get(deviceDesc);
- device.setVersions(usbVersion, deviceVersion);
-
- return device;
- }
- }
-
- private MemorySegment getDescriptor(Addressable hubHandle, int usbPortNumber, int descriptorType, int index,
- short languageID, MemorySession session) {
- return getDescriptor(hubHandle, usbPortNumber, descriptorType, index, languageID, 0, session);
-
- }
-
- private MemorySegment getDescriptor(Addressable hubHandle, int usbPortNumber, int descriptorType, int index,
- short languageID, int requestSize, MemorySession session) {
-
- int size = requestSize != 0 ? requestSize + (int) USBHelper.USB_DESCRIPTOR_REQUEST_Data$Offset : 256;
-
- // create descriptor requests
- var descriptorRequest = session.allocate(size);
- USBHelper.USB_DESCRIPTOR_REQUEST_ConnectionIndex.set(descriptorRequest, usbPortNumber);
- var setupPacket = descriptorRequest.asSlice(USBHelper.USB_DESCRIPTOR_REQUEST_SetupPacket$Offset,
- USBStructs.SetupPacket$Struct.byteSize());
- USBStructs.SetupPacket_bmRequest.set(setupPacket, (byte) 0x80); // device-to-host / type standard / recipient
- // device
- USBStructs.SetupPacket_bRequest.set(setupPacket, USBHelper.USB_REQUEST_GET_DESCRIPTOR);
- USBStructs.SetupPacket_wValue.set(setupPacket, (short) ((descriptorType << 8) | index));
- USBStructs.SetupPacket_wIndex.set(setupPacket, languageID);
- USBStructs.SetupPacket_wLength.set(setupPacket, (short) (size - USBHelper.USB_DESCRIPTOR_REQUEST_Data$Offset));
-
- // execute request
- var effectiveSizeHolder = session.allocate(JAVA_INT);
- if (Kernel32.DeviceIoControl(hubHandle, USBIoctl.IOCTL_USB_GET_DESCRIPTOR_FROM_NODE_CONNECTION(),
- descriptorRequest, size, descriptorRequest, size, effectiveSizeHolder, NULL) == 0)
- throw new WindowsUSBException(String.format("Cannot retrieve descriptor %d", index), Kernel32.GetLastError());
-
- // determine size of descriptor
- int expectedSize;
- if (descriptorType != USBDescriptors.CONFIGURATION_DESCRIPTOR_TYPE) {
- expectedSize = 255 & descriptorRequest.get(JAVA_BYTE, USBHelper.USB_DESCRIPTOR_REQUEST_Data$Offset);
- } else {
- var configDesc = descriptorRequest.asSlice(USBHelper.USB_DESCRIPTOR_REQUEST_Data$Offset,
- USBDescriptors.Configuration.byteSize());
- expectedSize = (short) USBDescriptors.Configuration_wTotalLength.get(configDesc);
- }
-
- // check against effective size
- var effectiveSize = effectiveSizeHolder.get(JAVA_INT, 0) - USBHelper.USB_DESCRIPTOR_REQUEST_Data$Offset;
- if (effectiveSize != expectedSize) {
- if (requestSize != 0)
- throw new USBException("Unexpected descriptor size");
-
- // repeat with correct size
- return getDescriptor(hubHandle, usbPortNumber, descriptorType, index, languageID, expectedSize, session);
- }
-
- return descriptorRequest.asSlice(USBHelper.USB_DESCRIPTOR_REQUEST_Data$Offset, effectiveSize);
- }
-
- private String getStringDescriptor(Addressable hubHandle, int usbPortNumber, int index) {
- if (index == 0)
- return null;
-
- try (var session = MemorySession.openConfined()) {
- var stringDesc = getDescriptor(hubHandle, usbPortNumber, USBDescriptors.STRING_DESCRIPTOR_TYPE, index,
- USBDescriptors.DEFAULT_LANGUAGE, session);
-
- int stringLen = 255 & (byte) USBHelper.USB_STRING_DESCRIPTOR_bLength.get(stringDesc);
- var chars =
- stringDesc.asSlice(USBHelper.USB_STRING_DESCRIPTOR_bString$Offset, stringLen - 2).toArray(JAVA_CHAR);
- return new String(chars);
- }
- }
-
- private long handleWindowMessage(MemoryAddress hWnd, int uMsg, long wParam, long lParam) {
-
- // check for message related to connecting/disconnecting devices
- if (uMsg == User32.WM_DEVICECHANGE() && (wParam == User32.DBT_DEVICEARRIVAL() || wParam == User32.DBT_DEVICEREMOVECOMPLETE())) {
- try (var session = MemorySession.openConfined()) {
- var data = MemorySegment.ofAddress(MemoryAddress.ofLong(lParam),
- DEV_BROADCAST_DEVICEINTERFACE_W.sizeof(), session);
- if (DEV_BROADCAST_HDR.dbch_devicetype$get(data) == User32.DBT_DEVTYP_DEVICEINTERFACE()) {
-
- // get device path
- var nameSlice =
- MemorySegment.ofAddress(DEV_BROADCAST_DEVICEINTERFACE_W.dbcc_name$slice(data).address(),
- 500, session);
- var devicePath = Win.createStringFromSegment(nameSlice);
- if (wParam == User32.DBT_DEVICEARRIVAL())
- onDeviceConnected(devicePath);
- else
- onDeviceDisconnected(devicePath);
- return 0;
- }
- }
- }
-
- // default message handling
- return User32.DefWindowProcW(hWnd, uMsg, wParam, lParam);
- }
-
- private void onDeviceConnected(String devicePath) {
- try (var session = MemorySession.openConfined()) {
-
- // get device information set of all USB devices present
- final var devInfoSetHandle = SetupAPI.SetupDiGetClassDevsW(USBHelper.GUID_DEVINTERFACE_USB_DEVICE, NULL,
- NULL, SetupAPI.DIGCF_PRESENT() | SetupAPI.DIGCF_DEVICEINTERFACE());
- if (Win.IsInvalidHandle(devInfoSetHandle))
- throw new WindowsUSBException("internal error (SetupDiGetClassDevsW)", Kernel32.GetLastError());
-
- // ensure the result is destroyed when the scope is left
- session.addCloseAction(() -> SetupAPI.SetupDiDestroyDeviceInfoList(devInfoSetHandle));
-
- var devIntfData = session.allocate(SP_DEVICE_INTERFACE_DATA.$LAYOUT());
- SP_DEVICE_INTERFACE_DATA.cbSize$set(devIntfData, (int) devIntfData.byteSize());
- var devicePathSegment = Win.createSegmentFromString(devicePath, session);
- if (SetupAPI.SetupDiOpenDeviceInterfaceW(devInfoSetHandle, devicePathSegment, 0, devIntfData) == 0)
- throw new WindowsUSBException("internal error (SetupDiOpenDeviceInterfaceW)", Kernel32.GetLastError());
-
- ForeignMemory.addCloseAction(devIntfData, (segment) -> SetupAPI.SetupDiDeleteDeviceInterfaceData(devInfoSetHandle, segment));
-
- var devInfo = session.allocate(SP_DEVINFO_DATA.$LAYOUT());
- SP_DEVINFO_DATA.cbSize$set(devInfo, (int) devInfo.byteSize());
- if (SetupAPI.SetupDiGetDeviceInterfaceDetailW(devInfoSetHandle, devIntfData, NULL, 0, NULL, devInfo) == 0) {
- int err = Kernel32.GetLastError();
- if (err != Kernel32.ERROR_INSUFFICIENT_BUFFER())
- throw new USBException("internal error (SetupDiGetDeviceInterfaceDetailW)", err);
- }
-
- // ensure all hubs are closed later
- final var hubHandles = new HashMap();
- session.addCloseAction(() -> hubHandles.forEach((path, handle) -> Kernel32.CloseHandle(handle)));
-
- try {
- // create device instance
- var device = createDeviceFromDeviceInfo(devInfoSetHandle, devInfo, devicePath, hubHandles);
-
- // add it to device list
- addDevice(device);
-
- } catch (Throwable e) {
- System.err.printf("Info: [JavaDoesUSB] failed to retrieve information about device %s - ignoring " +
- "device%n", devicePath);
- e.printStackTrace(System.err);
- }
- }
- }
-
- private void onDeviceDisconnected(String devicePath) {
- closeAndRemoveDevice(devicePath);
- }
-
- /**
- * Finds the index of the device in the list.
- *
- * This override uses a case-insensitive string comparison as Windows uses different casing
- * when initially enumerating devices and during later monitoring.
- *
- *
- * @param deviceList the device list
- * @param deviceId the unique device ID
- * @return index, or -1 if not found
- */
- @Override
- protected int findDeviceIndex(List deviceList, Object deviceId) {
- var id = deviceId.toString();
- for (int i = 0; i < deviceList.size(); i++) {
- var dev = (USBDeviceImpl) deviceList.get(i);
- if (id.equalsIgnoreCase(dev.getUniqueId().toString()))
- return i;
- }
- return -1;
- }
-
- private static boolean isCompositeDevice(String deviceService) {
- // usbccgp is the USB Generic Parent Driver used for composite devices
- return "usbccgp".equalsIgnoreCase(deviceService);
- }
-
- private static final Pattern MULTIPLE_INTERFACE_ID = Pattern.compile("USB\\\\VID_[0-9A-Fa-f]{4}&PID_[0-9A-Fa-f]{4}&MI_([0-9A-Fa-f]{2})");
-
- private static int extractInterfaceNumber(List hardwareIds) {
- // Also see https://docs.microsoft.com/en-us/windows-hardware/drivers/install/standard-usb-identifiers#multiple-interface-usb-devices
-
- for (var id : hardwareIds) {
- var matcher = MULTIPLE_INTERFACE_ID.matcher(id);
- if (matcher.find()) {
- var intfHexNumber = matcher.group(1);
- try {
- return Integer.parseInt(intfHexNumber, 16);
- } catch (NumberFormatException e) {
- // ignore and try next one
- }
- }
- }
-
- return -1;
- }
-}
diff --git a/java-does-usb/src/main/java/net/codecrete/usb/windows/WindowsUSBException.java b/java-does-usb/src/main/java/net/codecrete/usb/windows/WindowsUSBException.java
deleted file mode 100644
index c6463801..00000000
--- a/java-does-usb/src/main/java/net/codecrete/usb/windows/WindowsUSBException.java
+++ /dev/null
@@ -1,43 +0,0 @@
-//
-// Java Does USB
-// Copyright (c) 2022 Manuel Bleichenbacher
-// Licensed under MIT License
-// https://opensource.org/licenses/MIT
-//
-package net.codecrete.usb.windows;
-
-import net.codecrete.usb.USBException;
-import net.codecrete.usb.windows.gen.kernel32.Kernel32;
-
-import java.lang.foreign.MemorySegment;
-import java.lang.foreign.MemorySession;
-
-import static java.lang.foreign.MemoryAddress.NULL;
-import static java.lang.foreign.ValueLayout.ADDRESS;
-
-public class WindowsUSBException extends USBException {
- public WindowsUSBException(String message) {
- super(message);
- }
-
- public WindowsUSBException(String message, int errorCode) {
- super(String.format("%s - %s", message, getErrorMessage(errorCode)), errorCode);
- }
-
- public WindowsUSBException(String message, Throwable cause) {
- super(message, cause);
- }
-
- private static String getErrorMessage(int errorCode) {
- try (var session = MemorySession.openConfined()) {
- var messagePointerHolder = session.allocate(ADDRESS);
- Kernel32.FormatMessageW(Kernel32.FORMAT_MESSAGE_ALLOCATE_BUFFER()
- | Kernel32.FORMAT_MESSAGE_FROM_SYSTEM() | Kernel32.FORMAT_MESSAGE_IGNORE_INSERTS(),
- NULL, errorCode, 0, messagePointerHolder, 0, NULL);
- var messagePointer = messagePointerHolder.get(ADDRESS, 0);
- String message = Win.createStringFromSegment(MemorySegment.ofAddress(messagePointer, 4000, session));
- Kernel32.LocalFree(messagePointer);
- return message.trim();
- }
- }
-}
diff --git a/java-does-usb/src/main/java/net/codecrete/usb/windows/WindowsUsbDevice.java b/java-does-usb/src/main/java/net/codecrete/usb/windows/WindowsUsbDevice.java
new file mode 100644
index 00000000..b98839c2
--- /dev/null
+++ b/java-does-usb/src/main/java/net/codecrete/usb/windows/WindowsUsbDevice.java
@@ -0,0 +1,669 @@
+//
+// Java Does USB
+// Copyright (c) 2022 Manuel Bleichenbacher
+// Licensed under MIT License
+// https://opensource.org/licenses/MIT
+//
+
+package net.codecrete.usb.windows;
+
+import net.codecrete.usb.UsbControlTransfer;
+import net.codecrete.usb.UsbDirection;
+import net.codecrete.usb.UsbException;
+import net.codecrete.usb.UsbRecipient;
+import net.codecrete.usb.UsbTransferType;
+import net.codecrete.usb.common.Transfer;
+import net.codecrete.usb.common.UsbDeviceImpl;
+import net.codecrete.usb.usbstandard.SetupPacket;
+import org.jetbrains.annotations.NotNull;
+
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.lang.foreign.Arena;
+import java.lang.foreign.MemorySegment;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.regex.Pattern;
+
+import static java.lang.System.Logger.Level.DEBUG;
+import static java.lang.System.Logger.Level.INFO;
+import static java.lang.foreign.MemorySegment.NULL;
+import static java.lang.foreign.ValueLayout.ADDRESS;
+import static java.lang.foreign.ValueLayout.JAVA_BYTE;
+import static java.lang.foreign.ValueLayout.JAVA_INT;
+import static java.nio.charset.StandardCharsets.UTF_16LE;
+import static net.codecrete.usb.common.ForeignMemory.dereference;
+import static net.codecrete.usb.windows.CustomApis.CloseHandle;
+import static net.codecrete.usb.windows.CustomApis.WinUsb_ControlTransfer;
+import static net.codecrete.usb.windows.Win.allocateErrorState;
+import static net.codecrete.usb.windows.WindowsUsbException.throwException;
+import static net.codecrete.usb.windows.WindowsUsbException.throwLastError;
+import static windows.win32.devices.properties.Constants.DEVPKEY_Device_Children;
+import static windows.win32.devices.properties.Constants.DEVPKEY_Device_HardwareIds;
+import static windows.win32.devices.usb.Apis.WinUsb_AbortPipe;
+import static windows.win32.devices.usb.Apis.WinUsb_Free;
+import static windows.win32.devices.usb.Apis.WinUsb_GetAssociatedInterface;
+import static windows.win32.devices.usb.Apis.WinUsb_Initialize;
+import static windows.win32.devices.usb.Apis.WinUsb_ReadPipe;
+import static windows.win32.devices.usb.Apis.WinUsb_ResetPipe;
+import static windows.win32.devices.usb.Apis.WinUsb_SetCurrentAlternateSetting;
+import static windows.win32.devices.usb.Apis.WinUsb_SetPipePolicy;
+import static windows.win32.devices.usb.Apis.WinUsb_WritePipe;
+import static windows.win32.devices.usb.WINUSB_PIPE_POLICY.PIPE_TRANSFER_TIMEOUT;
+import static windows.win32.devices.usb.WINUSB_PIPE_POLICY.RAW_IO;
+import static windows.win32.foundation.GENERIC_ACCESS_RIGHTS.GENERIC_READ;
+import static windows.win32.foundation.GENERIC_ACCESS_RIGHTS.GENERIC_WRITE;
+import static windows.win32.foundation.WIN32_ERROR.ERROR_INVALID_PARAMETER;
+import static windows.win32.foundation.WIN32_ERROR.ERROR_IO_PENDING;
+import static windows.win32.storage.filesystem.Apis.CreateFileW;
+import static windows.win32.storage.filesystem.FILE_CREATION_DISPOSITION.OPEN_EXISTING;
+import static windows.win32.storage.filesystem.FILE_FLAGS_AND_ATTRIBUTES.FILE_ATTRIBUTE_NORMAL;
+import static windows.win32.storage.filesystem.FILE_FLAGS_AND_ATTRIBUTES.FILE_FLAG_OVERLAPPED;
+import static windows.win32.storage.filesystem.FILE_SHARE_MODE.FILE_SHARE_READ;
+import static windows.win32.storage.filesystem.FILE_SHARE_MODE.FILE_SHARE_WRITE;
+
+/**
+ * Windows implementation for USB device.
+ */
+@SuppressWarnings("java:S2160")
+public class WindowsUsbDevice extends UsbDeviceImpl {
+
+ private static final System.Logger LOG = System.getLogger(WindowsUsbDevice.class.getName());
+
+ private final WindowsAsyncTask asyncTask;
+ /**
+ * Indicates if the device is a composite device
+ */
+ private final boolean isComposite;
+
+ private List interfaceHandles;
+
+ // device paths by interface number (first interface of function)
+ private Map devicePaths;
+
+ /**
+ * Indicates if {@link #open()} has been called. Since separate interfaces can have separate underlying
+ * Windows device, {@link #claimInterface(int)} instead of {@link #open()} will open the Windows device.
+ * (volatile: written under the device monitor, read unlocked via {@link #isOpened()})
+ */
+ private volatile boolean showAsOpen;
+
+ WindowsUsbDevice(String devicePath, int vendorId, int productId, MemorySegment configDesc, boolean isComposite) {
+ super(devicePath, vendorId, productId);
+ asyncTask = WindowsAsyncTask.INSTANCE;
+ this.isComposite = isComposite;
+ if (isComposite)
+ devicePaths = new HashMap<>();
+ readDescription(configDesc);
+ }
+
+ private void readDescription(MemorySegment configDesc) {
+ var configuration = setConfigurationDescriptor(configDesc);
+
+ // build list of interface handles
+ interfaceHandles = configuration.interfaces().stream()
+ .map(intf -> {
+ var interfaceNumber = intf.getNumber();
+ var function = configuration.findFunction(interfaceNumber);
+ return new InterfaceHandle(interfaceNumber, function.firstInterfaceNumber());
+ }).
+ toList();
+ }
+
+ @Override
+ public boolean isOpened() {
+ return showAsOpen;
+ }
+
+ @Override
+ public synchronized void open() {
+ checkIsClosed("device is already open");
+ showAsOpen = true;
+ }
+
+ @Override
+ public synchronized void close() {
+ if (!isOpened())
+ return;
+
+ for (var intf : interfaceList) {
+ if (intf.isClaimed())
+ releaseInterface(intf.getNumber());
+ }
+
+ showAsOpen = false;
+ }
+
+ public void claimInterface(int interfaceNumber) {
+ // When a device is plugged in, a notification is sent. For composite devices, it is a notification
+ // that the composite device is ready. Each composite function will be registered separately and
+ // the related information will be available with a delay. So for composite functions, several
+ // retries might be needed until the device path is available.
+ var numRetries = 30; // 30 x 100ms
+ // Defer interruption: keep a local flag instead of re-asserting the interrupt
+ // (which would make the remaining backoff sleeps throw immediately and defeat
+ // the retry delay). Re-assert when leaving the method.
+ var wasInterrupted = false;
+ try {
+ while (true) {
+ if (claimInterfaceSynchronized(interfaceNumber))
+ return; // success
+
+ numRetries -= 1;
+ if (numRetries == 0)
+ throw new UsbException("claiming interface failed (function has no device path / interface GUID, might be missing WinUSB driver)");
+
+ // sleep and retry
+ try {
+ LOG.log(DEBUG, "Sleeping for 100ms...");
+ //noinspection BusyWait
+ Thread.sleep(100);
+ } catch (InterruptedException _) {
+ wasInterrupted = true;
+ }
+ }
+ } finally {
+ if (wasInterrupted)
+ Thread.currentThread().interrupt();
+ }
+ }
+
+ @SuppressWarnings("java:S3776")
+ private synchronized boolean claimInterfaceSynchronized(int interfaceNumber) {
+ checkIsOpen();
+
+ getInterfaceWithCheck(interfaceNumber, false);
+
+ var intfHandle = getInterfaceHandle(interfaceNumber);
+ var firstIntfHandle = intfHandle;
+ if (intfHandle.firstInterfaceNumber != interfaceNumber)
+ firstIntfHandle = getInterfaceHandle(intfHandle.firstInterfaceNumber);
+
+ var deviceOpenedHere = false;
+
+ try (var arena = Arena.ofConfined()) {
+ var errorState = allocateErrorState(arena);
+
+ // both the device and the first interface must be opened for any interface belonging to the same function
+ if (firstIntfHandle.deviceHandle == null) {
+ var devicePath = getInterfaceDevicePath(firstIntfHandle.interfaceNumber);
+ if (devicePath == null)
+ return false; // retry later
+
+ LOG.log(DEBUG, "opening device {0}", devicePath);
+
+ // open Windows device if needed
+ var pathSegment = arena.allocateFrom(devicePath, UTF_16LE);
+ var deviceHandle = CreateFileW(errorState, pathSegment, GENERIC_WRITE | GENERIC_READ,
+ FILE_SHARE_WRITE | FILE_SHARE_READ, NULL, OPEN_EXISTING,
+ FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED, NULL);
+
+ if (Win.isInvalidHandle(deviceHandle))
+ throwLastError(errorState, "claiming interface failed (opening USB device %s failed)", devicePath);
+
+ MemorySegment interfaceHandle = null;
+ try {
+ // open first interface
+ var interfaceHandleHolder = arena.allocate(ADDRESS);
+ if (WinUsb_Initialize(errorState, deviceHandle, interfaceHandleHolder) == 0) {
+ if (Win.getLastError(errorState) == ERROR_INVALID_PARAMETER)
+ throw new UsbException(
+ "claiming interface failed (required WinUSB driver is probably not installed for the device)",
+ ERROR_INVALID_PARAMETER
+ );
+ throwLastError(errorState, "claiming interface failed");
+ }
+ interfaceHandle = dereference(interfaceHandleHolder);
+
+ asyncTask.addDevice(deviceHandle);
+
+ // Assign handles only after all fallible operations have succeeded.
+ // Otherwise a later claim would see them and submit I/O on a closed handle.
+ firstIntfHandle.deviceHandle = deviceHandle;
+ firstIntfHandle.winusbHandle = interfaceHandle;
+ deviceOpenedHere = true;
+
+ } catch (Exception e) {
+ if (interfaceHandle != null)
+ WinUsb_Free(interfaceHandle);
+ CloseHandle(deviceHandle);
+ throw e;
+ }
+ }
+
+ if (intfHandle != firstIntfHandle) {
+ try {
+ // open associated interface
+ var interfaceHandleHolder = arena.allocate(ADDRESS);
+ if (WinUsb_GetAssociatedInterface(errorState, firstIntfHandle.winusbHandle,
+ (byte) (intfHandle.interfaceNumber - firstIntfHandle.interfaceNumber - 1),
+ interfaceHandleHolder) == 0)
+ throwLastError(errorState, "claiming (associated) interface failed");
+ intfHandle.winusbHandle = dereference(interfaceHandleHolder);
+
+ } catch (Exception e) {
+ if (deviceOpenedHere) {
+ // no interface has been claimed yet, so close() would never release the device
+ WinUsb_Free(firstIntfHandle.winusbHandle);
+ CloseHandle(firstIntfHandle.deviceHandle);
+ firstIntfHandle.winusbHandle = null;
+ firstIntfHandle.deviceHandle = null;
+ }
+ throw e;
+ }
+ }
+ }
+
+ firstIntfHandle.deviceOpenCount += 1;
+ setClaimed(interfaceNumber, true);
+ return true;
+ }
+
+ @Override
+ public synchronized void selectAlternateSetting(int interfaceNumber, int alternateNumber) {
+ checkIsOpen();
+
+ var intf = getInterfaceWithCheck(interfaceNumber, true);
+ var intfHandle = getInterfaceHandle(interfaceNumber);
+
+ // check alternate setting
+ var altSetting = intf.getAlternate(alternateNumber);
+
+ try (var arena = Arena.ofConfined()) {
+ var errorState = allocateErrorState(arena);
+ if (WinUsb_SetCurrentAlternateSetting(errorState, intfHandle.winusbHandle, (byte) alternateNumber) == 0)
+ throwLastError(errorState, "setting alternate interface failed");
+ }
+ intf.setAlternate(altSetting);
+ }
+
+ public synchronized void releaseInterface(int interfaceNumber) {
+ checkIsOpen();
+
+ getInterfaceWithCheck(interfaceNumber, true);
+
+ var intfHandle = getInterfaceHandle(interfaceNumber);
+ var firstIntfHandle = intfHandle;
+ if (intfHandle.firstInterfaceNumber != interfaceNumber)
+ firstIntfHandle = getInterfaceHandle(intfHandle.firstInterfaceNumber);
+
+ setClaimed(interfaceNumber, false);
+
+ if (intfHandle != firstIntfHandle) {
+ // close associated interface
+ WinUsb_Free(intfHandle.winusbHandle);
+ intfHandle.winusbHandle = null;
+ }
+
+ // close device if needed
+ firstIntfHandle.deviceOpenCount -= 1;
+ if (firstIntfHandle.deviceOpenCount == 0) {
+ WinUsb_Free(firstIntfHandle.winusbHandle);
+ firstIntfHandle.winusbHandle = null;
+
+ LOG.log(DEBUG, "closing device {0}", getCachedInterfaceDevicePath(interfaceNumber));
+
+ CloseHandle(firstIntfHandle.deviceHandle);
+ firstIntfHandle.deviceHandle = null;
+ }
+ }
+
+ @Override
+ public void controlTransferOut(@NotNull UsbControlTransfer setup, byte[] data) {
+ try (var arena = Arena.ofConfined()) {
+
+ // copy data to native memory
+ var transfer = createSyncControlTransfer();
+ var dataLength = data != null ? data.length : 0;
+ transfer.setDataSize(dataLength);
+ if (dataLength != 0) {
+ var buffer = arena.allocate(data.length);
+ buffer.copyFrom(MemorySegment.ofArray(data));
+ transfer.setData(buffer);
+ } else {
+ transfer.setData(NULL);
+ }
+
+ synchronized (transfer) {
+ submitControlTransfer(UsbDirection.OUT, setup, transfer);
+ waitForTransfer(transfer, 0, UsbDirection.OUT, 0);
+ }
+ }
+ }
+
+ @Override
+ public byte @NotNull [] controlTransferIn(@NotNull UsbControlTransfer setup, int length) {
+ try (var arena = Arena.ofConfined()) {
+ var transfer = createSyncControlTransfer();
+ transfer.setData(arena.allocate(length));
+ transfer.setDataSize(length);
+
+ synchronized (transfer) {
+ submitControlTransfer(UsbDirection.IN, setup, transfer);
+ waitForTransfer(transfer, 0, UsbDirection.IN, 0);
+ }
+
+ return transfer.data().asSlice(0, transfer.resultSize()).toArray(JAVA_BYTE);
+ }
+ }
+
+ @Override
+ public void transferOut(int endpointNumber, byte @NotNull [] data, int offset, int length, int timeout) {
+ // Auto arena: a transfer that times out may be abandoned (see UsbDeviceImpl.waitForTransfer),
+ // so the buffer must outlive a possible late completion instead of being freed deterministically.
+ var arena = Arena.ofAuto();
+ var buffer = arena.allocate(data.length);
+ buffer.copyFrom(MemorySegment.ofArray(data).asSlice(offset, length));
+ var transfer = createSyncTransfer(buffer);
+
+ synchronized (transfer) {
+ submitTransferOut(endpointNumber, transfer);
+ waitForTransfer(transfer, timeout, UsbDirection.OUT, endpointNumber);
+ }
+ }
+
+ @Override
+ public byte @NotNull [] transferIn(int endpointNumber, int timeout) {
+ var endpoint = getEndpoint(UsbDirection.IN, endpointNumber, UsbTransferType.BULK, UsbTransferType.INTERRUPT);
+
+ // Auto arena: a transfer that times out may be abandoned (see UsbDeviceImpl.waitForTransfer),
+ // so the buffer must outlive a possible late completion instead of being freed deterministically.
+ var arena = Arena.ofAuto();
+ var buffer = arena.allocate(endpoint.packetSize());
+ var transfer = createSyncTransfer(buffer);
+
+ synchronized (transfer) {
+ submitTransferIn(endpointNumber, transfer);
+ waitForTransfer(transfer, timeout, UsbDirection.IN, endpointNumber);
+ }
+
+ return buffer.asSlice(0, transfer.resultSize()).toArray(JAVA_BYTE);
+ }
+
+ private WindowsTransfer createSyncControlTransfer() {
+ var transfer = new WindowsTransfer();
+ transfer.setCompletion(UsbDeviceImpl::onSyncTransferCompleted);
+ return transfer;
+ }
+
+ private WindowsTransfer createSyncTransfer(MemorySegment data) {
+ var transfer = new WindowsTransfer();
+ transfer.setData(data);
+ transfer.setDataSize((int) data.byteSize());
+ transfer.setCompletion(UsbDeviceImpl::onSyncTransferCompleted);
+ return transfer;
+ }
+
+ @Override
+ protected Transfer createTransfer() {
+ return new WindowsTransfer();
+ }
+
+ @Override
+ protected void throwOSException(int errorCode, String message, Object... args) {
+ throwException(errorCode, message, args);
+ }
+
+ synchronized void submitControlTransfer(UsbDirection direction, UsbControlTransfer setup, WindowsTransfer transfer) {
+ checkIsOpen();
+ var intfHandle = findControlTransferInterface(setup);
+
+ try (var arena = Arena.ofConfined()) {
+ var setupPacket = new SetupPacket(arena);
+ var bmRequest =
+ (direction == UsbDirection.IN ? 0x80 : 0) | (setup.requestType().ordinal() << 5) | setup.recipient().ordinal();
+ setupPacket.setRequestType(bmRequest);
+ setupPacket.setRequest(setup.request());
+ setupPacket.setValue(setup.value());
+ setupPacket.setIndex(setup.index());
+ setupPacket.setLength(transfer.dataSize());
+
+ var errorState = allocateErrorState(arena);
+ asyncTask.prepareForSubmission(transfer);
+
+ // submit transfer
+ if (WinUsb_ControlTransfer(errorState, intfHandle.winusbHandle, setupPacket.segment(), transfer.data(),
+ transfer.dataSize(), NULL, transfer.overlapped()) == 0) {
+ var err = Win.getLastError(errorState);
+ if (err != ERROR_IO_PENDING) {
+ asyncTask.submissionFailed(transfer);
+ throwException(err, "submitting control transfer failed");
+ }
+ }
+ }
+ }
+
+ synchronized void submitTransferOut(int endpointNumber, WindowsTransfer transfer) {
+ var endpoint = getEndpoint(UsbDirection.OUT, endpointNumber, UsbTransferType.BULK, UsbTransferType.INTERRUPT);
+ var intfHandle = getInterfaceHandle(endpoint.interfaceNumber());
+
+ try (var arena = Arena.ofConfined()) {
+ var errorState = allocateErrorState(arena);
+ asyncTask.prepareForSubmission(transfer);
+
+ // submit transfer
+ if (WinUsb_WritePipe(errorState, intfHandle.winusbHandle, endpoint.endpointAddress(), transfer.data(),
+ transfer.dataSize(), NULL, transfer.overlapped()) == 0) {
+ var err = Win.getLastError(errorState);
+ if (err != ERROR_IO_PENDING) {
+ asyncTask.submissionFailed(transfer);
+ throwException(err, "submitting transfer OUT failed");
+ }
+ }
+ }
+ }
+
+ synchronized void submitTransferIn(int endpointNumber, WindowsTransfer transfer) {
+ var endpoint = getEndpoint(UsbDirection.IN, endpointNumber, UsbTransferType.BULK, UsbTransferType.INTERRUPT);
+ var intfHandle = getInterfaceHandle(endpoint.interfaceNumber());
+
+ try (var arena = Arena.ofConfined()) {
+ var errorState = allocateErrorState(arena);
+ asyncTask.prepareForSubmission(transfer);
+
+ // submit transfer
+ if (WinUsb_ReadPipe(errorState, intfHandle.winusbHandle, endpoint.endpointAddress(), transfer.data(),
+ transfer.dataSize(), NULL, transfer.overlapped()) == 0) {
+ var err = Win.getLastError(errorState);
+ if (err != ERROR_IO_PENDING) {
+ asyncTask.submissionFailed(transfer);
+ throwException(err, "submitting transfer IN failed");
+ }
+ }
+ }
+ }
+
+ synchronized void configureForAsyncIo(UsbDirection direction, int endpointNumber) {
+ var endpoint = getEndpoint(direction, endpointNumber, UsbTransferType.BULK, UsbTransferType.INTERRUPT);
+ var intfHandle = getInterfaceHandle(endpoint.interfaceNumber());
+
+ try (var arena = Arena.ofConfined()) {
+ var errorState = allocateErrorState(arena);
+
+ var timeoutHolder = arena.allocate(JAVA_INT);
+ if (WinUsb_SetPipePolicy(errorState, intfHandle.winusbHandle, endpoint.endpointAddress(),
+ PIPE_TRANSFER_TIMEOUT, (int) timeoutHolder.byteSize(), timeoutHolder) == 0)
+ throwLastError(errorState, "setting timeout failed");
+
+ var rawIoHolder = arena.allocate(JAVA_BYTE);
+ rawIoHolder.setAtIndex(JAVA_BYTE, 0, (byte) 1);
+ if (WinUsb_SetPipePolicy(errorState, intfHandle.winusbHandle, endpoint.endpointAddress(), RAW_IO,
+ (int) rawIoHolder.byteSize(), rawIoHolder) == 0)
+ throwLastError(errorState, "setting raw IO failed");
+ }
+ }
+
+ @Override
+ public synchronized void clearHalt(UsbDirection direction, int endpointNumber) {
+ var endpoint = getEndpoint(direction, endpointNumber, UsbTransferType.BULK, UsbTransferType.INTERRUPT);
+ var intfHandle = getInterfaceHandle(endpoint.interfaceNumber());
+
+ try (var arena = Arena.ofConfined()) {
+ var errorState = allocateErrorState(arena);
+ if (WinUsb_ResetPipe(errorState, intfHandle.winusbHandle, endpoint.endpointAddress()) == 0)
+ throwLastError(errorState, "clearing halt failed");
+ }
+ }
+
+ @Override
+ public synchronized void abortTransfers(UsbDirection direction, int endpointNumber) {
+ var endpoint = getEndpoint(direction, endpointNumber, UsbTransferType.BULK, UsbTransferType.INTERRUPT);
+ var intfHandle = getInterfaceHandle(endpoint.interfaceNumber());
+
+ try (var arena = Arena.ofConfined()) {
+ var errorState = allocateErrorState(arena);
+ if (WinUsb_AbortPipe(errorState, intfHandle.winusbHandle, endpoint.endpointAddress()) == 0)
+ throwLastError(errorState, "aborting transfers on endpoint failed");
+ }
+ }
+
+ @Override
+ public synchronized @NotNull InputStream openInputStream(int endpointNumber, int bufferSize) {
+ // check that endpoint number is valid
+ getEndpoint(UsbDirection.IN, endpointNumber, UsbTransferType.BULK, null);
+
+ return new WindowsEndpointInputStream(this, endpointNumber, bufferSize);
+ }
+
+ @Override
+ public synchronized @NotNull OutputStream openOutputStream(int endpointNumber, int bufferSize) {
+ // check that endpoint number is valid
+ getEndpoint(UsbDirection.OUT, endpointNumber, UsbTransferType.BULK, null);
+
+ return new WindowsEndpointOutputStream(this, endpointNumber, bufferSize);
+ }
+
+ private InterfaceHandle getInterfaceHandle(int interfaceNumber) {
+ for (var intfHandle : interfaceHandles) {
+ if (intfHandle.interfaceNumber == interfaceNumber)
+ return intfHandle;
+ }
+
+ throwException("invalid interface number %s", interfaceNumber);
+ throw new AssertionError("not reached");
+ }
+
+ private InterfaceHandle findControlTransferInterface(UsbControlTransfer setup) {
+
+ var interfaceNumber = -1;
+ int endpointNumber;
+
+ if (setup.recipient() == UsbRecipient.INTERFACE) {
+
+ interfaceNumber = setup.index() & 0xff;
+
+ } else if (setup.recipient() == UsbRecipient.ENDPOINT) {
+
+ endpointNumber = setup.index() & 0x7f;
+ var direction = (setup.index() & 0x80) != 0 ? UsbDirection.IN : UsbDirection.OUT;
+ if (endpointNumber != 0) {
+ interfaceNumber = getInterfaceNumber(direction, endpointNumber);
+ if (interfaceNumber == -1)
+ throwException("invalid endpoint number %d or interface not claimed", endpointNumber);
+ }
+ }
+
+ if (interfaceNumber >= 0) {
+ var intfHandle = getInterfaceHandle(interfaceNumber);
+ if (intfHandle.winusbHandle == null)
+ throwException("interface number %d has not been claimed", interfaceNumber);
+ return intfHandle;
+ }
+
+ // for control transfer to device, use any claimed interface
+ for (var intfHandle : interfaceHandles) {
+ if (intfHandle.winusbHandle != null)
+ return intfHandle;
+ }
+
+ throwException("control transfer cannot be executed as no interface has been claimed");
+ throw new AssertionError("not reached");
+ }
+
+ private String getInterfaceDevicePath(int interfaceNumber) {
+ var devicePath = getCachedInterfaceDevicePath(interfaceNumber);
+ if (devicePath != null)
+ return devicePath;
+
+ var parentDevicePath = (String) getUniqueId();
+
+ try (var deviceInfoSet = DeviceInfoSet.ofPath(parentDevicePath)) {
+ var childrenInstanceIDs = deviceInfoSet.getStringListProperty(DEVPKEY_Device_Children());
+ if (childrenInstanceIDs == null) {
+ LOG.log(DEBUG, "missing children instance IDs for device {0}", parentDevicePath);
+ return null;
+
+ } else {
+ LOG.log(DEBUG, "children instance IDs: {0}", childrenInstanceIDs);
+
+ for (var instanceId : childrenInstanceIDs) {
+ devicePath = getChildDevicePath(instanceId, interfaceNumber);
+ if (devicePath != null)
+ return devicePath;
+ }
+ }
+ }
+
+ return null; // retry later
+ }
+
+ private String getCachedInterfaceDevicePath(int interfaceNumber) {
+ if (!isComposite)
+ return (String) getUniqueId();
+ return devicePaths.get(interfaceNumber);
+ }
+
+ private String getChildDevicePath(String instanceId, int interfaceNumber) {
+ try (var deviceInfoSet = DeviceInfoSet.ofInstance(instanceId)) {
+
+ // get hardware IDs (to extract interface number)
+ var hardwareIds = deviceInfoSet.getStringListProperty(DEVPKEY_Device_HardwareIds());
+ if (hardwareIds == null) {
+ LOG.log(DEBUG, "child device {0} has no hardware IDs", instanceId);
+ return null;
+ }
+
+ var extractedNumber = extractInterfaceNumber(hardwareIds);
+ if (extractedNumber == -1) {
+ LOG.log(DEBUG, "child device {0} has no interface number", instanceId);
+ return null;
+ }
+
+ if (extractedNumber != interfaceNumber)
+ return null;
+
+ var devicePath = deviceInfoSet.getDevicePathByGUID(instanceId);
+ if (devicePath == null) {
+ LOG.log(INFO, "Child device {0} has no device path / interface GUID", instanceId);
+ throw new UsbException("claiming interface failed (composite function has no device path / interface GUID; the required WinUSB driver is probably not installed)");
+ }
+
+ if (devicePaths == null)
+ devicePaths = new HashMap<>();
+ devicePaths.put(interfaceNumber, devicePath);
+ return devicePath;
+ }
+ }
+
+ private static final Pattern MULTIPLE_INTERFACE_ID = Pattern.compile(
+ "USB\\\\VID_[0-9A-Fa-f]{4}&PID_[0-9A-Fa-f]{4}&MI_([0-9A-Fa-f]{2})");
+
+ private static int extractInterfaceNumber(List hardwareIds) {
+ // Also see https://docs.microsoft.com/en-us/windows-hardware/drivers/install/standard-usb-identifiers#multiple-interface-usb-devices
+
+ for (var id : hardwareIds) {
+ var matcher = MULTIPLE_INTERFACE_ID.matcher(id);
+ if (matcher.find()) {
+ var intfHexNumber = matcher.group(1);
+ try {
+ return Integer.parseInt(intfHexNumber, 16);
+ } catch (NumberFormatException _) {
+ // ignore and try next one
+ }
+ }
+ }
+
+ return -1;
+ }
+}
diff --git a/java-does-usb/src/main/java/net/codecrete/usb/windows/WindowsUsbDeviceRegistry.java b/java-does-usb/src/main/java/net/codecrete/usb/windows/WindowsUsbDeviceRegistry.java
new file mode 100644
index 00000000..d9f86393
--- /dev/null
+++ b/java-does-usb/src/main/java/net/codecrete/usb/windows/WindowsUsbDeviceRegistry.java
@@ -0,0 +1,396 @@
+//
+// Java Does USB
+// Copyright (c) 2022 Manuel Bleichenbacher
+// Licensed under MIT License
+// https://opensource.org/licenses/MIT
+//
+
+package net.codecrete.usb.windows;
+
+import net.codecrete.usb.UsbDevice;
+import net.codecrete.usb.UsbException;
+import net.codecrete.usb.common.ScopeCleanup;
+import net.codecrete.usb.common.UsbDeviceImpl;
+import net.codecrete.usb.common.UsbDeviceRegistry;
+import net.codecrete.usb.usbstandard.ConfigurationDescriptor;
+import net.codecrete.usb.usbstandard.DeviceDescriptor;
+import net.codecrete.usb.usbstandard.SetupPacket;
+import net.codecrete.usb.usbstandard.StringDescriptor;
+import windows.win32.devices.usb.USB_DESCRIPTOR_REQUEST;
+import windows.win32.devices.usb.USB_NODE_CONNECTION_INFORMATION_EX;
+import windows.win32.ui.windowsandmessaging.DEV_BROADCAST_DEVICEINTERFACE_W;
+import windows.win32.ui.windowsandmessaging.DEV_BROADCAST_HDR;
+import windows.win32.ui.windowsandmessaging.MSG;
+import windows.win32.ui.windowsandmessaging.WNDCLASSEXW;
+import windows.win32.ui.windowsandmessaging.WNDPROC;
+
+import java.lang.foreign.Arena;
+import java.lang.foreign.MemorySegment;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import static java.lang.System.Logger.Level.INFO;
+import static java.lang.foreign.MemorySegment.NULL;
+import static java.lang.foreign.ValueLayout.JAVA_BYTE;
+import static java.lang.foreign.ValueLayout.JAVA_INT;
+import static java.lang.foreign.ValueLayout.JAVA_SHORT;
+import static java.lang.foreign.ValueLayout.PathElement;
+import static java.nio.charset.StandardCharsets.UTF_16LE;
+import static net.codecrete.usb.usbstandard.Constants.CONFIGURATION_DESCRIPTOR_TYPE;
+import static net.codecrete.usb.usbstandard.Constants.DEFAULT_LANGUAGE;
+import static net.codecrete.usb.usbstandard.Constants.STRING_DESCRIPTOR_TYPE;
+import static net.codecrete.usb.windows.CustomApis.CloseHandle;
+import static net.codecrete.usb.windows.Win.allocateErrorState;
+import static net.codecrete.usb.windows.WindowsUsbException.throwException;
+import static net.codecrete.usb.windows.WindowsUsbException.throwLastError;
+import static windows.win32.devices.properties.Constants.DEVPKEY_Device_Address;
+import static windows.win32.devices.properties.Constants.DEVPKEY_Device_InstanceId;
+import static windows.win32.devices.properties.Constants.DEVPKEY_Device_Parent;
+import static windows.win32.devices.usb.Constants.GUID_DEVINTERFACE_USB_DEVICE;
+import static windows.win32.devices.usb.Constants.GUID_DEVINTERFACE_USB_HUB;
+import static windows.win32.devices.usb.Constants.IOCTL_USB_GET_DESCRIPTOR_FROM_NODE_CONNECTION;
+import static windows.win32.devices.usb.Constants.IOCTL_USB_GET_NODE_CONNECTION_INFORMATION_EX;
+import static windows.win32.devices.usb.Constants.USB_REQUEST_GET_DESCRIPTOR;
+import static windows.win32.foundation.GENERIC_ACCESS_RIGHTS.GENERIC_WRITE;
+import static windows.win32.storage.filesystem.Apis.CreateFileW;
+import static windows.win32.storage.filesystem.FILE_CREATION_DISPOSITION.OPEN_EXISTING;
+import static windows.win32.storage.filesystem.FILE_SHARE_MODE.FILE_SHARE_WRITE;
+import static windows.win32.system.io.Apis.DeviceIoControl;
+import static windows.win32.system.libraryloader.Apis.GetModuleHandleW;
+import static windows.win32.ui.windowsandmessaging.Apis.CreateWindowExW;
+import static windows.win32.ui.windowsandmessaging.Apis.DefWindowProcW;
+import static windows.win32.ui.windowsandmessaging.Apis.GetMessageW;
+import static windows.win32.ui.windowsandmessaging.Apis.RegisterClassExW;
+import static windows.win32.ui.windowsandmessaging.Apis.RegisterDeviceNotificationW;
+import static windows.win32.ui.windowsandmessaging.Constants.DBT_DEVICEARRIVAL;
+import static windows.win32.ui.windowsandmessaging.Constants.DBT_DEVICEREMOVECOMPLETE;
+import static windows.win32.ui.windowsandmessaging.Constants.HWND_MESSAGE;
+import static windows.win32.ui.windowsandmessaging.Constants.WM_DEVICECHANGE;
+import static windows.win32.ui.windowsandmessaging.DEV_BROADCAST_HDR_DEVICE_TYPE.DBT_DEVTYP_DEVICEINTERFACE;
+import static windows.win32.ui.windowsandmessaging.REGISTER_NOTIFICATION_FLAGS.DEVICE_NOTIFY_WINDOW_HANDLE;
+
+/**
+ * Windows implementation of USB device registry.
+ *
+ * To retrieve details of a USB device, this class accesses it indirectly
+ * via the parent. To address it the parent's handle (hub handle) and
+ * the device's port number is needed.
+ *
+ */
+public class WindowsUsbDeviceRegistry extends UsbDeviceRegistry {
+
+ private static final System.Logger LOG = System.getLogger(WindowsUsbDeviceRegistry.class.getName());
+
+ private static final long REQUEST_DATA_OFFSET
+ = USB_DESCRIPTOR_REQUEST.layout().byteOffset(PathElement.groupElement("Data"));
+
+ @Override
+ protected void monitorDevices() {
+ try (var arena = Arena.ofConfined()) {
+
+ MemorySegment hwnd;
+ var errorState = allocateErrorState(arena);
+
+ try {
+ final var className = arena.allocateFrom("USB_MONITOR", UTF_16LE);
+ final var windowName = arena.allocateFrom("USB device monitor", UTF_16LE);
+ final var instance = GetModuleHandleW(errorState, NULL);
+
+ // register window class
+ var wx = WNDCLASSEXW.allocate(arena);
+ WNDCLASSEXW.lpfnWndProc(wx, WNDPROC.allocate(arena, this::handleWindowMessage));
+ WNDCLASSEXW.hInstance(wx, instance);
+ WNDCLASSEXW.lpszClassName(wx, className);
+ var atom = RegisterClassExW(errorState, wx);
+ if (atom == 0)
+ throwLastError(errorState, "internal error (RegisterClassExW)");
+
+ // create message-only window
+ hwnd = CreateWindowExW(errorState, 0, className, windowName, 0, 0, 0, 0, 0, HWND_MESSAGE, NULL,
+ instance, NULL);
+ if (hwnd.address() == 0)
+ throwLastError(errorState, "internal error (CreateWindowExW)");
+
+ // configure notifications
+ var notificationFilter = DEV_BROADCAST_DEVICEINTERFACE_W.allocate(arena, 260);
+ DEV_BROADCAST_DEVICEINTERFACE_W.dbcc_size(notificationFilter, (int) notificationFilter.byteSize());
+ DEV_BROADCAST_DEVICEINTERFACE_W.dbcc_devicetype(notificationFilter, DBT_DEVTYP_DEVICEINTERFACE);
+ DEV_BROADCAST_DEVICEINTERFACE_W.dbcc_classguid(notificationFilter).copyFrom(GUID_DEVINTERFACE_USB_DEVICE());
+
+ var notifyHandle = RegisterDeviceNotificationW(errorState, hwnd, notificationFilter,
+ DEVICE_NOTIFY_WINDOW_HANDLE);
+ if (notifyHandle.address() == 0)
+ throwLastError(errorState, "internal error (RegisterDeviceNotificationW)");
+
+ // initial device enumeration
+ enumeratePresentDevices();
+
+ } catch (Exception e) {
+ enumerationFailed(e);
+ return;
+ }
+
+ // process messages
+ var msg = MSG.allocate(arena);
+ int err;
+ //noinspection StatementWithEmptyBody
+ while ((err = GetMessageW(errorState, msg, hwnd, 0, 0)) > 0)
+ ; // do nothing
+
+ if (err == -1)
+ throwLastError(errorState, "internal error (GetMessageW)");
+ }
+ }
+
+ @SuppressWarnings("java:S106")
+ private void enumeratePresentDevices() {
+
+ List deviceList = new ArrayList<>();
+ try (var cleanup = new ScopeCleanup();
+ var deviceInfoSet = DeviceInfoSet.ofPresentDevices(GUID_DEVINTERFACE_USB_DEVICE(), null)) {
+
+ // ensure all hubs are closed later
+ final var hubHandles = new HashMap();
+ cleanup.add(() -> hubHandles.forEach((_, handle) -> CloseHandle(handle)));
+
+ // iterate all devices
+ while (deviceInfoSet.next()) {
+
+ var instanceId = deviceInfoSet.getStringProperty(DEVPKEY_Device_InstanceId());
+ var devicePath = DeviceInfoSet.getDevicePath(instanceId, GUID_DEVINTERFACE_USB_DEVICE());
+
+ try {
+ deviceList.add(createDeviceFromDeviceInfo(deviceInfoSet, devicePath, hubHandles));
+
+ } catch (Exception e) {
+ LOG.log(INFO, String.format("failed to retrieve information about device %s - ignoring device", devicePath), e);
+ }
+ }
+
+ setInitialDeviceList(deviceList);
+ }
+ }
+
+ private UsbDevice createDeviceFromDeviceInfo(DeviceInfoSet deviceInfoSet, String devicePath,
+ Map hubHandles) {
+ try (var arena = Arena.ofConfined()) {
+
+ var usbPortNum = deviceInfoSet.getIntProperty(DEVPKEY_Device_Address());
+ var parentInstanceId = deviceInfoSet.getStringProperty(DEVPKEY_Device_Parent());
+ var hubPath = DeviceInfoSet.getDevicePath(parentInstanceId, GUID_DEVINTERFACE_USB_HUB());
+
+ // open hub if not open yet
+ var hubHandle = hubHandles.get(hubPath);
+ if (hubHandle == null) {
+ var hubPathSeg = arena.allocateFrom(hubPath, UTF_16LE);
+ var errorState = allocateErrorState(arena);
+ hubHandle = CreateFileW(errorState, hubPathSeg, GENERIC_WRITE, FILE_SHARE_WRITE,
+ NULL, OPEN_EXISTING, 0, NULL);
+ if (Win.isInvalidHandle(hubHandle))
+ throwLastError(errorState, "internal error (opening hub device)");
+ hubHandles.put(hubPath, hubHandle);
+ }
+
+ return createDevice(devicePath, deviceInfoSet.isCompositeDevice(), hubHandle, usbPortNum);
+ }
+ }
+
+ /**
+ * Retrieve device descriptor and create {@code UsbDevice} instance
+ *
+ * @param devicePath the device path
+ * @param hubHandle the hub handle (parent)
+ * @param usbPortNum the USB port number
+ * @return the {@code UsbDevice} instance
+ */
+ private UsbDevice createDevice(String devicePath, boolean isComposite, MemorySegment hubHandle, int usbPortNum) {
+
+ try (var arena = Arena.ofConfined()) {
+
+ // get device descriptor
+ var connInfo = USB_NODE_CONNECTION_INFORMATION_EX.allocate(arena, 0);
+ USB_NODE_CONNECTION_INFORMATION_EX.ConnectionIndex(connInfo, usbPortNum);
+ var sizeHolder = arena.allocate(JAVA_INT);
+ var errorState = allocateErrorState(arena);
+ if (DeviceIoControl(errorState, hubHandle, IOCTL_USB_GET_NODE_CONNECTION_INFORMATION_EX,
+ connInfo, (int) connInfo.byteSize(), connInfo, (int) connInfo.byteSize(), sizeHolder, NULL) == 0)
+ throwLastError(errorState, "internal error (getting device descriptor failed)");
+
+ var descriptorSegment = USB_NODE_CONNECTION_INFORMATION_EX.DeviceDescriptor(connInfo);
+ var deviceDescriptor = new DeviceDescriptor(descriptorSegment);
+
+ var vendorId = deviceDescriptor.vendorID();
+ var productId = deviceDescriptor.productID();
+
+ var configDesc = getDescriptor(hubHandle, usbPortNum, CONFIGURATION_DESCRIPTOR_TYPE, 0, (short) 0, arena);
+
+ // create new device
+ var device = new WindowsUsbDevice(devicePath, vendorId, productId, configDesc, isComposite);
+ device.setFromDeviceDescriptor(descriptorSegment);
+
+ var languages = getLanguages(hubHandle, usbPortNum, arena);
+ device.setProductString(descriptorSegment, index -> getStringDescriptor(hubHandle, usbPortNum, index, languages));
+ return device;
+ }
+ }
+
+ private MemorySegment getDescriptor(MemorySegment hubHandle, int usbPortNumber, int descriptorType, int index,
+ short languageID, Arena arena) {
+ return getDescriptor(hubHandle, usbPortNumber, descriptorType, index, languageID, 0, arena);
+
+ }
+
+ private MemorySegment getDescriptor(MemorySegment hubHandle, int usbPortNumber, int descriptorType, int index,
+ short languageID, int requestSize, Arena arena) {
+ var size = requestSize != 0 ? requestSize + (int) REQUEST_DATA_OFFSET : 256;
+
+ // create descriptor requests
+ var descriptorRequest = arena.allocate(size);
+ USB_DESCRIPTOR_REQUEST.ConnectionIndex(descriptorRequest, usbPortNumber);
+ var setupPacket = new SetupPacket(descriptorRequest.asSlice(
+ USB_DESCRIPTOR_REQUEST.SetupPacket_bmRequest$offset(), SetupPacket.LAYOUT.byteSize()));
+ setupPacket.setRequestType(0x80); // device-to-host / type standard / recipient device
+ setupPacket.setRequest(USB_REQUEST_GET_DESCRIPTOR);
+ setupPacket.setValue((descriptorType << 8) | index);
+ setupPacket.setIndex(languageID);
+ setupPacket.setLength(size - (int) REQUEST_DATA_OFFSET);
+
+ // execute request
+ var effectiveSizeHolder = arena.allocate(JAVA_INT);
+ var errorState = allocateErrorState(arena);
+ if (DeviceIoControl(errorState, hubHandle, IOCTL_USB_GET_DESCRIPTOR_FROM_NODE_CONNECTION,
+ descriptorRequest, size, descriptorRequest, size, effectiveSizeHolder, NULL) == 0)
+ throwLastError(errorState, "internal error (retrieving descriptor %d failed)", index);
+
+ // determine size of descriptor
+ int expectedSize;
+ if (descriptorType != CONFIGURATION_DESCRIPTOR_TYPE) {
+ expectedSize = 255 & descriptorRequest.get(JAVA_BYTE, REQUEST_DATA_OFFSET);
+ } else {
+ var configDesc =
+ new ConfigurationDescriptor(descriptorRequest.asSlice(REQUEST_DATA_OFFSET, ConfigurationDescriptor.LAYOUT.byteSize()));
+ expectedSize = configDesc.totalLength();
+ }
+
+ // check against effective size
+ var effectiveSize = effectiveSizeHolder.get(JAVA_INT, 0) - REQUEST_DATA_OFFSET;
+ if (effectiveSize != expectedSize) {
+ if (requestSize != 0)
+ throwException("internal error (unexpected descriptor size)");
+
+ // repeat with correct size
+ return getDescriptor(hubHandle, usbPortNumber, descriptorType, index, languageID, expectedSize, arena);
+ }
+
+ return descriptorRequest.asSlice(REQUEST_DATA_OFFSET, effectiveSize);
+ }
+
+ @SuppressWarnings("java:S106")
+ private String getStringDescriptor(MemorySegment hubHandle, int usbPortNumber, int index, short[] languages) {
+ if (index == 0)
+ return null;
+
+ try (var arena = Arena.ofConfined()) {
+ for (var language : languages) {
+ try {
+ var stringDesc = new StringDescriptor(getDescriptor(hubHandle, usbPortNumber, STRING_DESCRIPTOR_TYPE,
+ index, language, arena));
+ return stringDesc.string();
+
+ } catch (UsbException _) {
+ // ignore and try next language
+ }
+ }
+ }
+
+ // Even though this function is only called for string descriptors referenced in the
+ // configuration descriptor, some device might not provide them; so ignore it.
+ return null;
+ }
+
+ private short[] getLanguages(MemorySegment hubHandle, int usbPortNumber, Arena arena) {
+ try {
+ var languages = getDescriptor(hubHandle, usbPortNumber, STRING_DESCRIPTOR_TYPE, 0, (short) 0, arena);
+ var n = (languages.byteSize() - 2) / 2;
+ if (n == 0)
+ return new short[]{DEFAULT_LANGUAGE};
+ return languages.asSlice(2, n * 2).toArray(JAVA_SHORT);
+ } catch (UsbException _) {
+ return new short[]{DEFAULT_LANGUAGE};
+ }
+ }
+
+ @SuppressWarnings("java:S1144")
+ private long handleWindowMessage(MemorySegment hWnd, int uMsg, long wParam, long lParam) {
+
+ // check for message related to connecting/disconnecting devices
+ if (uMsg == WM_DEVICECHANGE && (wParam == DBT_DEVICEARRIVAL || wParam == DBT_DEVICEREMOVECOMPLETE)) {
+ var data = MemorySegment.ofAddress(lParam).reinterpret(DEV_BROADCAST_DEVICEINTERFACE_W.sizeof());
+ if (DEV_BROADCAST_HDR.dbch_devicetype(data) == DBT_DEVTYP_DEVICEINTERFACE) {
+
+ // get device path
+ var nameSlice =
+ MemorySegment.ofAddress(DEV_BROADCAST_DEVICEINTERFACE_W.dbcc_name(data).address()).reinterpret(500);
+ var devicePath = nameSlice.getString(0, UTF_16LE);
+ if (wParam == DBT_DEVICEARRIVAL)
+ onDeviceConnected(devicePath);
+ else
+ onDeviceDisconnected(devicePath);
+ return 0;
+ }
+ }
+
+ // default message handling
+ return DefWindowProcW(hWnd, uMsg, wParam, lParam);
+ }
+
+ @SuppressWarnings("java:S106")
+ private void onDeviceConnected(String devicePath) {
+ try (var cleanup = new ScopeCleanup();
+ var deviceInfoSet = DeviceInfoSet.ofPath(devicePath)) {
+
+ // ensure all hubs are closed later
+ final var hubHandles = new HashMap();
+ cleanup.add(() -> hubHandles.forEach((_, handle) -> CloseHandle(handle)));
+
+ try {
+ // create device instance
+ var device = createDeviceFromDeviceInfo(deviceInfoSet, devicePath, hubHandles);
+
+ // add it to device list
+ addDevice(device);
+
+ } catch (Exception e) {
+ LOG.log(INFO, String.format("failed to retrieve information about device %s - ignoring device", devicePath), e);
+ }
+ }
+ }
+
+ private void onDeviceDisconnected(String devicePath) {
+ closeAndRemoveDevice(devicePath);
+ }
+
+ /**
+ * Finds the index of the device in the list.
+ *
+ * This override uses a case-insensitive string comparison as Windows uses different casing
+ * when initially enumerating devices and during later monitoring.
+ *
+ *
+ * @param deviceList the device list
+ * @param deviceId the unique device ID
+ * @return index, or -1 if not found
+ */
+ @Override
+ protected int findDeviceIndex(List deviceList, Object deviceId) {
+ var id = deviceId.toString();
+ for (var i = 0; i < deviceList.size(); i++) {
+ var dev = (UsbDeviceImpl) deviceList.get(i);
+ if (id.equalsIgnoreCase(dev.getUniqueId().toString()))
+ return i;
+ }
+ return -1;
+ }
+}
diff --git a/java-does-usb/src/main/java/net/codecrete/usb/windows/WindowsUsbException.java b/java-does-usb/src/main/java/net/codecrete/usb/windows/WindowsUsbException.java
new file mode 100644
index 00000000..94f70bf5
--- /dev/null
+++ b/java-does-usb/src/main/java/net/codecrete/usb/windows/WindowsUsbException.java
@@ -0,0 +1,132 @@
+//
+// Java Does USB
+// Copyright (c) 2022 Manuel Bleichenbacher
+// Licensed under MIT License
+// https://opensource.org/licenses/MIT
+//
+package net.codecrete.usb.windows;
+
+import net.codecrete.usb.UsbException;
+import net.codecrete.usb.UsbStallException;
+
+import java.lang.foreign.Arena;
+import java.lang.foreign.MemorySegment;
+
+import static java.lang.foreign.MemorySegment.NULL;
+import static java.lang.foreign.ValueLayout.ADDRESS;
+import static java.nio.charset.StandardCharsets.UTF_16LE;
+import static net.codecrete.usb.common.ForeignMemory.dereference;
+import static windows.win32.foundation.Apis.LocalFree;
+import static windows.win32.foundation.Constants.STATUS_UNSUCCESSFUL;
+import static windows.win32.foundation.WIN32_ERROR.ERROR_GEN_FAILURE;
+import static windows.win32.system.diagnostics.debug.Apis.FormatMessageW;
+import static windows.win32.system.diagnostics.debug.FORMAT_MESSAGE_OPTIONS.FORMAT_MESSAGE_ALLOCATE_BUFFER;
+import static windows.win32.system.diagnostics.debug.FORMAT_MESSAGE_OPTIONS.FORMAT_MESSAGE_FROM_HMODULE;
+import static windows.win32.system.diagnostics.debug.FORMAT_MESSAGE_OPTIONS.FORMAT_MESSAGE_FROM_SYSTEM;
+import static windows.win32.system.diagnostics.debug.FORMAT_MESSAGE_OPTIONS.FORMAT_MESSAGE_IGNORE_INSERTS;
+import static windows.win32.system.libraryloader.Apis.GetModuleHandleW;
+
+/**
+ * Exception thrown if a Windows specific error occurs.
+ */
+public class WindowsUsbException extends UsbException {
+
+ /**
+ * Creates a new instance.
+ *
+ * The message for the Windows error code is looked up and appended to the message.
+ *
+ *
+ * @param message exception message
+ * @param errorCode Windows error code (usually returned from {@code GetLastError()})
+ */
+ public WindowsUsbException(String message, int errorCode) {
+ super(String.format("%s: %s", message, getErrorMessage(errorCode)), errorCode);
+ }
+
+ /**
+ * Throws an exception for the specified Windows error code.
+ *
+ * The message for the Windows error code is looked up and appended to the message.
+ *
+ *
+ * @param errorCode Windows error code (usually returned from {@code GetLastError()})
+ * @param message exception message format ({@link String#format(String, Object...)} style)
+ * @param args arguments for exception message
+ */
+ static void throwException(int errorCode, String message, Object... args) {
+ var formattedMessage = String.format(message, args);
+ if (errorCode == ERROR_GEN_FAILURE || errorCode == STATUS_UNSUCCESSFUL) {
+ throw new UsbStallException(formattedMessage);
+ } else {
+ throw new WindowsUsbException(formattedMessage, errorCode);
+ }
+ }
+
+ /**
+ * Throws a USB exception.
+ *
+ * @param message exception message format ({@link String#format(String, Object...)} style)
+ * @param args arguments for exception message
+ */
+ static void throwException(String message, Object... args) {
+ throw new UsbException(String.format(message, args));
+ }
+
+ /**
+ * Throws an exception for the last error.
+ *
+ * The last Windows error code is taken from the call capture state
+ * {@link Win#LAST_ERROR_STATE} provided as the first parameter.
+ *
- * Quit with Ctrl-C or whatever stops a program on your platform.
- *
- */
-public class MonitorDevices {
-
- public static void main(String[] args) throws IOException {
- USB.setOnDeviceConnected((device) -> System.out.println("Connected: " + device.toString()));
- USB.setOnDeviceDisconnected((device) -> System.out.println("Disconnected: " + device.toString()));
-
- for (var device : USB.getAllDevices())
- System.out.println("Present: " + device.toString());
- System.out.println("Monitoring...");
-
- //noinspection ResultOfMethodCallIgnored
- System.in.read();
- }
-}
diff --git a/java-does-usb/src/test/java/net/codecrete/usb/special/Continuous.java b/java-does-usb/src/test/java/net/codecrete/usb/special/Continuous.java
new file mode 100644
index 00000000..c432972b
--- /dev/null
+++ b/java-does-usb/src/test/java/net/codecrete/usb/special/Continuous.java
@@ -0,0 +1,61 @@
+package net.codecrete.usb.special;
+
+import net.codecrete.usb.Usb;
+import net.codecrete.usb.UsbDevice;
+import net.codecrete.usb.UsbException;
+
+import java.io.IOException;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.Random;
+
+public class Continuous {
+
+ @SuppressWarnings("ResultOfMethodCallIgnored")
+ public static void main(String[] args) throws IOException {
+ var device = Usb.findDevice(0xcafe, 0xceaf)
+ .or(() -> Usb.findDevice(0xcafe, 0xcea0))
+ .orElseThrow(() -> new IllegalStateException("No test device connected"));
+ var interfaceNumber = device.getProductId() == 0xceaf ? 0 : 3;
+
+ device.open();
+ device.claimInterface(interfaceNumber);
+
+ new Thread(() -> readData(device)).start();
+ new Thread(() -> sendData(device)).start();
+
+ System.out.println("Press RETURN to exit");
+ System.in.read();
+ device.close();
+ }
+
+ @SuppressWarnings({"java:S2925", "BusyWait"})
+ private static void sendData(UsbDevice device) {
+ var random = new Random();
+ var buffer = new byte[40];
+
+ while (true) {
+ random.nextBytes(buffer);
+ try {
+ device.transferOut(1, buffer);
+ Thread.sleep(3000);
+ } catch (UsbException _) {
+ return;
+ } catch (InterruptedException _) {
+ Thread.currentThread().interrupt();
+ }
+ }
+ }
+
+ private static void readData(UsbDevice device) {
+ var formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
+ while (true) {
+ try {
+ var packet = device.transferIn(2);
+ System.out.printf("%s packet of %d bytes received%n", LocalDateTime.now().format(formatter), packet.length);
+ } catch (UsbException _) {
+ return;
+ }
+ }
+ }
+}
diff --git a/java-does-usb/src/test/java/net/codecrete/usb/sample/EnumerateDevices.java b/java-does-usb/src/test/java/net/codecrete/usb/special/EnumerateDevices.java
similarity index 74%
rename from java-does-usb/src/test/java/net/codecrete/usb/sample/EnumerateDevices.java
rename to java-does-usb/src/test/java/net/codecrete/usb/special/EnumerateDevices.java
index c18a3b45..a88ff90f 100644
--- a/java-does-usb/src/test/java/net/codecrete/usb/sample/EnumerateDevices.java
+++ b/java-does-usb/src/test/java/net/codecrete/usb/special/EnumerateDevices.java
@@ -5,9 +5,9 @@
// https://opensource.org/licenses/MIT
//
-package net.codecrete.usb.sample;
+package net.codecrete.usb.special;
-import net.codecrete.usb.USB;
+import net.codecrete.usb.Usb;
/**
* Sample program displaying the connected USB devices
@@ -15,7 +15,7 @@
public class EnumerateDevices {
public static void main(String[] args) {
- for (var device : USB.getAllDevices()) {
+ for (var device : Usb.getDevices()) {
System.out.println(device);
}
}
diff --git a/java-does-usb/src/test/java/net/codecrete/usb/special/LogicAnalyzer.java b/java-does-usb/src/test/java/net/codecrete/usb/special/LogicAnalyzer.java
new file mode 100644
index 00000000..006f6693
--- /dev/null
+++ b/java-does-usb/src/test/java/net/codecrete/usb/special/LogicAnalyzer.java
@@ -0,0 +1,377 @@
+//
+// Java Does USB
+// Copyright (c) 2023 Manuel Bleichenbacher
+// Licensed under MIT License
+// https://opensource.org/licenses/MIT
+//
+// Firmware for logic analyzer is from Sigrok project
+// and licensed under GNU GPL (version 2, or later).
+//
+
+package net.codecrete.usb.special;
+
+import net.codecrete.usb.Usb;
+import net.codecrete.usb.UsbControlTransfer;
+import net.codecrete.usb.UsbDevice;
+import net.codecrete.usb.UsbDirection;
+import net.codecrete.usb.UsbException;
+import net.codecrete.usb.UsbRecipient;
+import net.codecrete.usb.UsbRequestType;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Arrays;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.locks.Condition;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
+
+/**
+ * Sample program for sampling data with a logic analyzer.
+ *
+ * This sample assumes that a Saleae 8 clone with the VID 0x0925 and PID
+ * 0x3881 is connected.
+ *
+ *
+ * The USB communication of Saleae 8 clones (and any other logic analyzer
+ * based on a similar Cypress chip) is very sensitive as the chip only has
+ * a tiny internal buffer and – for the maximum 24 MHz sample rate – operates
+ * rather close to the practical limit of USB 2.0 high-speed. If the USB bus
+ * has other traffic from other devices or if the JVM takes a GC time-out,
+ * the internal buffer overruns and the logic analyzer stops data acquisition.
+ *
+ *
+ * The firmware for the Cypress chips needs to be uploaded once after the
+ * device has been connected. After the firmware upload, the device disconnect
+ * and reconnect after about 2 to 3 seconds. Vendor and product ID do not
+ * change but the manufacturer name and serial number do.
+ *
+ */
+public class LogicAnalyzer implements Closeable {
+
+ /// USB vendor ID
+ static final int VID = 0x0925;
+ /// USB product ID
+ static final int PID = 0x3881;
+
+ // bulk endpoint number
+ static final int EP = 2;
+
+ /// Effective sample rate (in Hz)
+ private int effSampleRate;
+ /// Sample period (in clock ticks)
+ private int period;
+ /// Sample duration (in ms)
+ private int duration;
+ /// Flag to use 48 MHz clock (instead of 30 MHz)
+ private boolean use48Mhz;
+ /// Flag that buffer overrun has been detected
+ private volatile boolean bufferOverrunDetected;
+ /// Flag that acquisition should/has stopped
+ private volatile boolean stopped;
+ /// Filename for saving sample data
+ private String filename;
+ /// Variable is updated while new data is received
+ private volatile long activityValue;
+ /// Indicates a dry run (to suppress output)
+ private boolean isDryRun;
+ /// Buffer size for input stream (good for approx. 0.2s of data)
+ private int bufferSize;
+
+ public static void main(String[] args) {
+ try (var logicAnalyzer = new LogicAnalyzer()) {
+ logicAnalyzer.sampleData(24000000, 5000, "sample.bin");
+ }
+ }
+
+ private UsbDevice device;
+
+ LogicAnalyzer() {
+ var optionalDevice = Usb.findDevice(VID, PID);
+ if (optionalDevice.isEmpty())
+ throw new IllegalStateException("no logic analyzer connected");
+
+ device = optionalDevice.get();
+
+ checkFirmware();
+
+ device.open();
+ device.claimInterface(0);
+
+ dryRun(12000000, 100);
+ dryRun(10000000, 200);
+ }
+
+ @Override
+ public void close() {
+ device.close();
+ }
+
+ /**
+ * Sample data and save it to the file
+ * @param sampleRate sample rate (in Hz)
+ * @param duration duration (in ms)
+ * @param filename filename to save to, or {@code null} to not save it
+ */
+ void sampleData(int sampleRate, int duration, String filename) {
+ if (sampleRate < 20000 || sampleRate > 24000000) {
+ System.err.println("Sample rate outside the supported range of 10kHz to 24Mhz");
+ return;
+ }
+
+ stopped = false;
+ bufferOverrunDetected = false;
+ this.filename = filename;
+ prepareSampling(sampleRate, duration);
+ var acquirer = CompletableFuture.runAsync(this::saveSamples);
+ sleep(50); // give thread time to start
+ startAcquisition();
+ var watchdog = CompletableFuture.runAsync(this::detectBufferOverrun);
+ CompletableFuture.allOf(acquirer, watchdog).join();
+ }
+
+ void dryRun(int sampleRate, int duration) {
+ isDryRun = true;
+ sampleData(sampleRate, duration, null);
+ isDryRun = false;
+ }
+
+ void prepareSampling(int sampleRate, int duration) {
+ this.duration = duration;
+
+ // calculate the optimal sample rate and if to use the 48 or 30 MHz clock
+ int ticks48Mhz = (48000000 + sampleRate / 2) / sampleRate;
+ int ticks30Mhz = (30000000 + sampleRate / 2) / sampleRate;
+ double err48Mhz = Math.abs(48000000.0 / ticks48Mhz - sampleRate);
+ double err30Mhz = Math.abs(30000000.0 / ticks30Mhz - sampleRate);
+
+ if (ticks48Mhz <= 0x0600 && err48Mhz <= err30Mhz) {
+ use48Mhz = true;
+ period = ticks48Mhz;
+ effSampleRate = 48000000 / period;
+ } else {
+ use48Mhz = false;
+ period = ticks30Mhz;
+ effSampleRate = 30000000 / period;
+ }
+
+ bufferSize = (int) Math.round(effSampleRate * 0.2);
+ bufferSize = Math.max(bufferSize, 16 * 4096);
+ }
+
+ /**
+ * Start acquisition with the specified sample rate
+ */
+ void startAcquisition() {
+ // Command structure (3 bytes)
+ // Bit 5: 0 - 8 bit samples, 1 - 16 bit samples
+ // Bit 6: 0 - 30 MHz clock, 1 - 48 MHz clock
+ // Byte 0: flags (not needed here)
+ // Byte 1-2: clock ticks (-1) between two samples (16 bit, big endian)
+
+ int ticks = period - 1;
+ byte flags = use48Mhz ? (byte) (1 << 6) : 0;
+
+ var cmd = new byte[3];
+ cmd[0] = flags;
+ cmd[1] = (byte) (ticks >> 8);
+ cmd[2] = (byte) (ticks & 0xff);
+
+ // send the start command
+ final int commandCodeStart = 0xb1;
+ device.controlTransferOut(new UsbControlTransfer(UsbRequestType.VENDOR, UsbRecipient.DEVICE, commandCodeStart, 0, 0), cmd);
+ }
+
+ void saveSamples() {
+
+ // retrieve the sample data from the bulk endpoint
+ int expectedSize = (int) (((long) duration * effSampleRate + 500) / 1000);
+
+ byte[] sampleData = new byte[expectedSize];
+
+ int size = 0;
+ try (var is = device.openInputStream(EP, bufferSize)) {
+
+ while (size < expectedSize) {
+ int n = is.read(sampleData, size, sampleData.length - size);
+ if (n <= 0)
+ break;
+ size += n;
+ activityValue = size;
+ }
+
+ } catch (UsbException e) {
+ if (!stopped && !bufferOverrunDetected)
+ throw e;
+
+ } catch (IOException e) {
+ System.err.println("Retrieving samples failed");
+ e.printStackTrace(System.err);
+ return;
+ }
+
+ if (bufferOverrunDetected) {
+ System.err.println("Buffer overflow, acquisition has stopped");
+ } else if (!stopped) {
+ stopAcquisition();
+ } else {
+ stopped = true;
+ }
+
+ if (!isDryRun)
+ System.out.printf("%,d samples retrieved with %,d sample/s%n", size, effSampleRate);
+
+ if (filename != null) {
+ try {
+ Files.write(Path.of(filename), sampleData);
+ } catch (IOException e) {
+ System.err.printf("Saving samples to %s failed%n", filename);
+ e.printStackTrace(System.err);
+ }
+ }
+ }
+
+ void stopAcquisition() {
+ // stop the acquisition by halting the bulk endpoint and clearing the halt
+ stopped = true;
+ device.abortTransfers(UsbDirection.IN, EP);
+ }
+
+ void detectBufferOverrun() {
+ sleep(10);
+ // if the 'activityValue' hasn't changed within 20ms, the logic analyzer
+ // has stopped sending data (likely due to a buffer overrun)
+ long lastValue = 0;
+ while (true) {
+ sleep(5);
+
+ if (stopped)
+ break;
+
+ if (lastValue == activityValue) {
+ bufferOverrunDetected = true;
+ stopAcquisition();
+ return;
+ }
+
+ lastValue = activityValue;
+ }
+ }
+
+ void checkFirmware() {
+ if (device.getManufacturer() != null) {
+ System.out.println("Device ready");
+ return;
+ }
+
+ System.out.println("Uploading firmware...");
+
+ byte[] firmware;
+ // load open-source firmware from Sigrok project (see http://sigrok.org/wiki/Fx2lafw)
+ try (var is = getClass().getClassLoader().getResourceAsStream("fx2lafw-saleae-logic.fw")) {
+ firmware = is.readAllBytes();
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+
+ device.open();
+ device.claimInterface(0);
+
+ byte[] cmd = new byte[]{1};
+ device.controlTransferOut(new UsbControlTransfer(UsbRequestType.VENDOR, UsbRecipient.DEVICE, 0xa0, 0xe600, 0x0000), cmd);
+
+ final int len = firmware.length;
+ int offset = 0;
+ while (offset < len) {
+ int n = Math.min(len - offset, 0x1000);
+ byte[] chunk = Arrays.copyOfRange(firmware, offset, offset + n);
+ device.controlTransferOut(new UsbControlTransfer(UsbRequestType.VENDOR, UsbRecipient.DEVICE, 0xa0, offset, 0x0000), chunk);
+ offset += n;
+ }
+
+ cmd = new byte[]{0};
+ device.controlTransferOut(new UsbControlTransfer(UsbRequestType.VENDOR, UsbRecipient.DEVICE, 0xa0, 0xe600, 0x0000), cmd);
+
+ device.close();
+ DeviceMonitor.instance().awaitDevice(false);
+
+ System.out.println("Waiting for device to reconnect...");
+
+ DeviceMonitor.instance().awaitDevice(true);
+ sleep(200);
+
+ var optionalDevice = Usb.findDevice(VID, PID);
+ if (optionalDevice.isEmpty())
+ throw new IllegalStateException("no logic analyzer connected");
+ device = optionalDevice.get();
+ if (device.getManufacturer() == null)
+ throw new IllegalStateException("firmware upload failed");
+
+ System.out.println("Device is ready");
+ }
+
+ void sleep(long milliseconds) {
+ while (true) {
+ try {
+ //noinspection BusyWait
+ Thread.sleep(milliseconds); // NOSONAR
+ return;
+ } catch (InterruptedException _) {
+ // ignore and try again
+ }
+ }
+ }
+
+
+ static class DeviceMonitor {
+
+ private static DeviceMonitor singleInstance;
+
+ private final Lock deviceLock = new ReentrantLock();
+ private final Condition deviceConnected = deviceLock.newCondition();
+ private boolean isDeviceConnected;
+
+ static synchronized DeviceMonitor instance() {
+ if (singleInstance == null) {
+ singleInstance = new DeviceMonitor();
+ singleInstance.start();
+ }
+ return singleInstance;
+ }
+
+ private DeviceMonitor() {
+ }
+
+ private void start() {
+ Usb.setOnDeviceConnected(dev -> onDeviceConnected(dev, true));
+ Usb.setOnDeviceDisconnected(dev -> onDeviceConnected(dev, false));
+ isDeviceConnected = Usb.findDevice(VID, PID).isPresent();
+ }
+
+ private void onDeviceConnected(UsbDevice device, boolean connected) {
+ if (device.getVendorId() == VID && device.getProductId() == PID) {
+ try {
+ deviceLock.lock();
+ isDeviceConnected = connected;
+ deviceConnected.signalAll();
+
+ } finally {
+ deviceLock.unlock();
+ }
+ }
+ }
+
+ void awaitDevice(boolean connected) {
+ try {
+ deviceLock.lock();
+ while (isDeviceConnected != connected)
+ deviceConnected.awaitUninterruptibly();
+
+ } finally {
+ deviceLock.unlock();
+ }
+ }
+ }
+}
diff --git a/java-does-usb/src/test/java/net/codecrete/usb/special/MonitorDevices.java b/java-does-usb/src/test/java/net/codecrete/usb/special/MonitorDevices.java
new file mode 100644
index 00000000..ebf0df0f
--- /dev/null
+++ b/java-does-usb/src/test/java/net/codecrete/usb/special/MonitorDevices.java
@@ -0,0 +1,62 @@
+//
+// Java Does USB
+// Copyright (c) 2022 Manuel Bleichenbacher
+// Licensed under MIT License
+// https://opensource.org/licenses/MIT
+//
+
+package net.codecrete.usb.special;
+
+import net.codecrete.usb.Usb;
+import net.codecrete.usb.UsbControlTransfer;
+import net.codecrete.usb.UsbDevice;
+import net.codecrete.usb.UsbRecipient;
+import net.codecrete.usb.UsbRequestType;
+
+import java.io.IOException;
+
+/**
+ * Test program that communicates with the device as soon as it has been plugged in.
+ *
+ * Quit with Ctrl-C or whatever stops a program on your platform.
+ *
+ */
+public class MonitorDevices {
+
+ public static void main(String[] args) throws IOException {
+ Usb.setOnDeviceConnected(device -> {
+ System.out.println("Connected: " + device.toString());
+ talkToTestDevice(device);
+ });
+ Usb.setOnDeviceDisconnected(device -> System.out.println("Disconnected: " + device.toString()));
+
+ for (var device : Usb.getDevices()) {
+ System.out.println("Present: " + device.toString());
+ talkToTestDevice(device);
+ }
+ System.out.println("Monitoring...");
+
+ //noinspection ResultOfMethodCallIgnored
+ System.in.read();
+ }
+
+ private static void talkToTestDevice(UsbDevice device) {
+ if (device.getVendorId() != 0xcafe)
+ return; // no test device
+
+ int interfaceNumber = device.getProductId() == 0xcea0 ? 2 : 0;
+ device.open();
+ device.claimInterface(interfaceNumber);
+ var response = device.controlTransferIn(
+ new UsbControlTransfer(UsbRequestType.VENDOR, UsbRecipient.INTERFACE,
+ (byte) 0x05, (short) 0, (short) interfaceNumber),
+ 1
+ );
+
+ if (response.length == 1 || interfaceNumber == response[0]) {
+ System.out.println("Device responded");
+ } else {
+ System.err.println("Invalid response from device");
+ }
+ }
+}
diff --git a/java-does-usb/src/test/java/net/codecrete/usb/special/USBSerial.java b/java-does-usb/src/test/java/net/codecrete/usb/special/USBSerial.java
new file mode 100644
index 00000000..ec4e4b67
--- /dev/null
+++ b/java-does-usb/src/test/java/net/codecrete/usb/special/USBSerial.java
@@ -0,0 +1,98 @@
+//
+// Java Does USB
+// Copyright (c) 2023 Manuel Bleichenbacher
+// Licensed under MIT License
+// https://opensource.org/licenses/MIT
+//
+
+package net.codecrete.usb.special;
+
+import net.codecrete.usb.Usb;
+import net.codecrete.usb.UsbControlTransfer;
+import net.codecrete.usb.UsbDevice;
+import net.codecrete.usb.UsbDirection;
+import net.codecrete.usb.UsbRecipient;
+import net.codecrete.usb.UsbRequestType;
+
+/**
+ * Interacts with a USB CDC device (serial device) directly, without using the
+ * standard USB CDC driver.
+ *
+ *
+ * This test is aimed at Linux, which requires to unload and restore the
+ * kernel driver. It will neither work on macOS (the USB CDC driver claims the
+ * interface with exclusive access) nor on Windows (only devices with the
+ * WinUSB driver can be opened).
+ *
+ *
+ * For the test to work on Linux, the user must have sufficient permission to
+ * access the device at the USB level. So check the permissions under
+ * /dev/bus/usb/... The permissions of the serial device (/dev/tty...) do not
+ * apply for this test.
+ *
+ */
+public class USBSerial {
+ public static void main(String[] args) {
+
+ for (var device : Usb.getDevices()) {
+ int commInterfaceNum = getCDCCommInterfaceNum(device);
+ if (commInterfaceNum >= 0) {
+ System.out.printf("USB CDC device: %s%n", device);
+ interact(device, commInterfaceNum);
+ }
+ }
+ }
+
+ static void interact(UsbDevice device, int commInterfaceNum) {
+ // communication and data interface must have consecutive numbers
+ int dataInterfaceNum = commInterfaceNum + 1;
+
+ // open device and interfaces
+ device.open();
+ device.claimInterface(commInterfaceNum);
+ device.claimInterface(dataInterfaceNum);
+
+ // set line coding (9600bps, 8 bit)
+ byte[] coding = {(byte) 0x80, 0x25, 0, 0, 0, 0, 8};
+ device.controlTransferOut(
+ new UsbControlTransfer(UsbRequestType.CLASS, UsbRecipient.INTERFACE, 0x20, 0, commInterfaceNum),
+ coding);
+
+ // send some data
+ int dataOutEp = getDataOutEndpointNum(device, dataInterfaceNum);
+ byte[] data = {'H', 'e', 'l', 'l', 'o', '\r', '\n'};
+ device.transferOut(dataOutEp, data);
+
+ // close device and interfaces
+ device.releaseInterface(dataInterfaceNum);
+ device.releaseInterface(commInterfaceNum);
+ device.close();
+ }
+
+ static int getCDCCommInterfaceNum(UsbDevice device) {
+ // CDC ACM implementations consist of two consecutive interfaces
+ // with certain class, subclass and protocol codes
+ int numInterfaces = device.getInterfaces().size();
+ if (numInterfaces < 2)
+ return -1;
+
+ for (int i = 0; i < numInterfaces - 1; i += 1) {
+ var commIntf = device.getInterface(i).getCurrentAlternate();
+ var dataIntf = device.getInterface(i + 1).getCurrentAlternate();
+
+ if (commIntf.getClassCode() == 2 && commIntf.getSubclassCode() == 2 && commIntf.getProtocolCode() == 1 && dataIntf.getClassCode() == 10)
+ return i;
+ }
+
+ return -1;
+ }
+
+ static int getDataOutEndpointNum(UsbDevice device, int dataInterfaceNum) {
+ var altInterface = device.getInterface(dataInterfaceNum).getCurrentAlternate();
+ for (var endpoint : altInterface.getEndpoints()) {
+ if (endpoint.getDirection() == UsbDirection.OUT)
+ return endpoint.getNumber();
+ }
+ return -1;
+ }
+}
diff --git a/java-does-usb/src/test/java/net/codecrete/usb/special/Unplug.java b/java-does-usb/src/test/java/net/codecrete/usb/special/Unplug.java
new file mode 100644
index 00000000..abba3d4c
--- /dev/null
+++ b/java-does-usb/src/test/java/net/codecrete/usb/special/Unplug.java
@@ -0,0 +1,328 @@
+//
+// Java Does USB
+// Copyright (c) 2023 Manuel Bleichenbacher
+// Licensed under MIT License
+// https://opensource.org/licenses/MIT
+//
+
+package net.codecrete.usb.special;
+
+import net.codecrete.usb.TestDeviceConfig;
+import net.codecrete.usb.Usb;
+import net.codecrete.usb.UsbDevice;
+import net.codecrete.usb.UsbException;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+
+import static java.time.Duration.ofSeconds;
+
+/**
+ * Test for robustness when USB devices is unplugged during operation.
+ *
+ *
+ * Requires use of test device.
+ *
+ */
+public class Unplug {
+ private static final Map activeDevices = new HashMap<>();
+
+ static void main() throws IOException {
+ System.out.println("Plug and unplug test device multiple times.");
+ System.out.println("Hit ENTER to exit.");
+
+ Usb.setOnDeviceConnected(Unplug::onPluggedDevice);
+ Usb.setOnDeviceDisconnected(Unplug::onUnpluggedDevice);
+ Usb.getDevices().forEach(Unplug::onPluggedDevice);
+
+ //noinspection ResultOfMethodCallIgnored
+ System.in.read();
+ }
+
+ private static void onPluggedDevice(UsbDevice device) {
+ var config = TestDeviceConfig.getConfig(device);
+ if (config.isEmpty())
+ return;
+
+ var worker = new DeviceWorker(device, config.get());
+ activeDevices.put(device, worker);
+ worker.start();
+ }
+
+ private static void onUnpluggedDevice(UsbDevice device) {
+ var config = TestDeviceConfig.getConfig(device);
+ if (config.isEmpty())
+ return;
+
+ var worker = activeDevices.remove(device);
+ worker.setDisconnectTime(System.currentTimeMillis());
+ worker.join();
+
+ // test handling of disconnected devices
+ new Thread(() -> {
+ sleep(2000);
+ try {
+ device.open();
+ System.err.println("Device should not be openable after disconnect");
+ } catch (UsbException e) {
+ if (!e.getMessage().contains("disconnected"))
+ System.err.println("Unexpected error: " + e.getMessage());
+ }
+ }).start();
+ }
+
+ @SuppressWarnings({"SameParameterValue", "java:S2925"})
+ private static void sleep(long millis) {
+ try {
+ Thread.sleep(millis);
+
+ } catch (InterruptedException _) {
+ Thread.currentThread().interrupt();
+ }
+ }
+
+ static class DeviceWorker {
+
+ private final UsbDevice device;
+ private final TestDeviceConfig config;
+
+ private final int seed;
+
+ private long disconnectTime;
+
+ private final Map workTracking = new HashMap<>();
+
+ DeviceWorker(UsbDevice device, TestDeviceConfig config) {
+ this.device = device;
+ this.config = config;
+ this.seed = (int) System.currentTimeMillis();
+ }
+
+ void start() {
+ System.out.println("Device connected");
+
+ device.open();
+ device.claimInterface(config.interfaceNumber());
+
+ // start loopback sender and receiver
+ startThread((seed & 1) != 0 ? this::sendLoopbackDataStream : this::sendLoopbackData);
+ startThread((seed & 2) != 0 ? this::receiveLoopbackDataStream : this::receiveLoopbackData);
+
+ // start echo sender and receiver
+ if (config.endpointEchoOut() > 0) {
+ startThread(this::sendEcho);
+ startThread(this::receiveEcho);
+ }
+ }
+
+ private void startThread(Runnable action) {
+ var thread = new Thread(() -> runAction(action));
+ var work = new Work();
+ workTracking.put(thread, work);
+ thread.start();
+ }
+
+ void join() {
+ // wait for threads to finish
+ for (var thread : workTracking.keySet()) {
+ try {
+ boolean terminated = thread.join(ofSeconds(5));
+ if (!terminated)
+ System.err.printf("Thread \"%s\" failed to join within 5s%n", thread.getName());
+
+ } catch (InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ device.close();
+
+ // check achieved work
+ for (var e : workTracking.entrySet()) {
+ var work = e.getValue();
+ var expectedWork = work.expectedWorkPerSec * 0.001 * (work.finishTime - work.startTime);
+ if (work.actualWork < expectedWork)
+ System.err.printf("Thread \"%s\" achieved insufficient work executed. Expected: %.0f, achieved: %d%n",
+ e.getKey().getName(), expectedWork, work.actualWork);
+ }
+
+ // check that the threads haven't terminated early
+ for (var e : workTracking.entrySet()) {
+ long duration = Math.abs(e.getValue().finishTime - disconnectTime);
+ if (duration > 500)
+ System.err.printf("Thread \"%s\" has likely crashed early%n", e.getKey().getName());
+ }
+
+ System.out.println("Device disconnected");
+ }
+
+ void setDisconnectTime(long time) {
+ disconnectTime = time;
+ }
+
+ private synchronized void logFinish() {
+ var work = workTracking.get(Thread.currentThread());
+ work.finishTime = System.currentTimeMillis();
+ }
+
+ private void logStart(String operation, long expectedWorkPerSec) {
+ Thread.currentThread().setName(operation);
+ var work = workTracking.get(Thread.currentThread());
+ work.startTime = System.currentTimeMillis();
+ work.expectedWorkPerSec = expectedWorkPerSec;
+ }
+
+ private void logWork(long amount) {
+ var work = workTracking.get(Thread.currentThread());
+ work.actualWork += amount;
+ }
+
+ private void runAction(Runnable action) {
+ try {
+ action.run();
+ } catch (UsbException _) {
+ logFinish();
+ }
+ }
+
+ private void sendLoopbackData() {
+ logStart("sending loopback data", 300_000);
+ var prng = new PRNG();
+ var data = new byte[5000];
+ //noinspection InfiniteLoopStatement
+ while (true) {
+ prng.fill(data);
+ device.transferOut(config.endpointLoopbackOut(), data, 1000);
+ logWork(data.length);
+ }
+ }
+
+ private void receiveLoopbackData() {
+ logStart("receiving loopback data", 300_000);
+ var prng = new PRNG();
+ //noinspection InfiniteLoopStatement
+ while (true) {
+ byte[] data = device.transferIn(config.endpointLoopbackIn());
+ int index = prng.verify(data);
+ if (index >= 0)
+ throw new RuntimeException("invalid data received");
+ logWork(data.length);
+ }
+ }
+
+ private void sendLoopbackDataStream() {
+ logStart("sending loopback data with output stream", 300_000);
+ var prng = new PRNG();
+ var data = new byte[5000];
+ try (var os = device.openOutputStream(config.endpointLoopbackOut())) {
+ //noinspection InfiniteLoopStatement
+ while (true) {
+ prng.fill(data);
+ os.write(data);
+ logWork(data.length);
+ }
+ } catch (IOException e) {
+ if (e.getCause() instanceof UsbException usbException)
+ throw usbException;
+ throw new RuntimeException(e);
+ }
+ }
+
+ private void receiveLoopbackDataStream() {
+ logStart("receiving loopback data with input stream", 300_000);
+ var prng = new PRNG();
+ try (var is = device.openInputStream(config.endpointLoopbackIn())) {
+ //noinspection InfiniteLoopStatement
+ while (true) {
+ var data = new byte[2000];
+ int n = is.read(data);
+ int index = prng.verify(data, n);
+ if (index >= 0)
+ throw new RuntimeException("invalid data received");
+ logWork(n);
+ }
+ } catch (IOException e) {
+ if (e.getCause() instanceof UsbException usbException)
+ throw usbException;
+ throw new RuntimeException(e);
+ }
+ }
+
+ private void sendEcho() {
+ logStart("sending echo", 7);
+ var data = new byte[]{0x03, 0x45, 0x73, (byte) 0xb3, (byte) 0x9f, 0x3f, 0x00, 0x6a};
+ //noinspection InfiniteLoopStatement
+ while (true) {
+ device.transferOut(config.endpointEchoOut(), data);
+ logWork(1);
+ sleep(100);
+ }
+ }
+
+ private void receiveEcho() {
+ logStart("receiving echo", 14);
+ //noinspection InfiniteLoopStatement
+ while (true) {
+ device.transferIn(config.endpointEchoIn());
+ logWork(1);
+ }
+ }
+ }
+
+ static class Work {
+ long startTime;
+ long expectedWorkPerSec;
+ long actualWork;
+ long finishTime;
+ }
+
+ /**
+ * Pseudo random number generator
+ */
+ static class PRNG {
+ private int state;
+ private int nBytes;
+ private int bits;
+
+ int next() {
+ int x = state;
+ x ^= x << 13;
+ x ^= x >>> 17;
+ x ^= x << 5;
+ state = x;
+ return x;
+ }
+
+ void fill(byte[] data) {
+ int len = data.length;
+ for (int i = 0; i < len; i++) {
+ if (nBytes == 0) {
+ bits = next();
+ nBytes = 4;
+ }
+ data[i] = (byte) bits;
+ bits >>>= 8;
+ nBytes--;
+ }
+ }
+
+ int verify(byte[] data) {
+ return verify(data, data.length);
+ }
+
+ int verify(byte[] data, int len) {
+ for (int i = 0; i < len; i++) {
+ if (nBytes == 0) {
+ bits = next();
+ nBytes = 4;
+ }
+ if (data[i] != (byte) bits)
+ return i;
+ bits >>>= 8;
+ nBytes--;
+ }
+ return -1;
+ }
+ }
+}
diff --git a/java-does-usb/src/test/java/net/codecrete/usb/usbstandard/StringDescriptorTest.java b/java-does-usb/src/test/java/net/codecrete/usb/usbstandard/StringDescriptorTest.java
new file mode 100644
index 00000000..d2b638be
--- /dev/null
+++ b/java-does-usb/src/test/java/net/codecrete/usb/usbstandard/StringDescriptorTest.java
@@ -0,0 +1,107 @@
+package net.codecrete.usb.usbstandard;
+
+import net.codecrete.usb.UsbException;
+import org.junit.jupiter.api.Test;
+
+import java.lang.foreign.Arena;
+import java.lang.foreign.MemorySegment;
+import java.util.function.Consumer;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+class StringDescriptorTest {
+
+ @Test
+ void validStringDescriptor() {
+ testDescriptor(
+ new byte[]{0x0c, 0x03, 'H', 0, 'e', 0, 'l', 0, 'l', 0, 'o', 0},
+ stringDescriptor -> {
+ assertThat(stringDescriptor.isValid()).isTrue();
+ assertThat(stringDescriptor.string()).isEqualTo("Hello");
+ }
+ );
+ }
+
+ @Test
+ void truncateTrailingZeros() {
+ testDescriptor(
+ new byte[]{0x0e, 0x03, 'H', 0, 'e', 0, 'l', 0, 'l', 0, 'o', 0, 0, 0},
+ stringDescriptor -> {
+ assertThat(stringDescriptor.isValid()).isTrue();
+ assertThat(stringDescriptor.string()).isEqualTo("Hello");
+ }
+ );
+ }
+
+ @Test
+ void invalidDescriptorType() {
+ testDescriptor(
+ new byte[]{0x0c, 0x04, 'H', 0, 'e', 0, 'l', 0, 'l', 0, 'o', 0},
+ stringDescriptor -> assertThat(stringDescriptor.isValid()).isFalse()
+ );
+ }
+
+ @Test
+ void inconsistentLength() {
+ testDescriptor(
+ new byte[]{0x0c, 0x03, 'H', 0, 'e', 0, 'l', 0, 'l', 0, 'o', 0, 0, 0},
+ stringDescriptor -> assertThat(stringDescriptor.isValid()).isFalse()
+ );
+ }
+
+ @Test
+ void inconsistentLengthThrowsException() {
+ testDescriptor(
+ new byte[]{0x0c, 0x03, 'H', 0, 'e', 0, 'l', 0, 'l', 0, 'o', 0, 0, 0},
+ stringDescriptor -> {
+ assertThat(stringDescriptor.isValid()).isFalse();
+ assertThatThrownBy(stringDescriptor::string)
+ .isInstanceOf(UsbException.class)
+ .hasMessage("String descriptor is invalid");
+ }
+ );
+ }
+
+ @Test
+ void oddLength() {
+ testDescriptor(
+ new byte[]{0x0d, 0x03, 'H', 0, 'e', 0, 'l', 0, 'l', 0, 'o', 0, 0},
+ stringDescriptor -> assertThat(stringDescriptor.isValid()).isFalse()
+ );
+ }
+
+ @Test
+ void unicodeSurrogates() {
+ // In theory, USB is stuck with an old Unicode standard and the below is invalid.
+ // In practice, it will work anyway.
+ testDescriptor(
+ new byte[]{0x06, 0x03, 0x3D, (byte) 0xD8, 0x1B, (byte) 0xDE},
+ stringDescriptor -> {
+ assertThat(stringDescriptor.isValid()).isTrue();
+ assertThat(stringDescriptor.string()).isEqualTo("\uD83D\uDE1B");
+ }
+ );
+ }
+
+ @Test
+ void invalidUnicodeCharactersAreReplaced() {
+ testDescriptor(
+ new byte[]{0x06, 0x03, 'H', 0, 0x1B, (byte) 0xDE},
+ stringDescriptor -> {
+ assertThat(stringDescriptor.isValid()).isTrue();
+ assertThat(stringDescriptor.string()).isEqualTo("H�");
+ }
+ );
+ }
+
+ private void testDescriptor(byte[] descriptorBytes, Consumer validator) {
+ try (var arena = Arena.ofConfined()) {
+ var memorySegment = arena.allocate(descriptorBytes.length);
+ memorySegment.copyFrom(MemorySegment.ofArray(descriptorBytes));
+
+ var stringDescriptor = new StringDescriptor(memorySegment);
+ validator.accept(stringDescriptor);
+ }
+ }
+}
diff --git a/java-does-usb/src/test/resources/fx2lafw-saleae-logic.fw b/java-does-usb/src/test/resources/fx2lafw-saleae-logic.fw
new file mode 100644
index 00000000..96167c13
Binary files /dev/null and b/java-does-usb/src/test/resources/fx2lafw-saleae-logic.fw differ
diff --git a/java-does-usb/src/test/resources/tinylog.properties b/java-does-usb/src/test/resources/tinylog.properties
new file mode 100644
index 00000000..9cf0c142
--- /dev/null
+++ b/java-does-usb/src/test/resources/tinylog.properties
@@ -0,0 +1,3 @@
+writer = console
+writer.format = {date: HH:mm:ss.SSS} {level|size=5} [{class|size=40}] {message}
+writer.level = info
diff --git a/reference/.vscode/settings.json b/reference/.vscode/settings.json
new file mode 100644
index 00000000..de1cf8d1
--- /dev/null
+++ b/reference/.vscode/settings.json
@@ -0,0 +1,63 @@
+{
+ "files.associations": {
+ "vector": "cpp",
+ "array": "cpp",
+ "atomic": "cpp",
+ "bit": "cpp",
+ "*.tcc": "cpp",
+ "bitset": "cpp",
+ "cctype": "cpp",
+ "chrono": "cpp",
+ "cinttypes": "cpp",
+ "clocale": "cpp",
+ "cmath": "cpp",
+ "compare": "cpp",
+ "concepts": "cpp",
+ "condition_variable": "cpp",
+ "cstdarg": "cpp",
+ "cstddef": "cpp",
+ "cstdint": "cpp",
+ "cstdio": "cpp",
+ "cstdlib": "cpp",
+ "cstring": "cpp",
+ "ctime": "cpp",
+ "cwchar": "cpp",
+ "cwctype": "cpp",
+ "deque": "cpp",
+ "map": "cpp",
+ "string": "cpp",
+ "unordered_map": "cpp",
+ "exception": "cpp",
+ "algorithm": "cpp",
+ "functional": "cpp",
+ "iterator": "cpp",
+ "memory": "cpp",
+ "memory_resource": "cpp",
+ "numeric": "cpp",
+ "random": "cpp",
+ "ratio": "cpp",
+ "regex": "cpp",
+ "string_view": "cpp",
+ "system_error": "cpp",
+ "tuple": "cpp",
+ "type_traits": "cpp",
+ "utility": "cpp",
+ "fstream": "cpp",
+ "initializer_list": "cpp",
+ "iosfwd": "cpp",
+ "iostream": "cpp",
+ "istream": "cpp",
+ "limits": "cpp",
+ "mutex": "cpp",
+ "new": "cpp",
+ "numbers": "cpp",
+ "ostream": "cpp",
+ "semaphore": "cpp",
+ "sstream": "cpp",
+ "stdexcept": "cpp",
+ "stop_token": "cpp",
+ "streambuf": "cpp",
+ "thread": "cpp",
+ "typeinfo": "cpp"
+ }
+}
\ No newline at end of file
diff --git a/reference/README.md b/reference/README.md
index 265d80e6..a8d4b960 100644
--- a/reference/README.md
+++ b/reference/README.md
@@ -6,3 +6,4 @@ The C++ code contains less error checking, less test cases and does not cover al
- [MacOS](macos) (for use with Xcode)
- [Windows](windows) (for use with Visual Studio)
+- [Linux](linux) (for use with CMake)
diff --git a/reference/linux/.vscode/settings.json b/reference/linux/.vscode/settings.json
index 9eac4d83..753dfa42 100644
--- a/reference/linux/.vscode/settings.json
+++ b/reference/linux/.vscode/settings.json
@@ -53,6 +53,10 @@
"stop_token": "cpp",
"streambuf": "cpp",
"thread": "cpp",
- "typeinfo": "cpp"
+ "typeinfo": "cpp",
+ "set": "cpp",
+ "fstream": "cpp",
+ "iomanip": "cpp",
+ "sstream": "cpp"
}
}
\ No newline at end of file
diff --git a/reference/linux/CMakeLists.txt b/reference/linux/CMakeLists.txt
index 925a2428..8cad5236 100644
--- a/reference/linux/CMakeLists.txt
+++ b/reference/linux/CMakeLists.txt
@@ -9,11 +9,17 @@ find_package(Threads REQUIRED)
set(SOURCES
assertion.cpp assertion.hpp
+ blocking_queue.hpp
+ config_parser.cpp config_parser.hpp
+ configuration.cpp configuration.hpp
main.cpp
+ prng.cpp prng.hpp
scope.hpp
+ speed_test.cpp speed_test.hpp
tests.cpp tests.hpp
usb_device.cpp usb_device.hpp
usb_error.cpp usb_error.hpp
+ usb_iostream.cpp usb_iostream.hpp
usb_registry.cpp usb_registry.hpp
)
diff --git a/reference/linux/assertion.cpp b/reference/linux/assertion.cpp
index 4e3111b5..eed1265f 100644
--- a/reference/linux/assertion.cpp
+++ b/reference/linux/assertion.cpp
@@ -4,7 +4,7 @@
// Licensed under MIT License
// https://opensource.org/licenses/MIT
//
-// Reference C++ code for macOS
+// Reference C++ code common for Linux / macOS / Windows
//
#include "assertion.hpp"
@@ -16,7 +16,7 @@ static void failed(const char* check, const char* message) {
if (message == nullptr)
message = "Check failed";
std::cerr << message << ": " << check << std::endl;
- throw new check_failed_error();
+ throw check_failed_error();
}
void assert_equals(int expected, int actual, const char* message) {
diff --git a/reference/linux/assertion.hpp b/reference/linux/assertion.hpp
index 1d763e13..75d089fd 100644
--- a/reference/linux/assertion.hpp
+++ b/reference/linux/assertion.hpp
@@ -4,7 +4,7 @@
// Licensed under MIT License
// https://opensource.org/licenses/MIT
//
-// Reference C++ code for macOS
+// Reference C++ code common for Linux / macOS / Windows
//
#pragma once
diff --git a/reference/linux/blocking_queue.hpp b/reference/linux/blocking_queue.hpp
new file mode 100644
index 00000000..4b2a8b49
--- /dev/null
+++ b/reference/linux/blocking_queue.hpp
@@ -0,0 +1,70 @@
+//
+// Java Does USB
+// Copyright (c) 2023 Manuel Bleichenbacher
+// Licensed under MIT License
+// https://opensource.org/licenses/MIT
+//
+// Reference C++ code common for Linux / macOS / Windows
+//
+
+#pragma once
+
+#include
+#include
+#include
+
+
+/**
+ * Blocking FIFO queue for passing work from one thread to another.
+ *
+ * The queue is unbounded.
+ */
+template
+class blocking_queue {
+public:
+ /**
+ * Indicates if the queue is empty.
+ *
+ * @return 'true' if the queue is empty, 'false' if the queue contains elements
+ */
+ bool empty() const {
+ std::lock_guard lock(guard);
+ return queue.empty();
+ }
+
+ /**
+ * Adds the item to the end of the queue.
+ *
+ * @param item item to add
+ */
+ void put(const T& item) {
+ {
+ std::lock_guard lock(guard);
+ queue.push(item);
+ }
+
+ signal.notify_one();
+ }
+
+ /**
+ * Takes the oldest item from the queue and removes it.
+ *
+ * Waits until an item is available.
+ *
+ * @return item removed from queue
+ */
+ T take() {
+ std::unique_lock lock(guard);
+ while (queue.empty())
+ signal.wait(lock);
+
+ T item = queue.front();
+ queue.pop();
+ return item;
+ }
+
+private:
+ std::queue queue;
+ mutable std::mutex guard;
+ std::condition_variable signal;
+};
diff --git a/reference/linux/config_parser.cpp b/reference/linux/config_parser.cpp
new file mode 100644
index 00000000..66b13ec2
--- /dev/null
+++ b/reference/linux/config_parser.cpp
@@ -0,0 +1,189 @@
+//
+// Java Does USB
+// Copyright (c) 2022 Manuel Bleichenbacher
+// Licensed under MIT License
+// https://opensource.org/licenses/MIT
+//
+// Reference C++ code common for Linux / macOS / Windows
+//
+
+#include "config_parser.hpp"
+#include "usb_error.hpp"
+#include
+
+
+// --- USB descriptor types
+
+enum class usb_descriptor_type : uint8_t {
+ configuration = 0x02,
+ string = 0x03,
+ interface = 0x04,
+ endpoint = 0x05,
+ interface_association = 0x0b
+};
+
+
+// --- USB configuration descriptor
+
+#pragma pack(push, 1)
+
+struct usb_config_desc {
+ uint8_t bLength;
+ uint8_t bDescriptorType;
+ uint16_t wTotalLength;
+ uint8_t bNumInterfaces;
+ uint8_t bConfigurationValue;
+ uint8_t iConfiguration;
+ uint8_t bmAttributes;
+ uint8_t bMaxPower;
+};
+
+#pragma pack(pop)
+
+
+// --- USB interface descriptor
+
+#pragma pack(push, 1)
+
+struct usb_interface_desc {
+ uint8_t bLength;
+ uint8_t bDescriptorType;
+ uint8_t bInterfaceNumber;
+ uint8_t bAlternateSetting;
+ uint8_t bNumEndpoints;
+ uint8_t bInterfaceClass;
+ uint8_t bInterfaceSubClass;
+ uint8_t bInterfaceProtocol;
+ uint8_t iInterface;
+};
+
+#pragma pack(pop)
+
+
+// --- USB interface association descriptor
+
+#pragma pack(push, 1)
+
+struct usb_interface_association_desc {
+ uint8_t bLength;
+ uint8_t bDescriptorType;
+ uint8_t bFirstInterface;
+ uint8_t bInterfaceCount;
+ uint8_t bFunctionClass;
+ uint8_t bFunctionSubClass;
+ uint8_t bFunctionProtocol;
+ uint8_t iFunction;
+};
+
+#pragma pack(pop)
+
+
+// --- USB endpoint descriptor
+
+#pragma pack(push, 1)
+
+ struct usb_endpoint_desc {
+ uint8_t bLength;
+ uint8_t bDescriptorType;
+ uint8_t bEndpointAddress;
+ uint8_t bmAttributes;
+ uint16_t wMaxPacketSize;
+ uint8_t bInterval;
+ };
+
+#pragma pack(pop)
+
+
+// --- Configuration parser
+
+ static int peek_desc_length(const uint8_t* desc, int offset) { return desc[offset]; }
+ static usb_descriptor_type peek_desc_type(const uint8_t* desc, int offset) { return static_cast(desc[offset + 1]); }
+
+
+ config_parser::config_parser()
+ : configuration_value(0) { }
+
+void config_parser::parse(const uint8_t* config_desc, int desc_len)
+{
+ const usb_config_desc* header = reinterpret_cast(config_desc);
+ if (desc_len <= sizeof(usb_config_desc)
+ || header->bDescriptorType != static_cast(usb_descriptor_type::configuration)
+ || header->wTotalLength != desc_len)
+ throw usb_error("Invalid configuration descriptor");
+
+ configuration_value = header->bConfigurationValue;
+
+ int offset = peek_desc_length(config_desc, 0);
+ usb_alternate_interface* last_alternate = nullptr;
+
+ while (offset + 2 < desc_len) {
+
+ int len = peek_desc_length(config_desc, offset);
+ usb_descriptor_type type = peek_desc_type(config_desc, offset);
+
+ if (offset + len > desc_len)
+ break;
+
+ if (type == usb_descriptor_type::interface_association) {
+
+ const usb_interface_association_desc* ia_desc = reinterpret_cast(config_desc + offset);
+ functions.push_back(usb_composite_function(ia_desc->bFirstInterface, ia_desc->bInterfaceCount,
+ ia_desc->bFunctionClass, ia_desc->bFunctionSubClass, ia_desc->bFunctionProtocol));
+ last_alternate = nullptr;
+
+ } else if (type == usb_descriptor_type::interface) {
+
+ const usb_interface_desc* intf_desc = reinterpret_cast(config_desc + offset);
+ int number = intf_desc->bInterfaceNumber;
+
+ // If there is no interface with this number yet, it's a new interface
+ // and not just an additional alternate interface.
+ auto intf = get_interface(number);
+ if (intf == nullptr) {
+ interfaces.push_back(usb_interface(number));
+ intf = &interfaces.back();
+ }
+
+ // add alternate interface
+ last_alternate = intf->add_alternate(usb_alternate_interface(intf_desc->bAlternateSetting,
+ intf_desc->bInterfaceClass, intf_desc->bInterfaceSubClass, intf_desc->bInterfaceProtocol));
+
+ // If there is no function for this interface, there was not preceeding IAD.
+ // So create a new function with a single interface.
+ if (get_function(intf->number()) == nullptr)
+ functions.push_back(usb_composite_function(intf->number(), 1, last_alternate->class_code(), last_alternate->subclass_code(), last_alternate->protocol_code()));
+
+ } else if (type == usb_descriptor_type::endpoint) {
+
+ if (last_alternate == nullptr)
+ throw usb_error("invalid configuration descriptor");
+
+ const usb_endpoint_desc* ep_desc = reinterpret_cast(config_desc + offset);
+ last_alternate->add_endpoint(usb_endpoint(ep_desc->bEndpointAddress & 0x7f, static_cast(ep_desc->bEndpointAddress & 0x80),
+ static_cast(ep_desc->bmAttributes & 0x03), ep_desc->wMaxPacketSize));
+ }
+
+ offset += len;
+ }
+
+ if (offset != desc_len)
+ throw usb_error("invalid configuration descriptor");
+}
+
+usb_interface* config_parser::get_interface(int number) {
+ auto iter = std::find_if(interfaces.begin(), interfaces.end(), [number](const usb_interface& itf) { return itf.number() == number; });
+ if (iter == interfaces.end())
+ return nullptr;
+
+ return &*iter;
+}
+
+usb_composite_function* config_parser::get_function(int intf_number) {
+ auto iter = std::find_if(functions.begin(), functions.end(), [intf_number](const usb_composite_function& f) {
+ return intf_number >= f.first_interface() && intf_number < f.first_interface() + f.num_interfaces();
+ });
+ if (iter == functions.end())
+ return nullptr;
+
+ return &*iter;
+}
diff --git a/reference/linux/config_parser.hpp b/reference/linux/config_parser.hpp
new file mode 100644
index 00000000..04f6615c
--- /dev/null
+++ b/reference/linux/config_parser.hpp
@@ -0,0 +1,31 @@
+//
+// Java Does USB
+// Copyright (c) 2022 Manuel Bleichenbacher
+// Licensed under MIT License
+// https://opensource.org/licenses/MIT
+//
+// Reference C++ code common for Linux / macOS / Windows
+//
+
+#pragma once
+
+#include
+#include "configuration.hpp"
+
+///
+/// Parses a USB configuration descriptor.
+///
+class config_parser {
+public:
+ config_parser();
+ void parse(const uint8_t* config_desc, int desc_len);
+
+ uint8_t configuration_value;
+
+ std::vector interfaces;
+ std::vector functions;
+
+private:
+ usb_interface* get_interface(int number);
+ usb_composite_function* get_function(int intf_number);
+};
diff --git a/reference/linux/configuration.cpp b/reference/linux/configuration.cpp
new file mode 100644
index 00000000..3db7235c
--- /dev/null
+++ b/reference/linux/configuration.cpp
@@ -0,0 +1,64 @@
+//
+// Java Does USB
+// Copyright (c) 2022 Manuel Bleichenbacher
+// Licensed under MIT License
+// https://opensource.org/licenses/MIT
+//
+// Reference C++ code common for Linux / macOS / Windows
+//
+
+#include "configuration.hpp"
+
+// --- usb_endpoint
+
+usb_endpoint::usb_endpoint(int number, usb_direction direction, usb_transfer_type transfer_type, int packet_size)
+ : number_(number), direction_(direction), transfer_type_(transfer_type), packet_size_(packet_size) { }
+
+usb_endpoint usb_endpoint::invalid(-1, usb_direction::out, usb_transfer_type::bulk, 0);
+
+
+// --- usb_alternate_interface
+
+usb_alternate_interface::usb_alternate_interface(int number, int class_code, int subclass_code, int protocol_code)
+ : number_(number), class_code_(class_code), subclass_code_(subclass_code), protocol_code_(protocol_code) { }
+
+void usb_alternate_interface::add_endpoint(usb_endpoint&& endpoint) {
+ endpoints_.push_back(std::move(endpoint));
+}
+
+
+usb_alternate_interface usb_alternate_interface::invalid(-1, 0, 0, 0);
+
+
+// --- usb_interface
+
+usb_interface::usb_interface(int number)
+ : number_(number), is_claimed_(false), alternate_index_(0) { }
+
+const usb_alternate_interface& usb_interface::alternate() const {
+ if (alternate_index_ < 0 || alternate_index_ >= alternates_.size())
+ return usb_alternate_interface::invalid;
+ return alternates_[alternate_index_];
+}
+
+void usb_interface::set_claimed(bool claimed) {
+ is_claimed_ = claimed;
+}
+
+
+usb_alternate_interface* usb_interface::add_alternate(usb_alternate_interface&& alternate) {
+ alternates_.push_back(std::move(alternate));
+ return &alternates_.back();
+}
+
+void usb_interface::set_alternate(int index) {
+ alternate_index_ = index;
+}
+
+usb_interface usb_interface::invalid(-1);
+
+
+// --- usb_composite_function
+
+usb_composite_function::usb_composite_function(int first_interface, int num_interfaces, int class_code, int subclass_code, int protocol_code)
+ : first_interface_(first_interface), num_interfaces_(num_interfaces), class_code_(class_code), subclass_code_(subclass_code), protocol_code_(protocol_code) { }
diff --git a/reference/linux/configuration.hpp b/reference/linux/configuration.hpp
new file mode 100644
index 00000000..5f3dedc9
--- /dev/null
+++ b/reference/linux/configuration.hpp
@@ -0,0 +1,162 @@
+//
+// Java Does USB
+// Copyright (c) 2022 Manuel Bleichenbacher
+// Licensed under MIT License
+// https://opensource.org/licenses/MIT
+//
+// Reference C++ code common for Linux / macOS / Windows
+//
+
+#pragma once
+
+#include
+#include
+
+/// USB endpoint direction
+enum class usb_direction : uint8_t {
+ /// Direction OUT: host to device
+ out = 0x00,
+ /// Direction IN: device to host
+ in = 0x80
+};
+
+/// USB endpoint transfer type
+enum class usb_transfer_type : uint8_t {
+ /// Control transfer
+ control = 0x00,
+ /// Isochronous transfer
+ isochronous = 0x01,
+ /// Bulk transfer
+ bulk = 0x02,
+ /// Interrupt transfer
+ interrupt = 0x03
+};
+
+///
+/// USB endpoint
+///
+struct usb_endpoint {
+public:
+ /// Endpoint number
+ int number() const { return number_; }
+ /// Endpoint direction
+ usb_direction direction() const { return direction_; }
+ /// Endpoint transfer type
+ usb_transfer_type transfer_type() const { return transfer_type_; }
+ /// Maximum packet size
+ int packet_size() const { return packet_size_; }
+ /// Indicates if this endpoint is valid (or a return value indicating an error)
+ bool is_valid() const { return number_ >= 0; }
+
+private:
+ usb_endpoint(int number, usb_direction direction, usb_transfer_type transfer_type, int packet_size);
+
+ int number_;
+ usb_direction direction_;
+ usb_transfer_type transfer_type_;
+ int packet_size_;
+
+ static usb_endpoint invalid;
+
+ friend class config_parser;
+ friend class usb_device;
+};
+
+///
+/// USB alternate interface
+///
+struct usb_alternate_interface {
+public:
+ /// Alternate number
+ int number() const { return number_; }
+ /// Interface class code
+ int class_code() const { return class_code_; }
+ /// Interface subclass code
+ int subclass_code() const { return subclass_code_; }
+ /// Interface protocol code
+ int protocol_code() const { return protocol_code_; }
+ /// List of endpoints
+ const std::vector& endpoints() const { return endpoints_; }
+ /// Indicates if this alternate interface is valid (or a return value indicating an error)
+ bool is_valid() const { return number_ >= 0; }
+
+private:
+ usb_alternate_interface(int number, int class_code, int subclass_code, int protocol_code);
+ void add_endpoint(usb_endpoint&& endpoint);
+
+ int number_;
+ int class_code_;
+ int subclass_code_;
+ int protocol_code_;
+ std::vector endpoints_;
+
+ static usb_alternate_interface invalid;
+
+ friend class config_parser;
+ friend struct usb_interface;
+};
+
+///
+/// USB interface
+///
+struct usb_interface {
+public:
+ /// Interface number
+ int number() const { return number_; }
+ /// Indicates if interface has been claimed
+ bool is_claimed() const { return is_claimed_; }
+ /// Currently selected alternate interface
+ const usb_alternate_interface& alternate() const;
+ /// Indicates if this interface is valid (or a return value indicating an error)
+ bool is_valid() const { return number_ >= 0; }
+ /// List of all alternate interfaces of this interfaces
+ const std::vector& alternates() const { return alternates_; }
+
+private:
+ usb_interface(int number);
+ void set_claimed(bool claimed);
+ usb_alternate_interface* add_alternate(usb_alternate_interface&& alternate);
+ void set_alternate(int index);
+
+ int number_;
+ bool is_claimed_;
+ int alternate_index_;
+ std::vector alternates_;
+
+ static usb_interface invalid;
+
+ friend class config_parser;
+ friend class usb_device;
+};
+
+///
+/// USB composite function
+///
+/// For a composite USB device, the composite function describes a single function.
+/// A compsite function consists of a single or multiple consecutive interfaces.
+///
+///
+struct usb_composite_function {
+public:
+ /// Number of first interface
+ int first_interface() const { return first_interface_; }
+ /// Number of interfaces
+ int num_interfaces() const { return num_interfaces_; }
+ /// Function class code
+ int class_code() const { return class_code_; }
+ /// Function subclass code
+ int subclass_code() const { return subclass_code_; }
+ /// Function protocol code
+ int protocol_code() const { return protocol_code_; }
+
+private:
+ usb_composite_function(int first_interface, int num_interfaces, int class_code, int subclass_code, int protocol_code);
+
+ int first_interface_;
+ int num_interfaces_;
+ int class_code_;
+ int subclass_code_;
+ int protocol_code_;
+
+ friend class config_parser;
+};
diff --git a/reference/linux/main.cpp b/reference/linux/main.cpp
index 64e85de5..cbb3747c 100644
--- a/reference/linux/main.cpp
+++ b/reference/linux/main.cpp
@@ -4,7 +4,7 @@
// Licensed under MIT License
// https://opensource.org/licenses/MIT
//
-// Reference C++ code for macOS
+// Reference C++ code common for Linux / macOS / Windows
//
#include "tests.hpp"
diff --git a/reference/linux/prng.cpp b/reference/linux/prng.cpp
new file mode 100644
index 00000000..c9cd0b1a
--- /dev/null
+++ b/reference/linux/prng.cpp
@@ -0,0 +1,80 @@
+//
+// Java Does USB
+// Copyright (c) 2022 Manuel Bleichenbacher
+// Licensed under MIT License
+// https://opensource.org/licenses/MIT
+//
+// Reference C++ code common for Linux / macOS / Windows
+//
+
+#include "prng.hpp"
+
+prng::prng(uint32_t init) : state(init), nbytes(0), bits(0) {}
+
+void prng::reset(uint32_t init)
+{
+ state = init;
+ nbytes = 0;
+ bits = 0;
+}
+
+uint32_t prng::next()
+{
+ uint32_t x = state;
+ x ^= x << 13;
+ x ^= x >> 17;
+ x ^= x << 5;
+ state = x;
+ return x;
+}
+
+void prng::fill(uint8_t *buf, int len)
+{
+ for (int i = 0; i < len; i++)
+ {
+ if (nbytes == 0)
+ {
+ bits = next();
+ nbytes = 4;
+ }
+ buf[i] = bits;
+ bits >>= 8;
+ nbytes--;
+ }
+}
+
+void prng::fill(std::vector& buf, int len)
+{
+ if (len == -1 || len > buf.size())
+ len = static_cast(buf.size());
+
+ fill(buf.data(), len);
+}
+
+int prng::verify(const uint8_t *buf, int len)
+{
+ for (int i = 0; i < len; i++)
+ {
+ if (nbytes == 0)
+ {
+ bits = next();
+ nbytes = 4;
+ }
+
+ if (buf[i] != (uint8_t)bits)
+ return i;
+
+ bits >>= 8;
+ nbytes--;
+ }
+
+ return -1;
+}
+
+int prng::verify(const std::vector &buf, int len)
+{
+ if (len == -1 || len > buf.size())
+ len = static_cast(buf.size());
+
+ return verify(buf.data(), len);
+}
diff --git a/reference/linux/prng.hpp b/reference/linux/prng.hpp
new file mode 100644
index 00000000..5214dfa6
--- /dev/null
+++ b/reference/linux/prng.hpp
@@ -0,0 +1,72 @@
+//
+// Java Does USB
+// Copyright (c) 2022 Manuel Bleichenbacher
+// Licensed under MIT License
+// https://opensource.org/licenses/MIT
+//
+// Reference C++ code common for Linux / macOS / Windows
+//
+
+#pragma once
+
+#include
+#include
+
+/**
+ * Pseudo Random Number Generator
+ */
+struct prng
+{
+ /**
+ * Constructs a new instance
+ * @param init initial value
+ */
+ prng(uint32_t init);
+
+ /**
+ * Returns the next pseudo random value
+ * @return pseudo random value
+ */
+ uint32_t next();
+
+ /**
+ * Fills the buffer with pseudo random data
+ * @param buf buffer receiving the random data
+ * @param len length of the buffer (in bytes)
+ */
+ void fill(uint8_t *buf, int len);
+
+ /**
+ * Fills the buffer with pseudo random data
+ * @param buf buffer receiving the random data
+ * @param len number of bytes to fill (-1 for entire buffer)
+ */
+ void fill(std::vector& buf, int len = -1);
+
+ /**
+ * Verifies that the passed data matches the next bytes of the sequence.
+ * @param buf buffer with data to verify
+ * @param len length of the buffer (in bytes)
+ * @return -1 if they match, otherwise the position of the difference
+ */
+ int verify(const uint8_t *buf, int len);
+
+ /**
+ * Verifies that the passed data matches the next bytes of the sequence.
+ * @param buf buffer with data to verify
+ * @param len number of bytes to verify (-1 for entire buffer)
+ * @return -1 if they match, otherwise the position of the difference
+ */
+ int verify(const std::vector& buf, int len = -1);
+
+ /**
+ * Resets the generator to its initial state.
+ * @param init initial value
+ */
+ void reset(uint32_t init);
+
+private:
+ uint32_t state;
+ int nbytes;
+ uint32_t bits;
+};
diff --git a/reference/linux/scope.hpp b/reference/linux/scope.hpp
index 53e3e39f..85998dba 100644
--- a/reference/linux/scope.hpp
+++ b/reference/linux/scope.hpp
@@ -4,7 +4,7 @@
// Licensed under MIT License
// https://opensource.org/licenses/MIT
//
-// Reference C++ code for macOS
+// Reference C++ code common for Linux / macOS / Windows
//
#pragma once
diff --git a/reference/linux/speed_test.cpp b/reference/linux/speed_test.cpp
new file mode 100644
index 00000000..ab60607b
--- /dev/null
+++ b/reference/linux/speed_test.cpp
@@ -0,0 +1,142 @@
+//
+// Java Does USB
+// Copyright (c) 2022 Manuel Bleichenbacher
+// Licensed under MIT License
+// https://opensource.org/licenses/MIT
+//
+// Reference C++ code common for Linux / macOS / Windows
+//
+
+#include "speed_test.hpp"
+
+#include "prng.hpp"
+#include "usb_error.hpp"
+
+#include
+#include
+#include
+#include
+
+
+static constexpr uint32_t PRNG_INIT = 0x7b;
+
+
+speed_test::speed_test(usb_device_ptr device, int ep_out, int ep_in)
+: device_(device), ep_out_(ep_out), ep_in_(ep_in) { }
+
+void speed_test::run(int num_bytes) {
+ reset_buffers();
+ start_measurement();
+
+ std::thread sender(&speed_test::transmit, this, num_bytes / 2);
+ bool successful = receive(num_bytes / 2);
+ sender.join();
+
+ if (successful)
+ stop_measurement();
+}
+
+bool speed_test::transmit(int num_bytes) {
+
+ prng seq(PRNG_INIT);
+
+ std::vector buf(2000);
+ int pos = 0;
+
+ auto is_ptr = device_->open_output_stream(ep_out_);
+ std::ostream& os = *is_ptr;
+
+ while (num_bytes > 0) {
+ int n = std::min(num_bytes, static_cast(buf.size()));
+ try {
+ seq.fill(buf, n);
+ os.write(reinterpret_cast(buf.data()), n);
+
+ } catch (usb_error& error) {
+ std::cerr << std::endl << "ERROR: " << error.what() << " (writing at pos " << pos << ")" << std::endl;
+ return false;
+ }
+
+ num_bytes -= n;
+ pos += n;
+ update_progress(n);
+ }
+
+ return true;
+}
+
+bool speed_test::receive(int num_bytes) {
+
+ prng seq(PRNG_INIT);
+ int pos = 0;
+ std::vector data;
+
+ auto is_ptr = device_->open_input_stream(ep_in_);
+ std::istream& is = *is_ptr;
+
+ while (num_bytes > 0) {
+ int n;
+ try {
+ data.resize(2000);
+ is.read(reinterpret_cast(data.data()), data.capacity());
+ n = static_cast(is.gcount());
+ if (n <= 0)
+ break;
+ data.resize(n);
+
+ int p = seq.verify(data, n);
+ if (p != -1) {
+ std::cerr << std::endl << "Invalid data received at pos " << (pos + p) << std::endl;
+ return false;
+ }
+
+ } catch (usb_error& error) {
+ std::cerr << std::endl << "ERROR: " << error.what() << " (reading at pos " << pos << ")" << std::endl;
+ return false;
+ }
+
+ num_bytes -= n;
+ pos += n;
+
+ update_progress(n);
+ }
+
+ if (num_bytes != 0) {
+ std::cerr << std::endl << "ERROR: EOF encountered after " << pos << " bytes" << std::endl;
+ }
+
+ return true;
+}
+
+void speed_test::reset_buffers() {
+ usb_control_request request_set_value_no_data = { 0 };
+ request_set_value_no_data.bmRequestType = usb_control_request::request_type(usb_request_type::direction_out,
+ usb_request_type::type_vendor, usb_request_type::recipient_interface);
+ request_set_value_no_data.bRequest = 0x04;
+ request_set_value_no_data.wIndex = 0; // interface number
+ device_->control_transfer(request_set_value_no_data);
+
+}
+
+
+// --- throughput measurement ---
+
+void speed_test::start_measurement() {
+ start_time = std::chrono::high_resolution_clock::now();
+ processed_bytes = 0;
+}
+
+void speed_test::update_progress(int n) {
+ std::lock_guard lock(progress_mutex);
+ processed_bytes += n;
+}
+
+int speed_test::stop_measurement() {
+ auto end_time = std::chrono::high_resolution_clock::now();
+ std::chrono::duration dur = end_time - start_time;
+ double thr = processed_bytes / dur.count();
+
+ std::cout << "Throughput: " << std::fixed << std::setprecision(1) << thr << " kByte/s" << std::endl;
+
+ return (int)(thr * 1000);
+}
diff --git a/reference/linux/speed_test.hpp b/reference/linux/speed_test.hpp
new file mode 100644
index 00000000..99f19277
--- /dev/null
+++ b/reference/linux/speed_test.hpp
@@ -0,0 +1,38 @@
+//
+// Java Does USB
+// Copyright (c) 2022 Manuel Bleichenbacher
+// Licensed under MIT License
+// https://opensource.org/licenses/MIT
+//
+// Reference C++ code common for Linux / macOS / Windows
+//
+
+#pragma once
+
+#include "usb_device.hpp"
+
+#include
+#include
+
+class speed_test {
+public:
+ speed_test(usb_device_ptr device, int ep_out, int ep_in);
+ void run(int num_bytes);
+
+private:
+ void reset_buffers();
+ bool transmit(int num_bytes);
+ bool receive(int num_bytes);
+
+ void start_measurement();
+ void update_progress(int n);
+ int stop_measurement();
+
+ usb_device_ptr device_;
+ int ep_out_;
+ int ep_in_;
+
+ std::chrono::time_point start_time;
+ int processed_bytes;
+ std::mutex progress_mutex;
+};
diff --git a/reference/linux/tests.cpp b/reference/linux/tests.cpp
index 6b53c83f..9113dc53 100644
--- a/reference/linux/tests.cpp
+++ b/reference/linux/tests.cpp
@@ -4,11 +4,12 @@
// Licensed under MIT License
// https://opensource.org/licenses/MIT
//
-// Reference C++ code for macOS
+// Reference C++ code common for Linux / macOS / Windows
//
#include "tests.hpp"
#include "assertion.hpp"
+#include "speed_test.hpp"
#include
#include
@@ -20,14 +21,14 @@ using random_ushort_engine = std::independent_bits_engine<
void tests::run() {
registry.start();
- for (auto device : registry.get_devices()) {
+ for (auto& device : registry.get_devices()) {
std::cout << "Present: " << device->description() << std::endl;
}
registry.set_on_device_connected([this](auto device) { on_device_connected(device); });
registry.set_on_device_disconnected([this](auto device) { on_device_disconnected(device); });
- for (auto device : registry.get_devices())
+ for (auto& device : registry.get_devices())
on_device(device);
std::cout << "Press RETURN to quit" << std::endl;
@@ -36,53 +37,53 @@ void tests::run() {
}
void tests::test_current_device() {
- std::cout << "Found test device" << std::endl;
- test_device->open();
- test_device->claim_interface(0);
-
- test_control_transfers();
- test_bulk_transfers();
-
- test_device->release_interface();
- test_device->close();
- std::cout << "Test completed" << std::endl;
+ try {
+ std::cout << "Found test device" << std::endl;
+ test_device->open();
+ test_device->claim_interface(0);
+
+ test_control_transfers();
+ test_bulk_transfers();
+ test_speed();
+
+ test_device->release_interface(0);
+ test_device->close();
+ std::cout << "Test completed" << std::endl;
+ }
+ catch (const std::exception& e) {
+ std::cout << "Test failed: " << e.what() << std::endl;
+ }
}
void tests::test_control_transfers() {
- usb_control_request request_set_value_no_data = {
- .bmRequestType = usb_control_request::request_type(usb_request_type::direction_out,
- usb_request_type::type_vendor,
- usb_request_type::recipient_interface),
- .bRequest = 0x01,
- .wValue = 0x9a41,
- .wIndex = 0, // interface number
- .wLength = 0
- };
+ usb_control_request request_set_value_no_data = { 0 };
+ request_set_value_no_data.bmRequestType = usb_control_request::request_type(usb_request_type::direction_out,
+ usb_request_type::type_vendor, usb_request_type::recipient_interface);
+ request_set_value_no_data.bRequest = 0x01;
+ request_set_value_no_data.wValue = 0x9a41;
+ request_set_value_no_data.wIndex = 0; // interface number
+ request_set_value_no_data.wLength = 0;
test_device->control_transfer(request_set_value_no_data);
- usb_control_request request_get_data = {
- .bmRequestType = usb_control_request::request_type(usb_request_type::direction_in,
- usb_request_type::type_vendor,
- usb_request_type::recipient_interface),
- .bRequest = 0x03,
- .wValue = 0,
- .wIndex = 0, // interface number
- .wLength = 4
- };
+ usb_control_request request_get_data = { 0 };
+ request_get_data.bmRequestType = usb_control_request::request_type(usb_request_type::direction_in,
+ usb_request_type::type_vendor, usb_request_type::recipient_interface);
+ request_get_data.bRequest = 0x03;
+ request_get_data.wValue = 0;
+ request_get_data.wIndex = 0; // interface number
+ request_get_data.wLength = 4;
auto data = test_device->control_transfer_in(request_get_data);
std::vector expected_data{ 0x41, 0x9a, 0x00, 0x00 };
assert_equals(expected_data, data);
std::vector sent_value{ 0x83, 0x03, 0xda, 0x3d };
- usb_control_request request_set_value_data = {
- .bmRequestType = usb_control_request::request_type(usb_request_type::direction_out,
- usb_request_type::type_vendor,
- usb_request_type::recipient_interface),
- .bRequest = 0x02,
- .wValue = 0,
- .wIndex = 0, // interface number
- .wLength = static_cast(sent_value.size())
- };
+ usb_control_request request_set_value_data = { 0 };
+ request_set_value_data.bmRequestType = usb_control_request::request_type(usb_request_type::direction_out,
+ usb_request_type::type_vendor, usb_request_type::recipient_interface);
+ request_set_value_data.bRequest = 0x02;
+ request_set_value_data.wValue = 0;
+ request_set_value_data.wIndex = 0; // interface number
+ request_set_value_data.wLength = static_cast(sent_value.size());
test_device->control_transfer_out(request_set_value_data, sent_value);
data = test_device->control_transfer_in(request_get_data);
@@ -106,9 +107,9 @@ void tests::test_loopback(int num_bytes) {
// read in separate thread
std::thread reader([this, &rx_data, num_bytes]() {
- int bytes_read = 0;
+ size_t bytes_read = 0;
while (bytes_read < num_bytes) {
- auto data = test_device->transfer_in(2, 64);
+ auto data = test_device->transfer_in(2);
rx_data.insert(rx_data.end(), data.begin(), data.end());
bytes_read += data.size();
}
@@ -131,6 +132,13 @@ void tests::test_loopback(int num_bytes) {
assert_equals(random_data, rx_data);
}
+void tests::test_speed() {
+ int packet_size = test_device->get_endpoint(usb_direction::out, 1).packet_size();
+
+ speed_test test(test_device, 1, 2);
+ test.run(packet_size == 512 ? 20000000 : 2000000);
+}
+
void tests::on_device(usb_device_ptr device) {
if (is_test_device(device)) {
test_device = device;
@@ -156,7 +164,6 @@ std::vector tests::random_bytes(int num) {
return std::vector(p, p + num);
}
-
bool tests::is_test_device(usb_device_ptr device) {
return device->vendor_id() == 0xcafe && device->product_id() == 0xceaf;
}
diff --git a/reference/linux/tests.hpp b/reference/linux/tests.hpp
index 992de2b9..11cf328f 100644
--- a/reference/linux/tests.hpp
+++ b/reference/linux/tests.hpp
@@ -4,7 +4,7 @@
// Licensed under MIT License
// https://opensource.org/licenses/MIT
//
-// Reference C++ code for macOS
+// Reference C++ code common for Linux / macOS / Windows
//
#pragma once
@@ -20,6 +20,7 @@ class tests {
void test_current_device();
void test_control_transfers();
void test_bulk_transfers();
+ void test_speed();
void test_loopback(int num_bytes);
diff --git a/reference/linux/usb_device.cpp b/reference/linux/usb_device.cpp
index 711ad252..7d70c05d 100644
--- a/reference/linux/usb_device.cpp
+++ b/reference/linux/usb_device.cpp
@@ -4,12 +4,15 @@
// Licensed under MIT License
// https://opensource.org/licenses/MIT
//
-// Reference C++ code for macOS
+// Reference C++ code for Linux
//
#include "usb_device.hpp"
+#include "usb_iostream.hpp"
+#include "usb_registry.hpp"
#include "usb_error.hpp"
#include "scope.hpp"
+#include "config_parser.hpp"
#include
#include
@@ -17,9 +20,12 @@
#include
#include
+#include
-usb_device::usb_device(const char* path, int vendor_id, int product_id)
-: path_(path), fd_(-1), claimed_interface_(-1), vendor_id_(vendor_id), product_id_(product_id) {
+
+usb_device::usb_device(usb_registry* registry, const char* path, int vendor_id, int product_id)
+: registry_(registry), path_(path), fd_(-1), uses_urbs_(false), vendor_id_(vendor_id), product_id_(product_id) {
+ read_descriptor();
}
usb_device::~usb_device() {
@@ -27,6 +33,32 @@ usb_device::~usb_device() {
close();
}
+void usb_device::set_product_strings(const char* manufacturer, const char* product, const char* serial_number) {
+ manufacturer_ = manufacturer != nullptr ? manufacturer : "";
+ product_ = product != nullptr ? product : "";
+ serial_number_ = serial_number != nullptr ? serial_number : "";
+}
+
+void usb_device::read_descriptor() {
+ // read device and configuration descriptor
+ std::vector descriptors{};
+ {
+ std::ifstream file(path(), std::ios::binary);
+ uint8_t buf[256];
+ while (!file.eof()) {
+ file.read(reinterpret_cast(buf), sizeof(buf));
+ if (file.bad())
+ throw usb_error("failed to read device and configuration descriptor");
+ descriptors.insert(descriptors.end(), buf, buf + file.gcount());
+ }
+ }
+
+ int config_desc_offset = descriptors[0];
+ config_parser parser{};
+ parser.parse(descriptors.data() + config_desc_offset, descriptors.size() - config_desc_offset);
+ interfaces_ = std::move(parser.interfaces);
+}
+
std::string usb_device::description() const {
const char* fmt = "VID: 0x%04x, PID: 0x%04x, manufacturer: %s, product: %s, serial: %s";
@@ -41,6 +73,31 @@ std::string usb_device::description() const {
return desc;
}
+const std::vector