Files
iperf3-gui/docs
mikkeli 0dc747edef docs: describe the JNI architecture the app actually uses
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>
2026-08-26 19:03:59 +09:00
..

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.so for armeabi-v7a and arm64-v8a
  • Config: iperf_config.h generated from iperf_config.h.in with 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.