将 libsmb2 集成到 HarmonyOS ArkTS 项目

将 libsmb2 集成到 HarmonyOS ArkTS 项目

本文记录在鸿蒙媒体播放器项目 hmplayer 中集成 libsmb2 实现 SMB 网络文件浏览与播放的完整过程。libsmb2支持smb3协议。能够查看macos上的文件夹分享。配置macos分享的时候需要把选项中的window共享创建一个账号和密码。之后使用使用此账号密码进行连接。

整体架构

scss 复制代码
ArkTS 层 (Smb2Client.ets)
    ↓ NAPI 绑定 (libentry.so)
C/C++ 层 (napi_init.cpp)
    ↓ 静态链接
libsmb2 (thirdparty/libsmb2/arm64-v8a/lib/libsmb2.so.1)

ArkTS 通过 NAPI 调用 C++ 导出的函数,C++ 层直接调用 libsmb2 的 C API。整个模块编译为 libentry.so,在应用加载时自动注册。

第一步:放置预编译库

libsmb2 不需要在项目中从源码编译,直接放置预编译好的 arm64-v8a 架构的 .so 文件:

bash 复制代码
entry/src/main/cpp/thirdparty/libsmb2/arm64-v8a/
├── include/smb2/
│   ├── libsmb2.h
│   ├── smb2.h
│   ├── smb2-errors.h
│   ├── libsmb2-raw.h
│   └── libsmb2-dcerpc*.h
├── lib/
│   ├── libsmb2.so.6.1.0
│   ├── libsmb2.so.1 -> libsmb2.so.6.1.0
│   ├── libsmb2.so -> libsmb2.so.1
│   └── cmake/libsmb2/
└── lib/pkgconfig/

第二步:配置 CMake

entry/src/main/cpp/CMakeLists.txt 中添加:

cmake 复制代码
cmake_minimum_required(VERSION 3.4.1)
project(libsmb2project)

# 应用主库,包含 NAPI 绑定
add_library(entry SHARED napi_init.cpp)

# 链接 NAPI 运行时和日志库
target_link_libraries(entry PUBLIC libace_napi.z.so libhilog_ndk.z.so)

# 引入 libsmb2 预编译库
set(LIBSMB2_DIR ${CMAKE_CURRENT_SOURCE_DIR}/thirdparty/libsmb2/${OHOS_ARCH})
set(LIBSMB2_LIB ${LIBSMB2_DIR}/lib/libsmb2.so.1)

target_link_libraries(entry PRIVATE ${LIBSMB2_LIB})
target_include_directories(entry PRIVATE ${LIBSMB2_DIR}/include)

${OHOS_ARCH} 由 DevEco Studio 构建时自动注入,值为 arm64-v8a

第三步:配置 ABI 过滤

entry/build-profile.json5 中指定只构建 arm64-v8a

json5 复制代码
{
  externalNativeOptions: {
    path: './src/main/cpp/CMakeLists.txt',
    abiFilters: ['arm64-v8a'],
    arguments: '',
    cppFlags: '',
  },
}

第四步:编写 NAPI 绑定

entry/src/main/cpp/napi_init.cpp 是核心绑定文件,将 libsmb2 的 C API 暴露给 ArkTS。

模块注册

cpp 复制代码
#include <napi/native_api.h>

static napi_module entryModule = {
    .nm_version = 1,
    .nm_flags = 0,
    .nm_filename = nullptr,
    .nm_register_func = RegisterEntryModule,   // 你的注册函数
    .nm_modname = "entry",
    .nm_priv = nullptr,
    .reserved = {0},
};

extern "C" __attribute__((constructor)) void RegisterEntryModule(void)
{
    napi_module_register(&entryModule);
}

__attribute__((constructor)) 保证共享库加载时自动执行注册。

全局状态管理

Native 层使用全局变量维护单一连接:

cpp 复制代码
static smb2_context* g_smb2_ctx = nullptr;
static smb2dir*      g_smb2_dir = nullptr;
static smb2fh*       g_smb2_fh  = nullptr;

同一时间只允许一个 SMB 连接,打开新文件时自动关闭旧的文件句柄。

导出函数示例

每个 NAPI 函数都是 libsmb2 C API 的薄封装,负责 napi_value 与 C 类型之间的转换:

cpp 复制代码
// 初始化上下文
static napi_value InitContext(napi_env env, napi_callback_info info)
{
    // ...
    g_smb2_ctx = smb2_init_context();
    if (!g_smb2_ctx) {
        napi_create_int32(env, -1, &result);
        return result;
    }
    napi_create_int32(env, 0, &result);
    return result;
}

