UniFFI 网络接口实战:从阿里云 AI 到移动端集成

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,你将看到以下流转过程:

  1. Kotlin 发起 IO 协程。
  2. JNA 调用 Rust 层的 init_client
  3. Rust 使用 reqwest 发送 HTTPS 请求。
  4. Aliyun 返回 JSON 数据。
  5. Rust 解析 JSON 并将纯文本结果返回给 Kotlin
  6. 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}}
相关推荐
一只数据集1 小时前
水稻叶片图像与SPAD值标注数据集-140张高质量图像-精准农业机器学习训练数据集
人工智能·深度学习·机器学习
AI技术增长1 小时前
Pytorch图像去噪实战(一):从0复现DnCNN并解决训练不收敛问题(附完整工程+踩坑总结)
人工智能·pytorch·python
AIminminHu1 小时前
(AI篇)OpenGL渲染与几何内核那点事-(二-1-(12):给AI一副“身体”有多难?从“缸中之脑”到R2-D2,一文看透具身智能60年进化血泪史
人工智能·具身智能
三克的油1 小时前
YOLOV5数据学习
人工智能·学习·yolo
海兰1 小时前
【第22篇】Evaluation Example
人工智能·spring boot·log4j·alibaba·spring ai
喵叔哟1 小时前
大模型蒸馏全栈实战:从Claude黑盒克隆到开源模型轻量化落地--目录
人工智能
数据牧羊人的成长笔记1 小时前
分类算法的评价+KMeans聚类与降维算法+决策树与集成学习
人工智能·分类·数据挖掘
隔壁大炮1 小时前
Day07-词嵌入层解释
人工智能·深度学习·算法·计算机视觉·cnn
汽车仪器仪表相关领域1 小时前
Kvaser Memorator Light HS v2:单通道 CAN FD 便携记录仪,即插即用的故障诊断利器
运维·服务器·数据库·人工智能·功能测试·单元测试