鸿蒙PC:Linux 搭建 Rust 开发环境并实现计算器项目

前言

Rust 近几年在系统开发、跨平台 Native 模块、命令行工具和高性能后端中使用越来越多。对于鸿蒙 PC 应用来说,Rust 并不是直接替代 ArkTS,而是更适合承担核心计算逻辑复杂算法可复用业务模块这类稳定、可测试、对性能和安全性要求更高的部分。

本文通过一个可以在 DevEco Studio 模拟器运行的计算器项目,演示如何把 Rust 接入鸿蒙 PC 应用。整体方案是:

  • ArkTS 负责页面和交互。
  • C++ NAPI 负责鸿蒙侧 Native 桥接。
  • Rust 负责计算器核心运算。
  • CMake 负责把 Rust 静态库链接进 libentry.so
  • DevEco Studio 负责编译、打包和安装到模拟器。

本文项目已经按 arm64-v8ax86_64 两种 ABI 配置,避免 PC 模拟器安装时报 install parse native so failed

在这里插入图片描述

图片说明:这里保留运行效果图位置。发布前建议替换为 DevEco Studio 模拟器中计算器页面截图。

项目仓库地址: AtomGit仓库地址

一、项目目标

1.1 实现效果

本项目实现一个基础计算器,支持:

  1. 数字输入。
  2. 小数点输入。
  3. 加、减、乘、除。
  4. 清空。
  5. 退格。
  6. 正负号切换。
  7. 除零错误提示。
  8. 鸿蒙 PC 模拟器运行。

项目不是单纯 ArkTS 实现,而是把核心计算放在 Rust 中完成。这样文章重点不只是“做一个计算器”,而是展示一条可复用的 ArkTS + C++ NAPI + Rust 技术路线。

1.2 技术架构

整体调用链如下:

ArkTS UI
  ↓ import { calculate } from 'libentry.so'
C++ NAPI
  ↓ rust_calculate(left, right, operator)
Rust staticlib
  ↓ calculate + format_number
返回字符串结果

这种结构的优点是分层明确:

层级 技术 职责
UI 层 ArkTS 页面布局、按钮事件、状态管理
Native 桥接层 C++ NAPI 参数转换、Native 方法导出
核心逻辑层 Rust 运算、错误处理、结果格式化
构建层 CMake + Cargo 构建 Rust 静态库并链接到 so

二、环境准备

2.1 基础工具

需要准备以下工具:

工具 作用 建议
DevEco Studio 鸿蒙应用开发 IDE 使用已安装 OpenHarmony/HarmonyOS SDK 的版本
Rust 编译 Rust 核心库 使用 rustup 安装
CMake/Ninja Native 构建 DevEco SDK 内置
hdc 模拟器安装调试 DevEco SDK 内置
PowerShell/Terminal 执行构建命令 Windows 下使用 PowerShell 即可

本文实际工程路径建议使用英文目录:

D:\Desktop\HarmonyRustPcApp

不要把 DevEco 工程放在中文路径,例如:

D:\Desktop\rust环境搭建\HarmonyRustPcApp

DevEco/Hvigor 对项目路径有限制,中文路径会导致构建失败:

Invalid project path

2.2 安装 Rust

在 PowerShell 中检查 Rust:

rustc --version
cargo --version
rustup --version

如果命令不可用,需要把 Rust 默认安装目录加入 PATH:

C:\Users\你的用户名\.cargo\bin

本文示例机器中路径为:

C:\Users\GZX\.cargo\bin

2.3 安装鸿蒙 Rust target

鸿蒙 PC 模拟器和真机需要不同 ABI。建议同时安装:

rustup target add x86_64-unknown-linux-ohos
rustup target add aarch64-unknown-linux-ohos

如果下载慢,可以临时使用国内镜像:

$env:RUSTUP_DIST_SERVER='https://mirrors.ustc.edu.cn/rust-static'
rustup target add x86_64-unknown-linux-ohos
rustup target add aarch64-unknown-linux-ohos

验证已安装 target:

rustup target list --installed

期望看到:

aarch64-unknown-linux-ohos
x86_64-unknown-linux-ohos
x86_64-pc-windows-msvc

三、项目目录结构

3.1 工程结构

计算器项目目录如下:

