The root README still documented the abandoned approach: iperf3 3.21 binaries bundled in assets and run through a Process runner, and a lib/ layout that no longer matched. It now covers the JNI design, the current feature set and how to test against a local iperf3 server, and says plainly that the binaries still sitting in assets/iperf3/ are undeclared in pubspec.yaml and therefore never reach the APK. docs/README.md gains the threading, pipe-shutdown and stop-handling constraints that the fixes on this branch depend on, so the next change does not have to rediscover why the join order and the worker thread matter. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
5.5 KiB
iperf3-gui
A Flutter-based Android GUI wrapper for iperf3, the popular network performance measurement tool.
Architecture
lib/ # Flutter/Dart application layer
├── main.dart # App entry point (MaterialApp)
├── domain/ # Business logic (platform-independent)
│ ├── models/
│ │ ├── test_configuration.dart # Server, port, duration, interval, parallel streams
│ │ └── iperf3_result.dart # Raw output + exit code
├── data/
│ └── services/
│ └── iperf3_service.dart # Dart → MethodChannel bridge (async with Completer)
└── ui/ # Presentation layer
└── features/
└── test_run/
├── test_run_screen.dart # Server/port inputs + GO button
└── test_run_view_model.dart # State machine: idle → running → completed/error
android/app/src/main/
├── kotlin/ # Kotlin plugin layer
│ └── com/novoyuuparosk/iperf3gui/
│ ├── MainActivity.kt # Registers IperfRunner as FlutterPlugin
│ └── IperfRunner.kt # MethodChannel handler → native JNI calls
└── cpp/ # Native iperf3 library
├── CMakeLists.txt # Compiles iperf3 + cJSON + JNI into libiperf3jni.so
├── iperf_jni.c # JNI bridge: Java String[] → iperf3 API
├── iperf/ # iperf3 3.19 C source tree (see below)
├── iperf_config.h # Generated Android-compatible config header
└── iperf_config_android.h # Android-specific compile options
Data Flow
User taps GO
→ Dart TestRunViewModel.runTest(config)
→ Iperf3Service.run(config) via MethodChannel
→ Kotlin IperfRunner: submits to the iperf3-run worker thread, returns at once
→ JNI C: build_argv() (prepends argv[0]) → iperf_parse_arguments()
→ iperf_run_client(test) [blocks on the worker thread]
→ Reader thread pipes output → CallVoidMethod → main-thread hop → Dart onOutput
→ Dart streams each line to the UI via Iperf3Service.outputLines
→ On completion: Dart Completer resolves with Iperf3Result
Key Design Decisions
JNI Over Process Execution
Android SELinux Enforcing mode blocks execve() from the untrusted_app domain, making Process.start() of external binaries fail with Permission denied. Compiling iperf3 as a shared library linked into the app process eliminates this restriction entirely — iperf3 code runs in-process with no domain transitions needed.
Threading
runIperfLive() blocks for the whole test, so Kotlin submits it to a dedicated
single-thread executor and hands control straight back to Dart. Running it on the
Flutter platform thread froze the UI and — because output is delivered by posting
to the main looper — stalled every callback until the test was already over.
Callback Mechanism
The JNI bridge uses CallVoidMethod on stored jmethodIDs to invoke the Kotlin IperfCallback interface, NOT function pointer casts. Direct function pointer casts caused SIGBUS crashes on ARM due to JNINativeInterface method ID layout differences.
Output Streaming
iperf3 output is redirected via pipe to a background reader thread that attaches
to the Java VM via AttachCurrentThread and invokes callbacks via
CallVoidMethod. Dart re-publishes each line on Iperf3Service.outputLines, so
the transcript grows while the test runs.
The reader blocks in fgets() and only unblocks on EOF, so the pipe's write end
must be closed before pthread_join() — joining first deadlocks the worker
thread. The JNI caller solely owns the run-state struct and frees it after the
join; the reader must never free it.
Stop Handling
A stop shuts down the control socket, so iperf_run_client() returns -1 with
"Broken pipe". That is the expected outcome of a stop, not a failure, so the
bridge checks its own g_stop flag and reports a stop rather than an error; the
flag travels up to the UI as Iperf3Result.stopped.
Async Pattern
Dart uses Completer<Iperf3Result> to await native completion. The MethodChannel callback handler resolves the completer on onComplete, with a timeout fallback that calls stopIperf on the native layer.
iperf3 Source
- Version: 3.19
- Location:
android/app/src/main/cpp/iperf/ - Files: Full C source tree including cJSON, headers, man pages, PEM keys
- Build: CMake compiles into
libiperf3jni.soforarmeabi-v7aandarm64-v8a - Config:
iperf_config.hgenerated fromiperf_config.h.inwith Android-compatible POSIX/threading macros
Native JNI API
| Kotlin Method | JNI Signature | Purpose |
|---|---|---|
runIperfLive(String[], IperfCallback) |
runIperfLive |
Run a test to completion — blocks, call off the platform thread |
forceStopIperfTest(IperfCallback) |
forceStopIperfTest |
Graceful stop via shutdown() |
isTestRunning() |
isTestRunning |
Check if test is in progress |
setTempDir(String) |
setTempDir |
Point mkstemp() at the app cache dir |
Building
Prerequisites:
- Flutter 3.47+
- Android SDK with platform android-36
- JDK 17
- NDK r28+
- CMake 3.22.1
flutter pub get
flutter build apk --debug
adb install build/app/outputs/flutter-apk/app-debug.apk
Dependencies
- Dart:
provider - Android: CMake external native build
- Native: iperf3 3.19, cJSON (single file)
License
MIT — bundled iperf3 3.19 is BSD-3-Clause licensed.