// 连接共享
static napi_value ConnectShare(napi_env env, napi_callback_info info)
{
    size_t argc = 4;
    napi_value args[4];
    napi_get_cb_info(env, info, &argc, args, nullptr, nullptr);

    char server[256], share[256], user[256], password[256];
    // ... 从 napi_value 提取字符串

    int ret = smb2_connect_share(g_smb2_ctx, server, share, user, password);
    // ...
}

// 读取文件(随机访问)
static napi_value ReadFile(napi_env env, napi_callback_info info)
{
    size_t argc = 2;
    napi_value args[2];
    napi_get_cb_info(env, info, &argc, args, nullptr, nullptr);

    int64_t offset, size;
    // ... 提取参数

    uint8_t* buf = new uint8_t[size];
    int n = smb2_pread(g_smb2_ctx, g_smb2_fh, buf, size, offset);

    // 将数据拷贝到 ArrayBuffer 返回给 ArkTS
    void* data;
    napi_create_arraybuffer(env, n, &data, &ab);
    memcpy(data, buf, n);
    delete[] buf;
    return ab;
}

所有导出函数在 RegisterEntryModule 中统一注册:

cpp 复制代码
static napi_value RegisterEntryModule(napi_env env, napi_value exports)
{
    napi_property_descriptor desc[] = {
        { "initContext",        nullptr, InitContext,        nullptr, nullptr, nullptr, napi_default, nullptr },
        { "destroyContext",     nullptr, DestroyContext,     nullptr, nullptr, nullptr, napi_default, nullptr },
        { "setUser",            nullptr, SetUser,            nullptr, nullptr, nullptr, napi_default, nullptr },
        { "setPassword",        nullptr, SetPassword,        nullptr, nullptr, nullptr, napi_default, nullptr },
        { "setDomain",          nullptr, SetDomain,          nullptr, nullptr, nullptr, napi_default, nullptr },
        { "setAuthentication",  nullptr, SetAuthentication,  nullptr, nullptr, nullptr, napi_default, nullptr },
        { "connectShare",       nullptr, ConnectShare,       nullptr, nullptr, nullptr, napi_default, nullptr },
        { "disconnectShare",    nullptr, DisconnectShare,    nullptr, nullptr, nullptr, napi_default, nullptr },
        { "openDir",            nullptr, OpenDir,            nullptr, nullptr, nullptr, napi_default, nullptr },
        { "closeDir",           nullptr, CloseDir,           nullptr, nullptr, nullptr, napi_default, nullptr },
        { "readDir",            nullptr, ReadDir,            nullptr, nullptr, nullptr, napi_default, nullptr },
        { "getError",           nullptr, GetError,           nullptr, nullptr, nullptr, napi_default, nullptr },
        { "openFile",           nullptr, OpenFile,           nullptr, nullptr, nullptr, napi_default, nullptr },
        { "closeFile",          nullptr, CloseFile,          nullptr, nullptr, nullptr, napi_default, nullptr },
        { "getFileSize",        nullptr, GetFileSize,        nullptr, nullptr, nullptr, napi_default, nullptr },
        { "readFile",           nullptr, ReadFile,           nullptr, nullptr, nullptr, napi_default, nullptr },
    };
    napi_define_properties(env, exports, sizeof(desc) / sizeof(desc[0]), desc);
    return exports;
}

第五步:声明 TypeScript 类型

entry/src/main/cpp/types/libentry/index.d.ts 中声明类型,让 ArkTS 能获得完整的类型提示:

typescript 复制代码
export interface Smb2DirEntry {
  name: string
  type: number
  isDirectory: boolean
}

export const SMB2_SEC_UNDEFINED: number
export const SMB2_SEC_NTLMSSP: number
export const SMB2_SEC_KRB5: number

export function initContext(): number
export function destroyContext(): number
export function setUser(user: string): number
export function setPassword(password: string): number
export function setDomain(domain: string): number
export function setAuthentication(method: number): number
export function connectShare(
  server: string,
  share: string,
  user: string,
  password: string
): number
export function disconnectShare(): number
export function openDir(path: string): number
export function closeDir(): number
export function readDir(): Smb2DirEntry[]
export function getError(): string
export function openFile(path: string): number
export function closeFile(): number
export function getFileSize(): number
export function readFile(offset: number, size: number): ArrayBuffer

对应的 oh-package.json5

json5 复制代码
{
  name: 'libentry.so',
  types: './index.d.ts',
  version: '1.0.0',
}

第六步:ArkTS 封装层

在 ArkTS 侧通过 import libentry from 'libentry.so' 导入原生模块,封装为易用的类。

Smb2Client

typescript 复制代码
import libentry from 'libentry.so'

export class Smb2Client {
  private connected: boolean = false

  init(): number {
    return libentry.initContext()
  }