HarmonyRustPcApp
├── AppScope
│   └── app.json5
├── build-profile.json5
├── hvigorfile.ts
├── oh-package.json5
└── entry
    ├── build-profile.json5
    ├── hvigorfile.ts
    ├── oh-package.json5
    └── src
        └── main
            ├── cpp
            │   ├── CMakeLists.txt
            │   ├── napi_init.cpp
            │   ├── rust-core
            │   │   ├── Cargo.toml
            │   │   └── src
            │   │       └── lib.rs
            │   └── types
            │       └── libentry
            │           └── index.d.ts
            ├── ets
            │   ├── entryability
            │   │   └── EntryAbility.ets
            │   └── pages
            │       └── Index.ets
            ├── module.json5
            └── resources

3.2 关键文件说明

文件 作用
Index.ets 计算器页面和交互状态
napi_init.cpp 导出 Native 方法给 ArkTS
lib.rs Rust 计算逻辑
Cargo.toml Rust 静态库配置
CMakeLists.txt 调用 Cargo 并链接 Rust 静态库
index.d.ts 给 ArkTS 提供 NAPI 类型声明
entry/build-profile.json5 Native ABI 配置

四、配置鸿蒙 Native ABI

4.1 为什么要配置 ABI

如果 HAP 里只包含 arm64-v8a/libentry.so,但当前设备或模拟器是 x86_64,安装时会报错:

Install Failed: error: failed to install bundle.
code:9568347
error: install parse native so failed.
In the module named entry, the Abi type supported by the device does not match the Abi type configured in the C++ project.

这个错误不是 Rust 代码错误,而是设备 ABI 和 HAP 内 Native so ABI 不匹配。

4.2 正确配置 abiFilters

修改 entry/build-profile.json5

{
  "apiType": "stageMode",
  "buildOption": {
    "externalNativeOptions": {
      "path": "./src/main/cpp/CMakeLists.txt",
      "arguments": "",
      "cppFlags": "",
      "abiFilters": [
        "arm64-v8a",
        "x86_64"
      ]
    }
  },
  "targets": [
    {
      "name": "default"
    }
  ]
}

这样构建后会同时生成:

entry/build/default/intermediates/libs/default/arm64-v8a/libentry.so
entry/build/default/intermediates/libs/default/x86_64/libentry.so

PC 模拟器常见 ABI 是 x86_64,真机或 ARM 模拟器常见 ABI 是 arm64-v8a。开发阶段建议两个都打包,减少部署失败。

五、Rust 核心计算模块

5.1 Cargo.toml 配置

Rust 模块以 staticlib 形式输出,供 C++ 链接:

[package]
name = "rust_core"
version = "0.1.0"
edition = "2021"

[lib]
name = "rust_core"
crate-type = ["staticlib"]

[profile.release]
lto = true
codegen-units = 1
panic = "abort"

这里重点是:

  • crate-type = ["staticlib"]:输出静态库。
  • panic = "abort":Native 场景下减少 unwinding 风险。
  • lto = true:发布构建启用链接优化。

5.2 Rust 运算逻辑

Rust 中处理加、减、乘、除,并对除零和无效操作符返回明确错误:

fn calculate(left: f64, right: f64, operator: &str) -> String {
    match operator {
        "+" => format_number(left + right),
        "-" => format_number(left - right),
        "*" => format_number(left * right),
        "/" => {
            if right.abs() < f64::EPSILON {
                "Cannot divide by zero".to_string()
            } else {
                format_number(left / right)
            }
        }
        _ => "Invalid operator".to_string(),
    }
}

结果格式化函数会去掉多余小数尾零:

fn format_number(value: f64) -> String {
    if !value.is_finite() {
        return "Error".to_string();
    }

    let mut text = format!("{value:.10}");
    while text.contains('.') && text.ends_with('0') {
        text.pop();
    }
    if text.ends_with('.') {
        text.pop();
    }
    if text == "-0" {
        return "0".to_string();
    }
    text
}

5.3 FFI 导出函数

C++ 无法直接调用 Rust 的普通函数,需要通过 C ABI 导出:

