Rust写Android原生代理:拆解tg-ws-proxy-android的JNI FFI接口与tokio运行时设计
【免费下载链接】tg-ws-proxy-androidAndroid-форк популярного приложения Flowseal - tg-ws-proxy - локальный прокси-сервер MTProto с проксированием CF или без для частичного обхода проблем загрузки Telegram项目地址: https://gitcode.com/gh_mirrors/tg/tg-ws-proxy-android
tg-ws-proxy-android是一个把Rust 原生代理内核打进 APK 的 Android 应用:Kotlin 界面负责交互,真正的 MTProto 代理、WebSocket 连接池、TLS 握手全部由 Rust 编写的libtgwsproxy.so完成。这篇文章带你完整拆解它的JNI FFI 接口层与tokio 多线程运行时是如何协作的,帮助想学习 Rust 移动原生开发的人快速理解这条技术路线。
1. 为什么用 Rust 写 Android 代理内核
用原生语言写网络代理内核,收益非常直接:
- 无 GC 停顿:代理要转发大量小包,Rust 没有垃圾回收,延迟稳定
- 零成本抽象:
async/await编译为状态机,没有虚拟机的额外开销 - 产物极小:项目用 Cargo.toml 中
opt-level = "z"+LTO+panic = "abort"+strip的发布配置,把体积压到极致,最终就是 app/src/main/jniLibs/arm64-v8a/libtgwsproxy.so 这一个文件 - 内存安全:FFI 边界是 C 程序最容易崩溃的地方,Rust 的类型系统在库内部把大多数坑堵死
整体数据流如下:
Telegram 客户端 → 本地 MTProto (127.0.0.1:1443) → libtgwsproxy.so (Rust + tokio 内核) → WSS (经 CloudFlare 或直连) → Telegram 数据中心2. 接口层:JNA 如何跨语言调用 Rust
传统 JNI 需要手写jclass、jstring样板代码,而本项目选择了更轻的JNA(Java Native Access)路线:Kotlin 侧只需声明一个接口,JNA 自动完成函数签名匹配与参数编组。
Kotlin 侧的全部"JNI 代码"就在 NativeProxy.kt:
interface ProxyLibrary : Library { companion object { val INSTANCE = Native.load("tgwsproxy", ProxyLibrary::class.java) as ProxyLibrary } fun StartProxy(host: String, port: Int, dcIps: String, secret: String, verbose: Int): Int fun StopProxy(): Int fun SetPoolSize(size: Int) fun GetStats(): Pointer? fun FreeString(p: Pointer) // ... }Native.load("tgwsproxy", ...)会自动找到 jniLibs 下的libtgwsproxy.so并加载,之后每个方法调用都是一次普通的 C ABI 函数调用——接口名必须和 Rust 导出的符号完全一致。
2.1 Rust 侧的导出约定
Rust 侧通过#[no_mangle]+extern "C"导出符号,核心接口集中在 src/lib.rs:
| 接口 | 类型 | 作用 |
|---|---|---|
StartProxy(host, port, dcIps, secret, verbose) | 启动 | 绑定本地端口并启动代理主循环,返回 0 表示成功 |
StopProxy() | 停止 | 取消全部任务并等待优雅退出 |
SetPoolSize(n)/SetBufferSizeKb(kb) | 调参 | 运行中热更新连接池大小、收发缓冲区 |
SetCfProxyConfig(...)/SetFakeTlsDomain(...) | 调参 | 配置 CloudFlare 转发域与 FakeTLS 伪装域 |
GetStats()/GetSecretWithPrefix() | 查询 | 返回统计摘要与带前缀的代理密钥 |
FreeString(p) | 内存 | 释放 Rust 分配的 C 字符串 |
跨语言传字符串统一用 C 字符串:Rust 侧的cstr_to_string把*const c_char安全转成String,空指针会被容忍为空串(见 src/lib.rs)。
2.2 一块值得学习的内存所有权设计
注意GetStats()的返回值——它不是拷贝,而是把 Rust 的CString转成裸指针交给 Kotlin:
- Rust 分配:
CString::new(s).into_raw()(src/lib.rs) - Kotlin 读取:
ptr.getString(0)后必须回调FreeString(p)(NativeProxy.kt)
谁分配谁释放,FreeString内部用CString::from_raw把所有权接回来再析构。这是 FFI 库最经典的"跨语言所有权协议",也是这类项目里最容易踩内存泄漏/双重释放坑的地方,本项目的做法非常干净。
3. tokio 运行时设计:一个"永不销毁"的全局事件循环
整个代理内核跑在一个全局静态的 tokio 多核运行时上,构建逻辑在 src/lib.rs:
static RUNTIME: OnceCell<Runtime> = OnceCell::new(); fn runtime() -> &'static Runtime { RUNTIME.get_or_init(|| { tokio::runtime::Builder::new_multi_thread() .worker_threads(4) // 手机够用即可 .thread_name("tgwsproxy-rt") .enable_all() .build() .expect("failed to build global tokio runtime") }) }三个关键设计决策:
①OnceCell保证单例,且永不 drop。运行时只创建一次,StopProxy时只取消任务、不销毁运行时——这样用户可以"停止再启动",避免反复构建/销毁多线程运行时的开销与竞态。
② 固定 4 个 worker 线程。手机 CPU 大小核调度活跃,4 个 worker 足以并行处理多路 WebSocket 桥接(每条连接的上行/下行/keepalive 各是一个 task),又不会挤占前台 UI 线程。
③ 同步接口 vs 异步内核的"握手"。StartProxy是同步的 C 函数,但TcpListener::bind是异步的。项目用一个std::sync::mpsc通道解决:先rt.spawn出代理主任务,主线程阻塞在rx.recv()上,等 bind 成功才返回 0;bind 失败则handle.abort()并返回 -3(src/lib.rs)。Kotlin 侧因此可以同步拿到确定性的启动结果,无需自己轮询。
3.1 优雅停止:CancellationToken + 2 秒超时
停止流程(src/lib.rs)是教科书级的结构化并发实践:
cancel_tasks.cancel()——一个CancellationToken克隆到所有任务里,任何tokio::select!分支都会响应它退出;rt.block_on等待主任务,timeout(2s)兜底,防止个别卡死的 task 让 UI 线程永久挂起;pools.close_all()关闭连接池里所有 WebSocket,重置黑名单与统计;- 运行时本身保留,等待下次
StartProxy。
连接池的预连接、过期轮换、指数退避重试(1s→3600s)全部由 tokio task 驱动,实现在 src/proxy.rs 的WsPool中;其中甚至专门用同步函数包裹tokio::spawn来打破 async 类型循环依赖(src/proxy.rs 的注释解释了这个 E0391 编译错误的解法)。
3.2 运行时无关的调参通道
配置项(池大小、缓冲区、CF 代理开关等)以AtomicBool/AtomicI32+RwLock全局量存放于 src/config.rs,Kotlin 侧通过SetXxx接口随时热更新,代理任务在下一个循环读取新值——无需重启代理,这是 FFI 分层带来的典型好处:状态机与 UI 彻底解耦。
4. 构建流程:cargo-ndk 一条命令出 so
编译脚本 build_so.bat 展示了标准流程:
cargo ndk -t arm64-v8a --platform 24 -o app/src/main/jniLibs build --release cargo ndk -t armeabi-v7a --platform 21 -o app/src/main/jniLibs build --release要点:
crate-type = ["cdylib"](Cargo.toml):只产出动态库,这正是 Android 加载的形式- 分别针对 arm64-v8a(API 24+)与 armeabi-v7a(API 21+)两个架构交叉编译
- 产物直接放进 jniLibs,随 APK 分发;Kotlin 侧零改动
5. 关键文件速查
想动手跟读源码,按这个顺序效率最高:
| 文件 | 看什么 |
|---|---|
| src/lib.rs | 全部 FFI 导出函数、运行时单例、启停握手 |
| app/src/main/java/com/amurcanov/tgwsproxy/NativeProxy.kt | JNA 接口声明,与 Rust 符号一一对应 |
| src/proxy.rs | WS 连接池:预连接、轮换、退避 |
| src/ws.rs | rustls TLS 配置与会话复用缓存 |
| src/config.rs | 全部超时常量与全局配置状态 |
| build_so.bat | cargo-ndk 交叉编译与 target 安装 |
6. 小结:这条技术路线的三层结构
- FFI 边界层:
extern "C"导出 + JNA 声明,字符串用 C 字符串、指针有明确的FreeString回收协议; - 运行时层:全局
OnceCell<Runtime>单例,4 worker 线程,任务用CancellationToken统一取消,启停走"同步入口 + 异步内核"握手; - 业务层:连接池、桥接、TLS 全部是纯 async task,通过原子量读取运行中热更新的配置。
如果你想给自己的 Android 应用加一个 Rust 高性能内核——无论是代理、加密还是协议解析——这个项目就是一个可以直接抄作业的完整样板:JNA 省掉 JNI 样板,OnceCell全局运行时省掉生命周期管理,而cargo-ndk让构建只需两条命令。
【免费下载链接】tg-ws-proxy-androidAndroid-форк популярного приложения Flowseal - tg-ws-proxy - локальный прокси-сервер MTProto с проксированием CF или без для частичного обхода проблем загрузки Telegram项目地址: https://gitcode.com/gh_mirrors/tg/tg-ws-proxy-android
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考