  destroy(): number {
    this.connected = false
    return libentry.destroyContext()
  }

  setUser(user: string): number {
    return libentry.setUser(user)
  }

  setPassword(password: string): number {
    return libentry.setPassword(password)
  }

  setDomain(domain: string): number {
    return libentry.setDomain(domain)
  }

  setAuthentication(method: number): number {
    return libentry.setAuthentication(method)
  }

  connect(
    server: string,
    share: string,
    user: string,
    password: string
  ): number {
    const ret = libentry.connectShare(server, share, user, password)
    if (ret === 0) {
      this.connected = true
    }
    return ret
  }

  disconnect(): number {
    this.connected = false
    return libentry.disconnectShare()
  }

  openDir(path: string): number {
    return libentry.openDir(path)
  }

  closeDir(): number {
    return libentry.closeDir()
  }

  readDir(): libentry.Smb2DirEntry[] {
    return libentry.readDir()
  }

  getError(): string {
    return libentry.getError()
  }

  openFile(path: string): number {
    return libentry.openFile(path)
  }

  closeFile(): number {
    return libentry.closeFile()
  }

  getFileSize(): number {
    return libentry.getFileSize()
  }

  readFile(offset: number, size: number): ArrayBuffer {
    return libentry.readFile(offset, size)
  }
}

SmbFileCache(文件缓存下载)

SMB 文件不能直接作为视频播放器的数据源,需要先下载到本地缓存。SmbFileCache 通过分块读取 + 并发写入实现高效下载:

typescript 复制代码
export class SmbFileCache {
  private client: Smb2Client
  private readonly CHUNK_SIZE = 4 * 1024 * 1024 // 4MB

  async download(
    remotePath: string,
    localPath: string,
    onProgress?: (progress: number, speed: string) => void
  ): Promise<string> {
    // 1. 打开远程文件
    this.client.openFile(remotePath)
    const totalSize = this.client.getFileSize()

    // 2. 分块读取,通过 ConcurrentFileDownloader 写入本地文件
    let downloaded = 0
    let startTime = Date.now()

    while (downloaded < totalSize) {
      const chunkSize = Math.min(this.CHUNK_SIZE, totalSize - downloaded)
      const data = this.client.readFile(downloaded, chunkSize)

      // 并发写入磁盘(不阻塞主线程)
      await ConcurrentFileDownloader.writeChunk(localPath, downloaded, data)

      downloaded += chunkSize

      // 回调进度
      if (onProgress) {
        const elapsed = (Date.now() - startTime) / 1000
        const speed = downloaded / elapsed / 1024
        onProgress(downloaded / totalSize, `${speed.toFixed(1)} KB/s`)
      }
    }

    // 3. 关闭文件句柄
    this.client.closeFile()
    return localPath
  }
}

第七步:UI 层集成

SmbEntry 类型

目录条目类型与 NAPI 声明一致:

typescript 复制代码
interface SmbEntry {
  name: string
  type: number
  isDirectory: boolean
}

连接流程

pageSmbBrowser.ets 中,页面组件的生命周期管理 SMB 连接:

typescript 复制代码
@Entry
@Component
struct PageSmbBrowser {
  @State client: Smb2Client = new Smb2Client();
  @State entries: SmbEntry[] = [];
  @State currentPath: string = '/';
  @State downloadProgress: number = 0;
  @State downloadSpeed: string = '';
  @State isDownloading: boolean = false;

  aboutToAppear() {
    this.client.init();
    this.client.setUser('admin');
    this.client.setPassword('123456');
    this.client.setDomain('WORKGROUP');

    const ret = this.client.connect('192.168.1.100', 'Public', 'admin', '123456');
    if (ret === 0) {
      this.loadDirectory('/');
    }
  }

  aboutToDisappear() {
    this.client.disconnect();
    this.client.destroy();
  }

  loadDirectory(path: string) {
    const ret = this.client.openDir(path);
    if (ret === 0) {
      const raw = this.client.readDir();
      // 过滤 . 和 ..,目录优先排序
      this.entries = raw
        .filter(e => e.name !== '.' && e.name !== '..')
        .sort((a, b) => {
          if (a.isDirectory && !b.isDirectory) return -1;
          if (!a.isDirectory && b.isDirectory) return 1;
          return a.name.localeCompare(b.name);
        });
      this.client.closeDir();
    }
  }
}

文件操作

typescript 复制代码
// 点击目录:进入子目录
onDirClick(entry: SmbEntry) {
  const newPath = this.currentPath === '/' ? `/${entry.name}` : `${this.currentPath}/${entry.name}`;
  this.currentPath = newPath;
  this.loadDirectory(newPath);
}

