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.
This commit is contained in:
@@ -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
|
||||
@@ -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
|
||||
@@ -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 默认值。
|
||||
|
||||
+1
-3
@@ -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"
|
||||
|
||||
@@ -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` 前,请确认最终二进制没有注册其他全局分配器。
|
||||
|
||||
## 开发
|
||||
|
||||
@@ -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<DHTServer>, 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<DHTServer>, 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<DHTServer>, 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<DHTServer>, 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
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
-58
@@ -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<JavaVM>,
|
||||
/// listener 对象的全局引用(不可跨线程直接用,需通过 vm 先 attach)
|
||||
listener: Arc<Mutex<jni::objects::GlobalRef>>,
|
||||
}
|
||||
|
||||
impl JavaCallback {
|
||||
/// 从当前 JNI 调用线程创建,把 listener jobject 升级为全局引用。
|
||||
pub fn new(env: &mut JNIEnv, listener: &JObject) -> jni::errors::Result<Self> {
|
||||
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<F, R>(&self, f: F) -> jni::errors::Result<R>
|
||||
where
|
||||
F: FnOnce(&mut JNIEnv, &JObject) -> jni::errors::Result<R>,
|
||||
{
|
||||
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
|
||||
}
|
||||
}
|
||||
}};
|
||||
}
|
||||
-133
@@ -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
|
||||
})
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
mod callbacks;
|
||||
mod env;
|
||||
mod exports;
|
||||
mod server;
|
||||
mod types;
|
||||
-111
@@ -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<DHTServer>,
|
||||
}
|
||||
|
||||
impl ServerHandle {
|
||||
/// 在新建的 tokio runtime 里初始化 DHTServer。
|
||||
pub fn new(options: DHTOptions) -> Result<Self, String> {
|
||||
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<DHTServer> = 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<ServerHandle> {
|
||||
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) });
|
||||
}
|
||||
}
|
||||
-403
@@ -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<T> = Result<T, DhtOptionsConversionError>;
|
||||
|
||||
fn invalid_value(
|
||||
field: &'static str,
|
||||
expected: &'static str,
|
||||
value: impl Into<i64>,
|
||||
) -> DhtOptionsConversionError {
|
||||
DhtOptionsConversionError::InvalidValue {
|
||||
field,
|
||||
expected,
|
||||
value: value.into(),
|
||||
}
|
||||
}
|
||||
|
||||
fn checked_port(value: i32) -> DhtOptionsResult<u16> {
|
||||
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> {
|
||||
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> {
|
||||
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> {
|
||||
usize::try_from(value).map_err(|_| invalid_value(field, "a non-negative integer", value))
|
||||
}
|
||||
|
||||
fn checked_positive_usize(field: &'static str, value: i32) -> DhtOptionsResult<usize> {
|
||||
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<u8> {
|
||||
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<NetMode> {
|
||||
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<JObject<'local>> {
|
||||
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<JObject<'local>> {
|
||||
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<FileInfo>`。
|
||||
pub fn torrent_info_to_java<'local>(
|
||||
env: &mut JNIEnv<'local>,
|
||||
ti: &TorrentInfo,
|
||||
) -> jni::errors::Result<JObject<'local>> {
|
||||
let cls = env.find_class(CLASS_TORRENT_INFO)?;
|
||||
|
||||
// 构造 files list
|
||||
let list = build_file_list(env, &ti.files)?;
|
||||
|
||||
// 构造 peers list(List<String>)
|
||||
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<JObject<'local>> {
|
||||
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<JObject<'local>> {
|
||||
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<DHTOptions> {
|
||||
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<DHTOptions> {
|
||||
if obj.is_null() {
|
||||
Ok(DHTOptions::default())
|
||||
} else {
|
||||
java_to_dht_options(env, obj)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn assert_invalid<T>(result: DhtOptionsResult<T>, 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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user