From 95fdcbeefd33268007ef68313dce98af6c51857c Mon Sep 17 00:00:00 2001 From: chuan Date: Wed, 5 Aug 2026 22:09:58 +0800 Subject: [PATCH] Remove JNI support and related files - Deleted the JNI module and all associated files, including callbacks, env, exports, server, and types. - Updated Cargo.toml to remove the jni dependency and related features. - Removed JNI-related sections from README.md and CHANGELOG.md. - Adjusted the GitHub Actions workflow by removing the Rust CI configuration. --- .github/workflows/release.yml | 193 ---------------- .github/workflows/rust.yml | 92 -------- CHANGELOG.md | 1 - Cargo.toml | 4 +- README.md | 3 - jni/callbacks.rs | 92 -------- jni/env.rs | 58 ----- jni/exports.rs | 133 ----------- jni/mod.rs | 5 - jni/server.rs | 111 ---------- jni/types.rs | 403 ---------------------------------- src/lib.rs | 5 - 12 files changed, 1 insertion(+), 1099 deletions(-) delete mode 100644 .github/workflows/release.yml delete mode 100644 .github/workflows/rust.yml delete mode 100644 jni/callbacks.rs delete mode 100644 jni/env.rs delete mode 100644 jni/exports.rs delete mode 100644 jni/mod.rs delete mode 100644 jni/server.rs delete mode 100644 jni/types.rs diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml deleted file mode 100644 index b4189a5..0000000 --- a/.github/workflows/release.yml +++ /dev/null @@ -1,193 +0,0 @@ -name: Release - -on: - workflow_dispatch: # 只允许手动触发 - inputs: - version: - description: '版本号(例如:v1.0.0)' - required: true - default: 'v1.0.0' - -env: - CARGO_TERM_COLOR: always - -jobs: - # ────────────────────────────────────────────────────────────────── - # 各平台 Rust 构建(example 可执行 + JNI so/dll/dylib) - # ────────────────────────────────────────────────────────────────── - build-and-release: - name: Build and Release for ${{ matrix.target }} - runs-on: ${{ matrix.os }} - permissions: - contents: write - strategy: - fail-fast: false - matrix: - include: - # Linux x86_64 (GNU) - - target: x86_64-unknown-linux-gnu - os: ubuntu-latest - use_cross: false - platform: linux - jni_lib: libdht_crawler.so - build_features: mimalloc,metrics,jni - - # Linux ARM64 - - target: aarch64-unknown-linux-gnu - os: ubuntu-latest - use_cross: true - platform: linux - jni_lib: libdht_crawler.so - build_features: mimalloc,metrics,jni - - # Windows x86_64 (GNU/MinGW,在 Windows 上原生构建,无需交叉编译) - - target: x86_64-pc-windows-gnu - os: windows-latest - use_cross: false - platform: windows - jni_lib: dht_crawler.dll - build_features: mimalloc,metrics,jni - - # macOS x86_64 - - target: x86_64-apple-darwin - os: macos-latest - use_cross: false - platform: macos - jni_lib: libdht_crawler.dylib - build_features: mimalloc,metrics,jni - - # macOS ARM64 (Apple Silicon) - - target: aarch64-apple-darwin - os: macos-latest - use_cross: false - platform: macos - jni_lib: libdht_crawler.dylib - build_features: mimalloc,metrics,jni - - steps: - - name: Checkout code - uses: actions/checkout@v3 - - - name: Install Rust - uses: dtolnay/rust-toolchain@stable - with: - targets: ${{ matrix.target }} - - - name: Install cross - if: matrix.use_cross - run: cargo install cross --git https://github.com/cross-rs/cross - - - name: Setup MSYS2 MinGW (Windows GNU) - if: matrix.target == 'x86_64-pc-windows-gnu' - uses: msys2/setup-msys2@v2 - with: - update: true - install: mingw-w64-x86_64-gcc - - - name: Add MinGW to PATH (Windows GNU) - if: matrix.target == 'x86_64-pc-windows-gnu' - run: echo "C:\msys64\mingw64\bin" >> $env:GITHUB_PATH - shell: pwsh - - - name: Cache cargo registry - uses: actions/cache@v3 - with: - path: ~/.cargo/registry - key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }} - - - name: Cache cargo index - uses: actions/cache@v3 - with: - path: ~/.cargo/git - key: ${{ runner.os }}-cargo-index-${{ hashFiles('**/Cargo.lock') }} - - # release 构建不缓存 target 目录,避免占用 GitHub 缓存配额 - - name: Run tests - run: | - if [ "${{ matrix.use_cross }}" = "true" ]; then - cross test --target ${{ matrix.target }} --verbose - else - cargo test --target ${{ matrix.target }} --verbose - fi - shell: bash - - - name: Run tests with features - run: | - if [ "${{ matrix.use_cross }}" = "true" ]; then - cross test --target ${{ matrix.target }} --verbose --features ${{ matrix.build_features }} - else - cargo test --target ${{ matrix.target }} --verbose --features ${{ matrix.build_features }} - fi - shell: bash - - - name: Build (lib cdylib + examples) - run: | - if [ "${{ matrix.use_cross }}" = "true" ]; then - cross build --release --target ${{ matrix.target }} --lib --examples --features ${{ matrix.build_features }} - else - cargo build --release --target ${{ matrix.target }} --lib --examples --features ${{ matrix.build_features }} - fi - shell: bash - - - name: Get version - id: get_version - run: | - VERSION="${{ github.event.inputs.version }}" - echo "version=$VERSION" >> $GITHUB_OUTPUT - shell: bash - - # ────────── example 产物打包 ────────── - - - name: Prepare example artifacts (Linux/macOS) - if: "!contains(matrix.target, 'windows')" - run: | - cd target/${{ matrix.target }}/release/examples - tar czf dht_crawler_example-${{ steps.get_version.outputs.version }}-${{ matrix.target }}.tar.gz dht_crawler_example - mv dht_crawler_example-${{ steps.get_version.outputs.version }}-${{ matrix.target }}.tar.gz ${{ github.workspace }}/ - shell: bash - - - name: Prepare example artifacts (Windows GNU) - if: contains(matrix.target, 'windows') - run: | - cd target/${{ matrix.target }}/release/examples - Compress-Archive -Path dht_crawler_example.exe -DestinationPath dht_crawler_example-${{ steps.get_version.outputs.version }}-${{ matrix.target }}.zip - Move-Item -Path dht_crawler_example-${{ steps.get_version.outputs.version }}-${{ matrix.target }}.zip -Destination ${{ github.workspace }}/ - shell: pwsh - - # ────────── JNI 动态库打包(仅含单个 so/dll/dylib,体积最小)────────── - - - name: Prepare JNI artifacts (Linux/macOS) - if: "!contains(matrix.target, 'windows')" - run: | - VERSION=${{ steps.get_version.outputs.version }} - TARGET=${{ matrix.target }} - LIB=${{ matrix.jni_lib }} - WORKDIR=$(mktemp -d) - cp target/${TARGET}/release/${LIB} ${WORKDIR}/ - cd ${WORKDIR} - zip dht_crawler_jni-${VERSION}-${TARGET}.zip ${LIB} - mv dht_crawler_jni-${VERSION}-${TARGET}.zip ${{ github.workspace }}/ - shell: bash - - - name: Prepare JNI artifacts (Windows) - if: contains(matrix.target, 'windows') - run: | - $VERSION = "${{ steps.get_version.outputs.version }}" - $TARGET = "${{ matrix.target }}" - $LIB = "${{ matrix.jni_lib }}" - $WORKDIR = New-TemporaryFile | ForEach-Object { Remove-Item $_; New-Item -ItemType Directory -Path $_.FullName } - Copy-Item "target/$TARGET/release/$LIB" -Destination $WORKDIR.FullName - Set-Location $WORKDIR.FullName - Compress-Archive -Path $LIB -DestinationPath "dht_crawler_jni-$VERSION-$TARGET.zip" - Move-Item -Path "dht_crawler_jni-$VERSION-$TARGET.zip" -Destination "${{ github.workspace }}/" - shell: pwsh - - # ────────── 上传到 Release ────────── - - - name: Upload Release Assets - uses: softprops/action-gh-release@v1 - with: - tag_name: ${{ github.event.inputs.version }} - files: | - dht_crawler_example-${{ steps.get_version.outputs.version }}-${{ matrix.target }}.* - dht_crawler_jni-${{ steps.get_version.outputs.version }}-${{ matrix.target }}.zip diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml deleted file mode 100644 index 2eb7313..0000000 --- a/.github/workflows/rust.yml +++ /dev/null @@ -1,92 +0,0 @@ -name: Rust - -on: - push: - branches: [ "master" ] - pull_request: - branches: [ "master" ] - -env: - CARGO_TERM_COLOR: always - -jobs: - # 构建和测试 - test: - name: Test on ${{ matrix.os }} - runs-on: ${{ matrix.os }} - strategy: - fail-fast: false - matrix: - os: [ubuntu-latest, windows-latest, macos-latest] - steps: - - uses: actions/checkout@v4 - - name: Install Rust - uses: dtolnay/rust-toolchain@stable - - name: Cache cargo registry - uses: actions/cache@v3 - with: - path: | - ~/.cargo/registry - ~/.cargo/git - key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} - - name: Cache target directory - uses: actions/cache@v3 - with: - path: target - key: ${{ runner.os }}-cargo-target-${{ hashFiles('**/Cargo.lock') }} - - name: Build - run: cargo build --verbose - - name: Run tests - run: cargo test --verbose - - name: Build examples - run: cargo build --examples --verbose - - name: Build examples with mimalloc - run: cargo build --examples --verbose --features mimalloc - - name: Build examples with metrics - run: cargo build --examples --verbose --features metrics - - name: Build examples with all features - run: cargo build --examples --verbose --features mimalloc,metrics - - # 测试不同的 feature 组合 - test-features: - name: Test Features - runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - features: - - "" - - "mimalloc" - - "metrics" - - "mimalloc,metrics" - - "jni" - steps: - - uses: actions/checkout@v4 - - name: Install Rust - uses: dtolnay/rust-toolchain@stable - - name: Cache cargo registry - uses: actions/cache@v3 - with: - path: ~/.cargo/registry - key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }} - - name: Build with features - run: | - if [ -z "${{ matrix.features }}" ]; then - cargo build --verbose - else - cargo build --verbose --features "${{ matrix.features }}" - fi - - name: Test with features - run: | - if [ -z "${{ matrix.features }}" ]; then - cargo test --verbose - else - cargo test --verbose --features "${{ matrix.features }}" - fi - - name: Build examples with features - run: | - if [ -z "${{ matrix.features }}" ]; then - cargo build --examples --verbose - else - cargo build --examples --verbose --features "${{ matrix.features }}" - fi diff --git a/CHANGELOG.md b/CHANGELOG.md index 88895e0..1b8b69d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,4 +31,3 @@ - Metadata timeout 现在覆盖连接、握手、传输、SHA1 和解析的完整 Peer 尝试。 - UDP ingress、crawl events 和 Metadata queues 全部有界,并暴露 drop/depth 指标。 - DHT 回复增加总包、总字节、单来源限流,以及 `ping`/`get_peers` 10% 保底预算。 -- JNI `DHTOptions` 更新为 0.2 配置的扁平化子集;未映射字段继续采用 Rust 默认值。 diff --git a/Cargo.toml b/Cargo.toml index 50df462..b847f56 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,7 +14,7 @@ readme = "README.md" [lib] name = "dht_crawler" path = "src/lib.rs" -crate-type = ["rlib", "cdylib"] +crate-type = ["rlib"] [dependencies] tokio = { version = "1.35", features = ["rt", "rt-multi-thread", "net", "sync", "time", "macros"] } @@ -34,7 +34,6 @@ serde_bytes = "0.11.19" metrics = { version = "0.24", optional = true } async-channel = "2.5.0" crossbeam-queue = "0.3" -jni = { version = "0.21", optional = true } arc-swap = "1.7" [dev-dependencies] @@ -51,7 +50,6 @@ metrics-exporter-prometheus = { version = "0.18", default-features = false, feat default = [] metrics = ["dep:metrics"] mimalloc = [] -jni = ["dep:jni"] [[example]] name = "dht_crawler_example" diff --git a/README.md b/README.md index 774b7ff..4397b56 100644 --- a/README.md +++ b/README.md @@ -241,11 +241,8 @@ dht-crawler = { version = "0.2", features = ["metrics"] } | Feature | 用途 | |---|---| | `metrics` | 通过 `metrics` facade 记录指标 | -| `jni` | 构建 Java JNI 接口和 `cdylib` | | `mimalloc` | 将 mimalloc 注册为全局分配器 | -Java/Kotlin 封装、平台 native JAR 和示例由独立项目 -[`dht-crawler-java`](https://github.com/0xddy/dht-crawler-java) 维护。 启用 `mimalloc` 前,请确认最终二进制没有注册其他全局分配器。 ## 开发 diff --git a/jni/callbacks.rs b/jni/callbacks.rs deleted file mode 100644 index e0362f5..0000000 --- a/jni/callbacks.rs +++ /dev/null @@ -1,92 +0,0 @@ -use crate::DHTServer; -use crate::jni_bindings::env::JavaCallback; -use crate::jni_bindings::types::torrent_info_to_java; -use jni::objects::JValue; -use std::sync::Arc; - -/// 向 `DHTServer` 注册所有 Java 回调(on_torrent、on_error、on_metadata_fetch)。 -/// -/// `callback` 持有 Java listener 的全局引用,可跨线程安全使用。 -pub fn register_callbacks(server: &Arc, callback: JavaCallback) { - register_on_torrent(server, callback.clone()); - register_on_error(server, callback.clone()); - register_on_metadata_fetch(server, callback); -} - -/// 注册 on_torrent 回调:Rust TorrentInfo → Java listener.onTorrent(TorrentInfo) -fn register_on_torrent(server: &Arc, callback: JavaCallback) { - server.on_torrent(move |info| { - let result = callback.with_env(|env, listener| { - // 映射为 Java TorrentInfo 对象 - let j_info = torrent_info_to_java(env, &info).map_err(|e| { - log::error!("TorrentInfo 转换 Java 对象失败: {e}"); - e - })?; - env.call_method( - listener, - "onTorrent", - "(Lcn/lmcw/dht/model/TorrentInfo;)V", - &[JValue::Object(&j_info)], - )?; - Ok(()) - }); - if let Err(e) = result { - log::error!("回调 onTorrent 失败: {e}"); - } - }); -} - -/// 注册 on_error 回调:Rust DHTError → Java listener.onError(String) -fn register_on_error(server: &Arc, callback: JavaCallback) { - server.on_error(move |err| { - let msg = err.to_string(); - let result = callback.with_env(|env, listener| { - let j_msg = env.new_string(&msg)?; - env.call_method( - listener, - "onError", - "(Ljava/lang/String;)V", - &[JValue::Object(&j_msg.into())], - )?; - Ok(()) - }); - if let Err(e) = result { - log::error!("回调 onError 失败: {e}"); - } - }); -} - -/// 注册 on_metadata_fetch:在拉取 metadata 前询问 Java `onMetadataFetch(infoHash)`。 -/// 返回 `true` 才继续拉取;在阻塞线程池中执行 JNI,避免阻塞 tokio worker。 -/// JNI 调用或阻塞任务失败时按拒绝处理。 -fn register_on_metadata_fetch(server: &Arc, callback: JavaCallback) { - server.on_metadata_fetch(move |info_hash| { - let cb = callback.clone(); - async move { - let r = tokio::task::spawn_blocking(move || { - cb.with_env(|env, listener| { - let j_s = env.new_string(&info_hash)?; - let out = env.call_method( - listener, - "onMetadataFetch", - "(Ljava/lang/String;)Z", - &[JValue::Object(&j_s.into())], - )?; - out.z() - }) - }) - .await; - match r { - Ok(Ok(allow)) => allow, - Ok(Err(e)) => { - log::error!("回调 onMetadataFetch 失败: {e},拒绝拉取"); - false - } - Err(e) => { - log::error!("onMetadataFetch spawn_blocking 失败: {e},拒绝拉取"); - false - } - } - } - }); -} diff --git a/jni/env.rs b/jni/env.rs deleted file mode 100644 index 0426e2e..0000000 --- a/jni/env.rs +++ /dev/null @@ -1,58 +0,0 @@ -use jni::objects::JObject; -use jni::{JNIEnv, JavaVM}; -use std::sync::{Arc, Mutex}; - -/// 跨线程安全的 Java 回调持有者。 -/// 保存 JavaVM 指针和 Java 侧 listener 的全局引用, -/// 在任意 Rust 线程中都可 attach 并回调 Java 方法。 -#[derive(Clone)] -pub struct JavaCallback { - vm: Arc, - /// listener 对象的全局引用(不可跨线程直接用,需通过 vm 先 attach) - listener: Arc>, -} - -impl JavaCallback { - /// 从当前 JNI 调用线程创建,把 listener jobject 升级为全局引用。 - pub fn new(env: &mut JNIEnv, listener: &JObject) -> jni::errors::Result { - let vm = env.get_java_vm()?; - let global_ref = env.new_global_ref(listener)?; - Ok(Self { - vm: Arc::new(vm), - listener: Arc::new(Mutex::new(global_ref)), - }) - } - - /// 在任意线程中 attach JVM,执行 `f(env, listener)` 后自动 detach。 - /// `f` 内可调用 Java 方法;出错时 f 应检查 exception 并返回 Err。 - pub fn with_env(&self, f: F) -> jni::errors::Result - where - F: FnOnce(&mut JNIEnv, &JObject) -> jni::errors::Result, - { - let mut guard = self.vm.attach_current_thread()?; - let listener_guard = self.listener.lock().unwrap(); - let result = f(&mut guard, listener_guard.as_obj()); - // 检查并清除 Java 侧遗留的异常,避免后续 JNI 调用受污染 - if let Err(ref _e) = result - && guard.exception_check().unwrap_or(false) - { - let _ = guard.exception_clear(); - } - result - } -} - -/// 安全地执行 JNI 代码块,捕获 Rust panic,避免 JVM 崩溃。 -/// 若发生 panic,向 Java 抛出 RuntimeException 后返回 `default`。 -#[macro_export] -macro_rules! jni_catch { - ($env:expr, $default:expr, $body:expr) => {{ - match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| $body)) { - Ok(v) => v, - Err(_) => { - let _ = $env.throw_new("java/lang/RuntimeException", "Rust internal panic"); - $default - } - } - }}; -} diff --git a/jni/exports.rs b/jni/exports.rs deleted file mode 100644 index 2e9a933..0000000 --- a/jni/exports.rs +++ /dev/null @@ -1,133 +0,0 @@ -use crate::jni_bindings::callbacks::register_callbacks; -use crate::jni_bindings::env::JavaCallback; -use crate::jni_bindings::server::{ServerHandle, handle_ref, into_handle_ptr, take_handle}; -use crate::jni_bindings::types::java_to_dht_options_or_default; -use jni::JNIEnv; -use jni::objects::{JClass, JObject}; -use jni::sys::{jint, jlong}; - -// ────────────────────────────────────────────────────────────────────────────── -// cn.lmcw.dht.DhtCrawlerJni 的 JNI 导出 -// ────────────────────────────────────────────────────────────────────────────── - -/// 创建 DHTServer 并返回句柄(jlong)。 -/// -/// JVM 签名:`native long createServer(Object options, Object listener);` -/// -/// - `options`:包含 native 配置字段的 DTO,或 null 则使用默认选项。 -/// - `listener`:实现 `onTorrent`、`onError` 和 `onMetadataFetch` 的对象,或 null。 -/// - 返回:服务器句柄(成功)或 0(失败)。 -#[unsafe(no_mangle)] -pub extern "system" fn Java_cn_lmcw_dht_DhtCrawlerJni_createServer( - mut env: JNIEnv, - _class: JClass, - options: JObject, - listener: JObject, -) -> jlong { - crate::jni_catch!(&mut env, 0, { - // 解析选项 - let opts = match java_to_dht_options_or_default(&mut env, &options) { - Ok(o) => o, - Err(e) => { - let _ = env.throw_new("java/lang/IllegalArgumentException", e.to_string()); - return 0; - } - }; - - // 创建 ServerHandle(初始化 runtime + DHTServer) - let handle = match ServerHandle::new(opts) { - Ok(h) => h, - Err(e) => { - let _ = env.throw_new("java/lang/RuntimeException", &e); - return 0; - } - }; - - // 若提供了 listener,注册回调 - if !listener.is_null() { - match JavaCallback::new(&mut env, &listener) { - Ok(cb) => register_callbacks(&handle.server, cb), - Err(e) => { - let _ = env.throw_new("java/lang/RuntimeException", e.to_string()); - return 0; - } - } - } - - into_handle_ptr(handle) - }) -} - -/// 启动 DHTServer(在后台 tokio 任务中运行,不阻塞 JNI 线程)。 -/// -/// Java 签名:`native void startServer(long handle);` -#[unsafe(no_mangle)] -pub extern "system" fn Java_cn_lmcw_dht_DhtCrawlerJni_startServer( - mut env: JNIEnv, - _class: JClass, - handle: jlong, -) { - crate::jni_catch!(&mut env, (), { - let h = unsafe { - match handle_ref(handle) { - Some(h) => h, - None => { - let _ = env.throw_new("java/lang/IllegalArgumentException", "无效的服务器句柄"); - return; - } - } - }; - if let Err(e) = h.start() { - let _ = env.throw_new("java/lang/RuntimeException", &e); - } - }); -} - -/// 停止并销毁 DHTServer:发关闭信号后释放 tokio runtime 等全部 Rust 资源。 -/// 调用后句柄失效,勿再传入任何 native 方法。 -/// -/// Java 签名:`native void stopServer(long handle);` -#[unsafe(no_mangle)] -pub extern "system" fn Java_cn_lmcw_dht_DhtCrawlerJni_stopServer( - mut env: JNIEnv, - _class: JClass, - handle: jlong, -) { - jni_shutdown_and_release(&mut env, handle); -} - -fn jni_shutdown_and_release(env: &mut JNIEnv, handle: jlong) { - crate::jni_catch!(env, (), { - if handle == 0 { - return; - } - unsafe { - match take_handle(handle) { - Some(h) => h.shutdown_and_destroy_in_background(), - None => { - let _ = env.throw_new("java/lang/IllegalArgumentException", "无效的服务器句柄"); - } - } - } - }); -} - -/// 获取节点池当前大小。 -/// -/// Java 签名:`native int getNodePoolSize(long handle);` -#[unsafe(no_mangle)] -pub extern "system" fn Java_cn_lmcw_dht_DhtCrawlerJni_getNodePoolSize( - mut env: JNIEnv, - _class: JClass, - handle: jlong, -) -> jint { - crate::jni_catch!(&mut env, 0, { - let h = unsafe { - match handle_ref(handle) { - Some(h) => h, - None => return 0, - } - }; - h.node_pool_size() as jint - }) -} diff --git a/jni/mod.rs b/jni/mod.rs deleted file mode 100644 index a85f488..0000000 --- a/jni/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -mod callbacks; -mod env; -mod exports; -mod server; -mod types; diff --git a/jni/server.rs b/jni/server.rs deleted file mode 100644 index a618eb5..0000000 --- a/jni/server.rs +++ /dev/null @@ -1,111 +0,0 @@ -use crate::{DHTOptions, DHTServer}; -use std::sync::Arc; -use tokio::runtime::Runtime; - -/// JNI 侧持有的服务器句柄,包含 tokio runtime 和 DHTServer 实例。 -/// 通过 `Box::into_raw` 转成 `jlong` 句柄传给 Java, -/// 在 destroy 时通过 `Box::from_raw` 恢复并 drop。 -pub struct ServerHandle { - pub runtime: Runtime, - pub server: Arc, -} - -impl ServerHandle { - /// 在新建的 tokio runtime 里初始化 DHTServer。 - pub fn new(options: DHTOptions) -> Result { - let runtime = Runtime::new().map_err(|e| format!("无法创建 tokio runtime: {e}"))?; - let server = runtime - .block_on(DHTServer::new(options)) - .map_err(|e| format!("DHTServer 初始化失败: {e}"))?; - Ok(Self { - runtime, - server: Arc::new(server), - }) - } - - /// 在 runtime 里 spawn server.start(),不阻塞调用线程。 - pub fn start(&self) -> Result<(), String> { - let server: Arc = Arc::clone(&self.server); - self.runtime.spawn(async move { - if let Err(e) = server.start().await { - log::error!("DHT server 运行错误: {e}"); - } - }); - Ok(()) - } - - /// 发送关闭信号(非阻塞)。仅供单独使用,通常应调用 `shutdown_and_destroy_in_background`。 - #[allow(dead_code)] - pub fn stop(&self) { - self.server.shutdown(); - } - - /// 在专用后台线程中 drop 整个 handle(含 Runtime), - /// 避免 Runtime::drop 阻塞 JNI 调用线程。 - /// 先发关闭信号,然后把 handle 所有权移入后台线程; - /// 后台线程等待 tokio runtime 中所有任务退出后统一释放资源。 - pub fn shutdown_and_destroy_in_background(self) { - self.server.shutdown(); - if let Err(e) = std::thread::Builder::new() - .name("dht-jni-shutdown".to_owned()) - .spawn(move || { - // drop(self) 在此发生:Runtime::drop 阻塞等待所有 tokio 任务退出 - // 但此时已在独立线程,不会卡 JNI 调用线程 - drop(self); - }) - { - // 线程创建失败(极罕见),fallback 在当前线程同步释放,保底不泄漏 - log::error!("dht-jni-shutdown 线程创建失败,在当前线程同步释放: {e}"); - } - } - - /// 返回节点池大小。 - pub fn node_pool_size(&self) -> usize { - self.server.get_node_pool_size() - } -} - -// ────────────────────────────────────────────────────────────────────────────── -// 句柄指针工具 -// ────────────────────────────────────────────────────────────────────────────── - -/// 将 `ServerHandle` 装箱并返回原始指针,供 Java 以 `long` 持有。 -pub fn into_handle_ptr(handle: ServerHandle) -> i64 { - Box::into_raw(Box::new(handle)) as i64 -} - -/// 从 Java 传入的 `long` 句柄获取不可变引用。 -/// -/// # Safety -/// 调用方必须确保句柄未被 destroy,且在单次 JNI 调用生命周期内使用。 -pub unsafe fn handle_ref<'a>(ptr: i64) -> Option<&'a ServerHandle> { - if ptr == 0 { - return None; - } - Some(unsafe { &*(ptr as *const ServerHandle) }) -} - -/// 消费句柄:从裸指针重建 Box 并返回 `ServerHandle` 所有权。 -/// 调用后 Java 侧不得再使用该句柄。 -/// -/// # Safety -/// 只能调用一次;ptr 必须是由 `into_handle_ptr` 生成的合法指针。 -pub unsafe fn take_handle(ptr: i64) -> Option { - if ptr == 0 { - return None; - } - Some(*unsafe { Box::from_raw(ptr as *mut ServerHandle) }) -} - -/// 消费句柄:从裸指针重建 Box 并 drop,释放所有资源(包括 runtime)。 -/// 注意:会阻塞当前线程直到 Runtime 中所有任务退出。 -/// 通常应优先使用 `take_handle` + `shutdown_and_destroy_in_background`。 -/// -/// # Safety -/// 只能调用一次,调用后 Java 侧不得再使用该句柄。 -#[allow(dead_code)] -pub unsafe fn destroy_handle(ptr: i64) { - if ptr != 0 { - drop(unsafe { Box::from_raw(ptr as *mut ServerHandle) }); - } -} diff --git a/jni/types.rs b/jni/types.rs deleted file mode 100644 index d0154ba..0000000 --- a/jni/types.rs +++ /dev/null @@ -1,403 +0,0 @@ -use crate::{DHTOptions, FileInfo, MetadataOptions, TorrentInfo, types::NetMode}; -use jni::JNIEnv; -use jni::objects::{JObject, JString, JValue}; -use jni::sys::jlong; - -#[derive(Debug, thiserror::Error)] -pub enum DhtOptionsConversionError { - #[error(transparent)] - Jni(#[from] jni::errors::Error), - #[error("invalid DHTOptions.{field}: expected {expected}, got {value}")] - InvalidValue { - field: &'static str, - expected: &'static str, - value: i64, - }, -} - -type DhtOptionsResult = Result; - -fn invalid_value( - field: &'static str, - expected: &'static str, - value: impl Into, -) -> DhtOptionsConversionError { - DhtOptionsConversionError::InvalidValue { - field, - expected, - value: value.into(), - } -} - -fn checked_port(value: i32) -> DhtOptionsResult { - u16::try_from(value).map_err(|_| invalid_value("port", "an integer in 0..=65535", value)) -} - -fn checked_non_negative_u32(field: &'static str, value: i32) -> DhtOptionsResult { - u32::try_from(value).map_err(|_| invalid_value(field, "a non-negative integer", value)) -} - -fn checked_non_negative_u64(field: &'static str, value: i64) -> DhtOptionsResult { - u64::try_from(value).map_err(|_| invalid_value(field, "a non-negative integer", value)) -} - -fn checked_non_negative_usize(field: &'static str, value: i32) -> DhtOptionsResult { - usize::try_from(value).map_err(|_| invalid_value(field, "a non-negative integer", value)) -} - -fn checked_positive_usize(field: &'static str, value: i32) -> DhtOptionsResult { - let value = checked_non_negative_usize(field, value)?; - if value == 0 { - return Err(invalid_value( - field, - "an integer greater than or equal to 1", - 0, - )); - } - Ok(value) -} - -fn checked_percentage(field: &'static str, value: i32) -> DhtOptionsResult { - if !(0..=100).contains(&value) { - return Err(invalid_value(field, "an integer in 0..=100", value)); - } - Ok(u8::try_from(value).expect("0..=100 always fits in u8")) -} - -fn checked_netmode(value: i32) -> DhtOptionsResult { - match value { - 0 => Ok(NetMode::Ipv4Only), - 1 => Ok(NetMode::Ipv6Only), - 2 => Ok(NetMode::DualStack), - _ => Err(invalid_value("netMode", "one of 0, 1, or 2", value)), - } -} - -// ────────────────────────────────────────────────────────────────────────────── -// 常量:Java 类全限定名 -// ────────────────────────────────────────────────────────────────────────────── -const CLASS_TORRENT_INFO: &str = "cn/lmcw/dht/model/TorrentInfo"; -const CLASS_FILE_INFO: &str = "cn/lmcw/dht/model/FileInfo"; -const CLASS_ARRAY_LIST: &str = "java/util/ArrayList"; - -// ────────────────────────────────────────────────────────────────────────────── -// 基础类型转换 -// ────────────────────────────────────────────────────────────────────────────── - -/// Rust String → Java String(jstring) -pub fn rust_str_to_jstring<'local>( - env: &mut JNIEnv<'local>, - s: &str, -) -> jni::errors::Result> { - let js: JString<'local> = env.new_string(s)?; - Ok(js.into()) -} - -// ────────────────────────────────────────────────────────────────────────────── -// FileInfo: Rust → Java -// ────────────────────────────────────────────────────────────────────────────── - -/// 将 Rust `FileInfo` 构造为 Java `cn.lmcw.dht.model.FileInfo` 对象。 -pub fn file_info_to_java<'local>( - env: &mut JNIEnv<'local>, - fi: &FileInfo, -) -> jni::errors::Result> { - env.with_local_frame_returning_local(4, |env| { - let cls = env.find_class(CLASS_FILE_INFO)?; - let path = rust_str_to_jstring(env, &fi.path)?; - env.new_object( - &cls, - "(Ljava/lang/String;J)V", - &[JValue::Object(&path), JValue::Long(fi.size as jlong)], - ) - }) -} - -// ────────────────────────────────────────────────────────────────────────────── -// TorrentInfo: Rust → Java -// ────────────────────────────────────────────────────────────────────────────── - -/// 将 Rust `TorrentInfo` 构造为 Java `cn.lmcw.dht.model.TorrentInfo` 对象。 -/// files 字段被构造为 `java.util.ArrayList`。 -pub fn torrent_info_to_java<'local>( - env: &mut JNIEnv<'local>, - ti: &TorrentInfo, -) -> jni::errors::Result> { - let cls = env.find_class(CLASS_TORRENT_INFO)?; - - // 构造 files list - let list = build_file_list(env, &ti.files)?; - - // 构造 peers list(List) - let peers_list = build_string_list(env, &ti.peers)?; - - let info_hash = rust_str_to_jstring(env, &ti.info_hash)?; - let magnet = rust_str_to_jstring(env, &ti.magnet_link)?; - let name = rust_str_to_jstring(env, &ti.name)?; - - let obj = env.new_object( - &cls, - "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;JLjava/util/List;JLjava/util/List;J)V", - &[ - JValue::Object(&info_hash), - JValue::Object(&magnet), - JValue::Object(&name), - JValue::Long(ti.total_size as jlong), - JValue::Object(&list), - JValue::Long(ti.piece_length as jlong), - JValue::Object(&peers_list), - JValue::Long(ti.timestamp as jlong), - ], - )?; - Ok(obj) -} - -/// 构造 `java.util.ArrayList` 并填入 FileInfo 对象列表。 -fn build_file_list<'local>( - env: &mut JNIEnv<'local>, - files: &[FileInfo], -) -> jni::errors::Result> { - let list_cls = env.find_class(CLASS_ARRAY_LIST)?; - let list = env.new_object(&list_cls, "()V", &[])?; - for fi in files { - let jfi = file_info_to_java(env, fi)?; - env.call_method( - &list, - "add", - "(Ljava/lang/Object;)Z", - &[JValue::Object(&jfi)], - )?; - env.delete_local_ref(jfi)?; - } - Ok(list) -} - -/// 构造 `java.util.ArrayList` 并填入字符串列表。 -fn build_string_list<'local>( - env: &mut JNIEnv<'local>, - strs: &[String], -) -> jni::errors::Result> { - let list_cls = env.find_class(CLASS_ARRAY_LIST)?; - let list = env.new_object(&list_cls, "()V", &[])?; - for s in strs { - let js = rust_str_to_jstring(env, s)?; - env.call_method( - &list, - "add", - "(Ljava/lang/Object;)Z", - &[JValue::Object(&js)], - )?; - env.delete_local_ref(js)?; - } - Ok(list) -} - -// ────────────────────────────────────────────────────────────────────────────── -// DHTOptions: Java → Rust -// ────────────────────────────────────────────────────────────────────────────── - -/// 从 JVM bindings 的 options DTO 读取字段,构造 Rust `DHTOptions`。 -pub fn java_to_dht_options(env: &mut JNIEnv, obj: &JObject) -> DhtOptionsResult { - let port = checked_port(env.get_field(obj, "port", "I")?.i()?)?; - let metadata_timeout_secs = checked_non_negative_u64( - "metadataTimeout", - env.get_field(obj, "metadataTimeout", "J")?.j()?, - )?; - let metadata_max_queue_size = checked_positive_usize( - "maxMetadataQueueSize", - env.get_field(obj, "maxMetadataQueueSize", "I")?.i()?, - )?; - let metadata_max_worker_count = checked_positive_usize( - "maxMetadataWorkerCount", - env.get_field(obj, "maxMetadataWorkerCount", "I")?.i()?, - )?; - let pool_capacity = checked_positive_usize( - "poolCapacity", - env.get_field(obj, "poolCapacity", "I")?.i()?, - )?; - let find_node_rate = checked_non_negative_u32( - "findNodeRatePerSecond", - env.get_field(obj, "findNodeRatePerSecond", "I")?.i()?, - )?; - let find_node_burst = checked_non_negative_u32( - "findNodeBurst", - env.get_field(obj, "findNodeBurst", "I")?.i()?, - )?; - let max_find_node_in_flight = checked_positive_usize( - "maxFindNodeInFlight", - env.get_field(obj, "maxFindNodeInFlight", "I")?.i()?, - )?; - let max_new_destinations = checked_non_negative_u32( - "maxNewDestinationsPerMinute", - env.get_field(obj, "maxNewDestinationsPerMinute", "I")? - .i()?, - )?; - let max_replacements = checked_non_negative_u32( - "maxReplacementsPerMinute", - env.get_field(obj, "maxReplacementsPerMinute", "I")?.i()?, - )?; - let request_timeout_secs = checked_non_negative_u64( - "requestTimeoutSeconds", - env.get_field(obj, "requestTimeoutSeconds", "J")?.j()?, - )?; - let max_response_rate = checked_non_negative_u32( - "maxResponseRatePerSecond", - env.get_field(obj, "maxResponseRatePerSecond", "I")?.i()?, - )?; - let max_response_bytes = checked_non_negative_u64( - "maxResponseBytesPerSecond", - env.get_field(obj, "maxResponseBytesPerSecond", "J")?.j()?, - )?; - let max_response_per_source = checked_non_negative_u32( - "maxResponseRatePerSource", - env.get_field(obj, "maxResponseRatePerSource", "I")?.i()?, - )?; - let pressure_floor = checked_percentage( - "metadataPressureFloorPercent", - env.get_field(obj, "metadataPressureFloorPercent", "I")? - .i()?, - )?; - let recent_probe_ttl = checked_non_negative_u64( - "recentProbeTtlSeconds", - env.get_field(obj, "recentProbeTtlSeconds", "J")?.j()?, - )?; - let responsive_capacity = checked_positive_usize( - "responsiveCapacity", - env.get_field(obj, "responsiveCapacity", "I")?.i()?, - )?; - let responsive_ttl = checked_non_negative_u64( - "responsiveTtlSeconds", - env.get_field(obj, "responsiveTtlSeconds", "J")?.j()?, - )?; - let low_watermark = checked_non_negative_usize( - "poolLowWatermark", - env.get_field(obj, "poolLowWatermark", "I")?.i()?, - )?; - if low_watermark > pool_capacity { - return Err(invalid_value( - "poolLowWatermark", - "an integer no greater than poolCapacity", - i64::try_from(low_watermark).expect("Java int always fits in i64"), - )); - } - let subnet_in_flight = checked_positive_usize( - "maxInFlightPerSubnet", - env.get_field(obj, "maxInFlightPerSubnet", "I")?.i()?, - )?; - let hash_queue_capacity = checked_positive_usize( - "hashQueueCapacity", - env.get_field(obj, "hashQueueCapacity", "I")?.i()?, - )?; - let netmode = checked_netmode(env.get_field(obj, "netMode", "I")?.i()?)?; - - let mut options = DHTOptions { - port, - netmode, - hash_queue_capacity, - metadata: MetadataOptions { - timeout_secs: metadata_timeout_secs, - max_queue_size: metadata_max_queue_size, - max_worker_count: metadata_max_worker_count, - ..MetadataOptions::default() - }, - ..DHTOptions::default() - }; - options.crawl.pool.capacity = pool_capacity; - options.crawl.pool.recent_probe_ttl_secs = recent_probe_ttl; - options.crawl.pool.responsive_capacity = responsive_capacity; - options.crawl.pool.responsive_ttl_secs = responsive_ttl; - options.crawl.pool.low_watermark = low_watermark; - options.crawl.rate_limit.max_find_node_rate_per_sec = find_node_rate; - options.crawl.rate_limit.burst = find_node_burst; - options.crawl.rate_limit.max_in_flight = max_find_node_in_flight; - options.crawl.rate_limit.max_new_destinations_per_minute = max_new_destinations; - options.crawl.rate_limit.request_timeout_secs = request_timeout_secs; - options.crawl.rate_limit.max_response_rate_per_sec = max_response_rate; - options.crawl.rate_limit.max_response_bytes_per_sec = max_response_bytes; - options.crawl.rate_limit.max_response_rate_per_source = max_response_per_source; - options.crawl.rate_limit.metadata_pressure_floor_percent = pressure_floor; - options.crawl.rate_limit.max_replacements_per_minute = max_replacements; - options.crawl.rate_limit.max_in_flight_per_subnet = subnet_in_flight; - Ok(options) -} - -/// 从 JVM bindings 的 options DTO 读取,或若为 null 则返回默认选项。 -pub fn java_to_dht_options_or_default( - env: &mut JNIEnv, - obj: &JObject, -) -> DhtOptionsResult { - if obj.is_null() { - Ok(DHTOptions::default()) - } else { - java_to_dht_options(env, obj) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - fn assert_invalid(result: DhtOptionsResult, field: &'static str, value: i64) { - match result { - Err(DhtOptionsConversionError::InvalidValue { - field: actual_field, - value: actual_value, - .. - }) => { - assert_eq!(actual_field, field); - assert_eq!(actual_value, value); - } - _ => panic!("expected an invalid-value error"), - } - } - - #[test] - fn port_accepts_java_boundaries_and_rejects_out_of_range_values() { - assert_eq!(checked_port(0).unwrap(), 0); - assert_eq!(checked_port(i32::from(u16::MAX)).unwrap(), u16::MAX); - assert_invalid(checked_port(-1), "port", -1); - assert_invalid(checked_port(i32::from(u16::MAX) + 1), "port", 65_536); - } - - #[test] - fn signed_values_are_checked_before_unsigned_conversion() { - assert_eq!(checked_non_negative_u32("rate", 0).unwrap(), 0); - assert_eq!( - checked_non_negative_u32("rate", i32::MAX).unwrap(), - i32::MAX as u32 - ); - assert_invalid(checked_non_negative_u32("rate", -1), "rate", -1); - - assert_eq!(checked_non_negative_u64("timeout", 0).unwrap(), 0); - assert_eq!( - checked_non_negative_u64("timeout", i64::MAX).unwrap(), - i64::MAX as u64 - ); - assert_invalid(checked_non_negative_u64("timeout", -1), "timeout", -1); - } - - #[test] - fn capacities_and_in_flight_limits_must_be_positive() { - assert_eq!(checked_positive_usize("capacity", 1).unwrap(), 1); - assert_eq!( - checked_positive_usize("capacity", i32::MAX).unwrap(), - i32::MAX as usize - ); - assert_invalid(checked_positive_usize("capacity", 0), "capacity", 0); - assert_invalid(checked_positive_usize("capacity", -1), "capacity", -1); - } - - #[test] - fn percentage_and_netmode_only_accept_documented_values() { - assert_eq!(checked_percentage("percent", 0).unwrap(), 0); - assert_eq!(checked_percentage("percent", 100).unwrap(), 100); - assert_invalid(checked_percentage("percent", -1), "percent", -1); - assert_invalid(checked_percentage("percent", 101), "percent", 101); - - assert_eq!(checked_netmode(0).unwrap(), NetMode::Ipv4Only); - assert_eq!(checked_netmode(1).unwrap(), NetMode::Ipv6Only); - assert_eq!(checked_netmode(2).unwrap(), NetMode::DualStack); - assert_invalid(checked_netmode(-1), "netMode", -1); - assert_invalid(checked_netmode(3), "netMode", 3); - } -} diff --git a/src/lib.rs b/src/lib.rs index 0c46f6c..7399e83 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -55,8 +55,3 @@ pub mod prelude { PoolOptions, RateLimitOptions, SchedulerOptions, TargetOptions, TorrentInfo, }; } - -#[cfg(feature = "jni")] -#[path = "../jni/mod.rs"] -/// JNI entry points consumed by the external JVM bindings. -pub mod jni_bindings;