// 点击媒体文件:下载到缓存后播放
onMediaClick(entry: SmbEntry) {
  const remotePath = this.currentPath === '/' ? `/${entry.name}` : `${this.currentPath}/${entry.name}`;
  const localPath = `/data/storage/el2/base/cache/smb/${entry.name}`;

  this.isDownloading = true;
  const cache = new SmbFileCache(this.client);
  cache.download(remotePath, localPath, (progress, speed) => {
    this.downloadProgress = progress;
    this.downloadSpeed = speed;
  }).then((path) => {
    // 播放本地缓存文件(调用导航跳转到播放器页面)
    this.isDownloading = false;
    // pushPath({ name: PageName.videoPlayer, param: { uri: path } });
  });
}

// 返回上级目录
onBack() {
  if (this.currentPath === '/') return;
  const parts = this.currentPath.split('/').filter(p => p.length > 0);
  parts.pop();
  this.currentPath = parts.length > 0 ? '/' + parts.join('/') : '/';
  this.loadDirectory(this.currentPath);
}

API 列表

集成的 16 个 NAPI 函数覆盖了 SMB 文件浏览与读取的完整流程:

函数 对应 libsmb2 API 说明
initContext() smb2_init_context() 创建 SMB 上下文
destroyContext() smb2_destroy_context() 销毁上下文
setUser(user) smb2_set_user() 设置用户名
setPassword(password) smb2_set_password() 设置密码
setDomain(domain) smb2_set_domain() 设置域名
setAuthentication(method) smb2_set_authentication() 认证方式(0=自动, 1=NTLM, 2=Kerberos)
connectShare(server, share, user, password) smb2_connect_share() 连接 SMB 共享
disconnectShare() smb2_disconnect_share() 断开连接
openDir(path) smb2_opendir() 打开远程目录
closeDir() smb2_closedir() 关闭目录句柄
readDir() smb2_readdir() 循环至 NULL 读取全部目录条目
getError() smb2_get_error() 获取最近一次错误信息
openFile(path) smb2_open(path, O_RDONLY) 打开文件(只读)
closeFile() smb2_close() 关闭文件句柄
getFileSize() smb2_fstat() 获取文件大小
readFile(offset, size) smb2_pread() 按偏移量读取指定字节,返回 ArrayBuffer

关键注意事项

  1. 单一连接限制 :Native 层使用全局变量 g_smb2_ctxg_smb2_dirg_smb2_fh,同一时间只能维持一个 SMB 连接、一个目录句柄、一个文件句柄。对于单页面浏览场景足够,如需并发连接需重构为句柄池模式。

  2. 只读文件访问openFile 固定使用 O_RDONLY 标志,不支持写操作。

  3. 同步 API :绑定只使用了 libsmb2 的同步接口(smb2_connect_sharesmb2_pread 等),未接入 libsmb2 的异步事件循环(smb2_get_fd/smb2_service)。在鸿蒙的 TaskPool 中执行可避免阻塞 UI 主线程。

  4. 权限声明 :在 module.json5 中需要声明 ohos.permission.INTERNETohos.permission.GET_NETWORK_INFO

  5. ABI 支持 :当前仅构建 arm64-v8a。如需支持 armeabi-v7ax86_64,需交叉编译对应架构的 libsmb2 并添加到 abiFilters 中。

参考文档

参考文档:参考文档

相关推荐
HwJack207 小时前
HarmonyOS 6APP开发之摸透ArkUI FrameNode
华为·harmonyos
求学中--9 小时前
状态管理一文通:@State、@Prop、@Link、@Provide/Consume全解析
人工智能·小程序·uni-app·wpf·harmonyos
求学中--9 小时前
ArkUI组件库完全指南:从基础组件到自定义装饰器
低代码·华为·小程序·uni-app·harmonyos
●VON10 小时前
鸿蒙原生APP开发实战指南:三套低成本AI辅助方案全解析
人工智能·华为·chatgpt·大模型·harmonyos·image
枫叶丹410 小时前
【HarmonyOS 6.0】Data Augmentation Kit 智慧化数据检索 C 接口解析:向量化、知识检索与知识问答
c语言·开发语言·华为·harmonyos
2301_8152795211 小时前
鸿蒙原生开发的“硬核通道”:ArkTS 与 C/C++ 高性能互操作全栈指南 —— FFI 机制深度解析与实战精要
c语言·c++·harmonyos
前端不太难1 天前
鸿蒙 App 的“无状态 System”设计
华为·状态模式·harmonyos
UnicornDev1 天前
【Flutter x HarmonyOS 6】魔方计时APP——计时逻辑实现
flutter·华为·harmonyos·鸿蒙·鸿蒙系统
AlbertZein2 天前
ImageKnifePro 源码解读:鸿蒙图片加载框架全貌
harmonyos