#[no_mangle]
pub unsafe extern "C" fn rust_calculate(
    left: c_double,
    right: c_double,
    operator: *const c_char,
) -> *mut c_char {
    if operator.is_null() {
        return into_c_string("Invalid operator".to_string());
    }

    let operator = CStr::from_ptr(operator).to_string_lossy();
    into_c_string(calculate(left, right, operator.as_ref()))
}

Rust 返回的是 C 字符串指针,因此还需要提供释放函数:

#[no_mangle]
pub unsafe extern "C" fn rust_free_c_string(value: *mut c_char) {
    if !value.is_null() {
        let _ = CString::from_raw(value);
    }
}

只要 Rust 通过 CString::into_raw() 把内存交给 C/C++,就必须提供对应释放函数。否则 Native 层频繁调用会产生内存泄漏。

六、C++ NAPI 桥接层

6.1 声明 Rust 函数

C++ 中先声明 Rust 导出的 C ABI 函数:

extern "C" {
char *rust_calculate(double left, double right, const char *operator_text);
void rust_free_c_string(char *value);
}

6.2 参数转换

ArkTS 调用 Native 方法时,参数类型是 napi_value。需要转换为 C++ 类型:

static double GetDoubleArg(napi_env env, napi_value value)
{
    double result = 0.0;
    napi_get_value_double(env, value, &result);
    return result;
}

字符串参数转换:

static std::string GetStringArg(napi_env env, napi_value value)
{
    size_t length = 0;
    napi_get_value_string_utf8(env, value, nullptr, 0, &length);

    std::vector<char> buffer(length + 1);
    napi_get_value_string_utf8(env, value, buffer.data(), buffer.size(), &length);
    return std::string(buffer.data(), length);
}

6.3 导出 calculate 方法

Native 方法接收三个参数:左操作数、右操作数、运算符。

static napi_value Calculate(napi_env env, napi_callback_info info)
{
    size_t argc = 3;
    napi_value args[3] = {nullptr, nullptr, nullptr};
    napi_get_cb_info(env, info, &argc, args, nullptr, nullptr);

    double left = argc > 0 ? GetDoubleArg(env, args[0]) : 0.0;
    double right = argc > 1 ? GetDoubleArg(env, args[1]) : 0.0;
    std::string operatorText = argc > 2 ? GetStringArg(env, args[2]) : "";

    char *message = rust_calculate(left, right, operatorText.c_str());

    napi_value result = nullptr;
    napi_create_string_utf8(env, message == nullptr ? "Error" : message, NAPI_AUTO_LENGTH, &result);

    rust_free_c_string(message);
    return result;
}

6.4 注册 NAPI 模块

static napi_value Init(napi_env env, napi_value exports)
{
    napi_property_descriptor descriptors[] = {
        {"calculate", nullptr, Calculate, nullptr, nullptr, nullptr, napi_default, nullptr},
    };
    napi_define_properties(env, exports, sizeof(descriptors) / sizeof(descriptors[0]), descriptors);
    return exports;
}

这里的 calculate 就是 ArkTS 侧最终导入的方法。

七、ArkTS 页面实现

7.1 声明 Native 类型

为了让 ArkTS 识别 libentry.so 中的函数,需要提供类型声明:

export const calculate: (left: number, right: number, operator: string) => string;

declare const _default: {
  calculate: typeof calculate;
};

export default _default;

路径为:

entry/src/main/cpp/types/libentry/index.d.ts

7.2 ArkTS 导入 Native 方法

页面中使用命名导入:

import { calculate } from 'libentry.so';

不要把 Native 模块当成无类型对象随意调用,否则 ArkTS 严格模式可能报:

Use explicit types instead of "any", "unknown"

7.3 页面状态设计

计算器页面维护几个核心状态:

@State display: string = '0';
@State expression: string = '';
@State storedValue: number | null = null;
@State pendingOperator: string = '';
@State shouldResetDisplay: boolean = false;
@State hasError: boolean = false;

字段说明:

状态 作用
display 当前显示内容
expression 上方表达式提示
storedValue 已输入的左操作数
pendingOperator 等待执行的运算符
shouldResetDisplay 下一次输入是否重置屏幕
hasError 当前是否为错误状态

7.4 调用 Rust 完成计算

当用户点击运算符或等号时,ArkTS 调用 Native:

const result: string = calculate(this.storedValue, currentValue, this.pendingOperator);

