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:
chuan
2026-08-05 22:09:58 +08:00
parent 571472d16f
commit 95fdcbeefd
12 changed files with 1 additions and 1099 deletions
-193
View File
@@ -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
-92
View File
@@ -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
-1
View File
@@ -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
View File
@@ -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"
-3
View File
@@ -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` 前,请确认最终二进制没有注册其他全局分配器。
## 开发
-92
View File
@@ -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
View File
@@ -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
View File
@@ -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
})
}
-5
View File
@@ -1,5 +0,0 @@
mod callbacks;
mod env;
mod exports;
mod server;
mod types;
-111
View File
@@ -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
View File
@@ -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 Stringjstring
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 listList<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);
}
}
-5
View File
@@ -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;