UniFFI 网络接口实战:从阿里云 AI 到移动端集成
本文,将带您走进 UniFFI 的网络接口实战,包含下面的几部分内容:
1、rust 中的 UniFFI 网络访问
2、Android 中的 UniFFI 数据对接
3、阿里云 AI 集成到 rust 生成的 so 中 和 Android 的 kotlin 调用 UniFFI
如果您对基础部分,不太理解,可以阅读我的前一篇文章。 UniFFI 跨平台开发Rust 与 Android (Kotlin) 集成
第一章:走进阿里云 AI 接口
在开始编写代码前,我们先通过最基础的 curl 命令了解 API 的本质。
1.1 接口请求初探
Bash
curl -X POST https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions \
-H "Authorization: Bearer $DASHSCOPE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "deepseek-v4-pro",
"messages": [{"role": "user", "content": "9.9和9.11哪个大"}],
"reasoning_effort": "high"
}'
1.2 参数详解(形象比喻)
| 组成部分 | 示例 / 参数名 | 含义说明 | 形象比喻 |
|---|---|---|---|
| 请求动作 | POST |
指定请求类型为"提交数据"。 | 寄出一封挂号信 |
| 请求地址 | https://dashscope... |
阿里云 API 的大门地址。 | 收信人的详细门牌号 |
| 身份验证 | Authorization |
携带 API Key 证明身份。 | 进门需要的门禁卡 |
| 模型选择 | "model" |
决定谁来回答你的问题。 | 指定某位领域的专家 |
| 对话内容 | "messages" |
包含角色和具体问题。 | 咨询单上的具体内容 |
| 推理强度 | "reasoning_effort" |
强制模型进入"深度思考"模式。 | 开启大脑的"烧脑模式" |
第二章:Rust 核心层实现
我们将使用 Rust 编写高性能的核心逻辑,并通过 UniFFI 将其"翻译"给 Java/Kotlin。
2.1 项目结构 (File Tree)
plaintext
------------- src
---------------------- bin
----------------------------------- uniffi-bindgen.rs (绑定生成器)
---------------------- client
----------------------------------- mod.rs (API 逻辑)
---------------------- lib.rs (库入口 & 全局初始化)
---------------------- errors.rs (错误定义)
---------------------- wrapper.rs (UniFFI 导出包装层)
------------- Cargo.toml (依赖配置)
2.2 依赖配置 (Cargo.toml)
案例代码
toml
[package]
name = "my-ai-project"
version = "0.1.0"
edition = "2021"
[lib]
name = "asrust"
path = "src/lib.rs"
crate-type = ["cdylib", "staticlib"]
[dependencies]
thiserror = "1.0"
tokio = { version = "1", features = ["full"] }
reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls", "blocking"] }
serde = { version = "1.0", features = ["derive"] }
log = "0.4"
android_logger = "0.13"
uniffi = { version = "0.31.1", features = ["cli", "tokio"] }
once_cell = "1.19" # 确保版本兼容性
[build-dependencies]
uniffi = { version = "0.31.1", features = ["build"] }
2.3 生成器入口 (uniffi-bindgen.rs)
用于生成各语言胶水代码的工具。
案例代码
rust
fn main() {
uniffi::uniffi_bindgen_main()
}
2.4 API 客户端逻辑 (client/mod.rs)
这里负责真正的网络请求
案例代码
rust
use serde::{Deserialize, Serialize};
/// 消息结构体
#[derive(serde::Serialize)]
struct Message {
/// 消息角色
role: String,
/// 消息内容
content: String,
}
/// 聊天请求结构体
#[derive(serde::Serialize)]
struct ChatRequest {
/// 模型名称
model: String,
/// 消息列表
messages: Vec<Message>,
}
/// 聊天输入结构体
#[derive(Debug, Serialize)]
struct ChatInput {
/// 用户提示词
prompt: String,
}
/// 聊天响应结构体
#[derive(Debug, Deserialize)]
struct ChatResponse {
/// 输出内容
output: ChatOutput,
}
/// 聊天输出结构体
#[derive(Debug, Deserialize)]
struct ChatOutput {
/// AI 生成的文本
text: String,
}
/// 通义千问客户端
pub struct QwenClient {
/// API 密钥
api_key: String,
/// HTTP 客户端
http_client: reqwest::Client,
}
impl QwenClient {
/// 创建新的 QwenClient 实例
///
/// # 参数
/// * `api_key` - API 密钥
pub fn new(api_key: String) -> Self {
Self {
api_key,
http_client: reqwest::Client::new(),
}
}
/// 发送聊天请求并获取回复
///
/// # 参数
/// * `prompt` - 用户提示词
///
/// # 返回值
/// AI 的文本回复或错误信息
pub async fn chat(&self, prompt: &str) -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
// 阿里云 DashScope API 的兼容模式端点
let url = "https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions";
// 构建聊天请求体,使用 qwen-turbo 模型
let body = ChatRequest {
model: "qwen-turbo".to_string(),
messages: vec![Message {
role: "user".to_string(),
content: prompt.to_string(),
}],
};
// 发送 POST 请求到 API
let response = self.http_client
.post(url)
// 设置认证头,使用 Bearer Token 方式
.header("Authorization", format!("Bearer {}", self.api_key.trim()))
// 设置内容类型为 JSON
.header("Content-Type", "application/json")
// 序列化请求体为 JSON
.json(&body)
// 发送请求并等待响应
.send()
.await?;
// 获取响应状态码
let status = response.status();
// 获取响应文本内容
let res_text = response.text().await?;
// 记录响应内容和状态码用于调试
log::info!("chat..res_text: {}", res_text);
log::info!("chat..status: {}", status);
Ok(res_text)
}
}
2.5 错误定义 (errors.rs)
案例代码
rust
#[derive(Debug, thiserror::Error, uniffi::Error)]
pub enum QwenError {
#[error("环境配置错误: {0}")]
EnvError(String),
#[error("请求失败: {0}")]
RequestError(String),
}
2.6 导出包装层 (wrapper.rs)
UniFFI 不直接支持 async 导出到 Java,所以我们在这里做同步转换。
案例代码
rust
use std::sync::Arc;
use crate::errors::QwenError;
use crate::client::QwenClient as InternalClient;
use crate::RUNTIME;
#[derive(uniffi::Object)]
pub struct QwenWrapper {
pub(crate) inner: Arc<InternalClient>,
}
#[uniffi::export]
impl QwenWrapper {
/// 供外部语言调用的同步接口
pub fn run_chat(&self, prompt: String) -> Result<String, QwenError> {
RUNTIME.block_on(async {
self.internal_chat(prompt).await
})
}
}
impl QwenWrapper {
async fn internal_chat(&self, prompt: String) -> Result<String, QwenError> {
let _guard = RUNTIME.enter();
self.inner
.chat(&prompt)
.await
.map_err(|e| QwenError::RequestError(e.to_string()))
}
}
2.7 库入口 (lib.rs)
案例代码
rust
pub mod errors;
pub mod wrapper;
pub mod client;
use std::sync::Arc;
use crate::errors::QwenError;
use crate::wrapper::QwenWrapper;
use crate::client::QwenClient as InternalClient;
use once_cell::sync::Lazy;
use tokio::runtime::Runtime;
uniffi::setup_scaffolding!("asrust");
static RUNTIME: Lazy<Runtime> = Lazy::new(|| {
Runtime::new().expect("Failed to create Tokio runtime")
});
#[uniffi::export]
pub fn init_client(api_key: String) -> Result<Arc<QwenWrapper>, QwenError> {
// 仅在 Android 环境下初始化日志
#[cfg(target_os = "android")]
android_logger::init_once(
android_logger::Config::default()
.with_max_level(log::LevelFilter::Trace)
.with_tag("RustApp"),
);
if api_key.is_empty() {
return Err(QwenError::EnvError("API_KEY 不能为空".into()));
}
let internal_client = InternalClient::new(api_key);
Ok(Arc::new(QwenWrapper {
inner: Arc::new(internal_client),
}))
}
2.8 两个命令,生成 需要的文件
命令1:生成 so 库
cargo ndk -t arm64-v8a -t x86_64 build --release
命令2: 生成胶水层 kotlin 需要注意两个路径,自己根据实际情况调整
cargo run --bin uniffi-bindgen generate \
--library ./target/x86_64-linux-android/release/libasrust.so \
--language kotlin \
--out-dir ./src
第三章:Android 集成
将 Rust 编译出的 .so 文件和 UniFFI 生成的 .kt 文件放入 Android 项目。
3.1 权限与 Gradle 配置
在 AndroidManifest.xml:
xml
<uses-permission android:name="android.permission.INTERNET" />
在 build.gradle.kts (app):
groovy
dependencies {
implementation("net.java.dev.jna:jna:5.14.0@aar") // UniFFI 必需
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.6.2")
}
3.2 so和胶水kotlin
目录结构
---- app
---------------- src
------------------------- main
--------------------------------------- java
------------------------------------------------------- 胶水层生成的kotlin
--------------------------------------- jniLibs
-------------------------------------------------------- arm64-v8a
----------------------------------------------------------------------------- libxxxx.so
-------------------------------------------------------- x86_64
----------------------------------------------------------------------------- libxxxx.so
3.3 界面调用 (Jetpack Compose)
案例代码
kotlin
@Composable
fun AiChatScreen() {
val scope = rememberCoroutineScope()
var responseText by remember { mutableStateOf("等待点击...") }
Column {
Button(onClick = {
scope.launch(Dispatchers.IO) {
try {
val apiKey = "你的_API_KEY"
val client = initClient(apiKey)
val reply = client.runChat("你好世界,有什么作用?")
// 回到主线程更新 UI
withContext(Dispatchers.Main) {
responseText = reply
Log.i(TAG, "cosmo...reply: $reply")
}
} catch (e: Exception) {
Log.e(TAG, "Error: ${e.message}")
}
}
}) {
Text("询问 AI")
}
Text(text = responseText)
}
}
第四章:成果验证
点击按钮后,查看 Android Studio 的 Logcat,你将看到以下流转过程:
- Kotlin 发起 IO 协程。
- JNA 调用 Rust 层的
init_client。 - Rust 使用
reqwest发送 HTTPS 请求。 - Aliyun 返回 JSON 数据。
- Rust 解析 JSON 并将纯文本结果返回给 Kotlin。
- Compose 刷新界面,显示 AI 的回答。
最终显示的 log 信息如下
17:33:36.849 D android_logger: android_logger: log::set_logger failed: attempted to set a logger after the logging system was already initialized
17:33:36.850 I asrust::client: cosmo...chat..url: https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions
17:33:36.850 D reqwest::connect: starting new connection: https://dashscope.aliyuncs.com/
17:33:39.420 V reqwest::retry: shouldn't retry!
17:33:39.420 I asrust::client: cosmo...chat..res_text: {"choices":[{"finish_reason":"stop","index":0,"message":{"content":"你好!"你好,世界"是一个简单的问候语,通常用于打招呼或表达友好。它的作用主要包括:\n\n1. **交流与沟通**:在人际交往中,问候语是建立联系的第一步,能够拉近人与人之间的距离。\n\n2. **表达善意**:通过"你好",可以传递友好、尊重和开放的态度。\n\n3. **开启对话**:在交谈开始时,一句"你好"可以自然地引导对话的进行。\n\n4. **文化习惯**:在许多文化中,问候是社交礼仪的一部分,体现了对他人的一种基本尊重。\n\n5. **情感连接**:即使是简单的"你好",也能让人感受到被关注和欢迎。\n\n如果你是在某种特定情境下使用这句话(比如编程中的示例、艺术作品等),它的作用可能会有所不同。你可以进一步说明背景,我可以更具体地解释它的意义! 😊","role":"assistant"}}],"created":1777368818,"id":"chatcmpl-f477cc9e-3691-9b31-b6fa-1234567890","model":"qwen-turbo","object":"chat.completion","usage":{"completion_tokens":181,"prompt_tokens":20,"prompt_tokens_details":{"cached_tokens":0},"total_tokens":201}}
17:33:39.420 I asrust::client: cosmo...chat..status: 200 OK
17:33:39.421 I cosmo...reply: {"choices":[{"finish_reason":"stop","index":0,"message":{"content":"你好!"你好,世界"是一个简单的问候语,通常用于打招呼或表达友好。它的作用主要包括:\n\n1. **交流与沟通**:在人际交往中,问候语是建立联系的第一步,能够拉近人与人之间的距离。\n\n2. **表达善意**:通过"你好",可以传递友好、尊重和开放的态度。\n\n3. **开启对话**:在交谈开始时,一句"你好"可以自然地引导对话的进行。\n\n4. **文化习惯**:在许多文化中,问候是社交礼仪的一部分,体现了对他人的一种基本尊重。\n\n5. **情感连接**:即使是简单的"你好",也能让人感受到被关注和欢迎。\n\n如果你是在某种特定情境下使用这句话(比如编程中的示例、艺术作品等),它的作用可能会有所不同。你可以进一步说明背景,我可以更具体地解释它的意义! 😊","role":"assistant"}}],"created":1777368818,"id":"chatcmpl-f477cc9e-3691-9b31-b6fa-1234567890","model":"qwen-turbo","object":"chat.completion","usage":{"completion_tokens":181,"prompt_tokens":20,"prompt_tokens_details":{"cached_tokens":0},"total_tokens":201}}