如果 Rust 返回错误字符串,页面进入错误状态:

if (result.startsWith('Cannot') || result.startsWith('Invalid') || result === 'Error') {
  this.setError(result);
  return;
}

这样 UI 层不需要重复实现除零和无效运算判断,核心规则集中在 Rust 中。

八、CMake 调用 Cargo 构建 Rust

8.1 选择 Rust target

CMake 根据鸿蒙 ABI 选择 Rust target:

if(CMAKE_OHOS_ARCH_ABI STREQUAL "arm64-v8a")
    set(RUST_TARGET "aarch64-unknown-linux-ohos")
elseif(CMAKE_OHOS_ARCH_ABI STREQUAL "x86_64")
    set(RUST_TARGET "x86_64-unknown-linux-ohos")
elseif(CMAKE_OHOS_ARCH_ABI STREQUAL "armeabi-v7a")
    set(RUST_TARGET "armv7-unknown-linux-ohos")
else()
    message(FATAL_ERROR "Unsupported OHOS ABI for Rust: ${CMAKE_OHOS_ARCH_ABI}")
endif()

8.2 查找 cargo

DevEco Studio 启动 CMake 时不一定继承系统 PATH,因此 CMake 里需要兜底查找 cargo:

find_program(CARGO_EXECUTABLE cargo)
if(NOT CARGO_EXECUTABLE AND CMAKE_HOST_WIN32 AND EXISTS "$ENV{USERPROFILE}/.cargo/bin/cargo.exe")
    set(CARGO_EXECUTABLE "$ENV{USERPROFILE}/.cargo/bin/cargo.exe")
endif()
if(NOT CARGO_EXECUTABLE)
    message(FATAL_ERROR "Could not find cargo. Add CARGO_HOME/bin to PATH or install Rust with rustup.")
endif()

8.3 构建 Rust 静态库

add_custom_command(
    OUTPUT ${RUST_STATIC_LIB}
    COMMAND ${CARGO_EXECUTABLE} build
            --manifest-path ${RUST_CRATE_DIR}/Cargo.toml
            --release
            --target ${RUST_TARGET}
            --target-dir ${RUST_TARGET_DIR}
    DEPENDS ${RUST_CRATE_DIR}/Cargo.toml ${RUST_CRATE_DIR}/src/lib.rs
    WORKING_DIRECTORY ${RUST_CRATE_DIR}
    COMMENT "Building Rust static library for ${RUST_TARGET}"
)

最后把 Rust 静态库链接进 libentry.so

add_library(entry SHARED napi_init.cpp)
target_link_libraries(entry PUBLIC rust_core libace_napi.z.so)

九、构建与运行

9.1 安装依赖

在项目根目录执行:

ohpm install

9.2 构建 HAP

hvigorw --mode module -p module=entry assembleHap --stacktrace

构建成功会看到:

BUILD SUCCESSFUL

HAP 输出位置:

entry/build/default/outputs/default/entry-default-unsigned.hap

9.3 验证 Native so

构建成功后应同时存在:

entry/build/default/intermediates/libs/default/arm64-v8a/libentry.so
entry/build/default/intermediates/libs/default/x86_64/libentry.so

如果只存在 arm64-v8a,PC 模拟器安装时很可能失败。

9.4 DevEco Studio 运行

运行步骤:

  1. 使用 DevEco Studio 打开英文路径项目。
  2. 等待 Sync 完成。
  3. 选择 PC/2in1 模拟器。
  4. 点击 Run。
  5. 如果提示签名问题,启用自动签名。

十、常见问题与解决方案

10.1 中文路径导致构建失败

错误示例:

Invalid project path

解决方式:把工程移动到英文路径,例如:

D:\Desktop\HarmonyRustPcApp

10.2 CMake 找不到 cargo

错误示例:

Could not find CARGO_EXECUTABLE using the following names: cargo

解决方式:

  • C:\Users\你的用户名\.cargo\bin 加入 PATH。
  • 重启 DevEco Studio。
  • 在 CMake 中增加 cargo 路径兜底。

10.3 缺少 Rust OHOS target

错误示例:

can't find crate for `std`
the `aarch64-unknown-linux-ohos` target may not be installed

解决方式:

rustup target add aarch64-unknown-linux-ohos
rustup target add x86_64-unknown-linux-ohos

10.4 安装时报 native so ABI 不匹配

错误示例:

install parse native so failed
Abi type supported by the device does not match the Abi type configured in the C++ project.

解决方式:在 entry/build-profile.json5 中配置:

"abiFilters": [
  "arm64-v8a",
  "x86_64"
]

10.5 ArkTS 报 any/unknown 类型错误

错误示例:

Use explicit types instead of "any", "unknown"

解决方式:

  • 提供 entry/src/main/cpp/types/libentry/index.d.ts
  • 使用命名导入。
  • 给 Native 调用结果显式标注类型。
import { calculate } from 'libentry.so';

const result: string = calculate(1, 2, '+');

十一、项目验证结果

11.1 Rust 单元测试

Rust 侧单元测试覆盖:

  • 整数结果格式化。
  • 小数结果格式化。
  • 加减乘除。
  • 除零错误。
  • 无效操作符。

测试命令:

cargo test --manifest-path entry/src/main/cpp/rust-core/Cargo.toml

通过结果:

running 3 tests
test tests::calculates_basic_operations ... ok
test tests::formats_integer_like_results_without_decimal_tail ... ok
test tests::reports_invalid_operations ... ok

11.2 HAP 构建验证

构建命令:

hvigorw --mode module -p module=entry assembleHap --stacktrace

验证结果:

BUILD SUCCESSFUL

产物示例:

entry-default-unsigned.hap

11.3 ABI 产物验证

构建后检查:

arm64-v8a/libentry.so
x86_64/libentry.so

这说明项目可以覆盖 ARM 设备和 PC 模拟器两类常见运行环境。

十二、扩展方向

12.1 支持更复杂表达式

当前计算器是二元运算模型:

left operator right

后续可以在 Rust 中实现表达式解析,例如:

1 + 2 * (3 + 4)

可选方案:

  • 手写 tokenizer。
  • 使用 shunting-yard 算法。
  • 在 Rust 中维护表达式 AST。

12.2 支持历史记录

可以在 ArkTS 中维护历史记录列表:

interface HistoryItem {
  expression: string;
  result: string;
}

也可以把历史记录持久化到本地存储,提升工具完整度。

12.3 支持科学计算

Rust 层可以继续增加:

  • 平方。
  • 开方。
  • 百分比。
  • 三角函数。
  • 幂运算。
  • 取余。

这些逻辑放在 Rust 中更容易测试,也方便复用到其他平台。

十三、工程实践建议

13.1 Rust 负责稳定核心

适合放进 Rust 的内容包括:

  • 数学计算。
  • 文本解析。
  • 加密解密。
  • 文件格式处理。
  • 协议解析。
  • 跨平台核心业务规则。

不建议把所有 UI 状态都搬到 Rust。UI 状态通常和 ArkUI 组件生命周期强相关,放在 ArkTS 中更自然。

13.2 Native 边界要清晰

ArkTS 和 Rust 之间不要传递复杂对象。初期建议使用:

  • number
  • string
  • boolean
  • 简单 JSON 字符串

复杂数据可以先序列化成 JSON,再由 Rust 解析。这样调试成本更低。

总结

本文完成了一个鸿蒙 PC Rust 计算器项目。项目采用 ArkTS 编写界面,C++ NAPI 负责 Native 桥接,Rust 实现核心四则运算,并通过 CMake/Cargo 集成进 DevEco Studio 构建流程。

关键经验有三点:

  1. 鸿蒙 Native 项目必须重视 ABI,PC 模拟器通常需要 x86_64
  2. Rust 接入鸿蒙时要同时配置 aarch64-unknown-linux-ohosx86_64-unknown-linux-ohos
  3. DevEco 工程路径建议使用英文路径,避免 Hvigor 路径校验失败。

这个项目虽然是计算器,但已经覆盖了 Rust 接入鸿蒙 PC 应用的核心链路。后续可以在这个基础上扩展科学计算器、表达式解析器、历史记录、主题切换或更复杂的 Rust 算法模块。

如果这篇文章对你有帮助,欢迎点赞、收藏、关注。你的支持是我持续分享鸿蒙、Rust 和跨平台 Native 开发实践的动力。


相关资源

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