【新功能介绍】【js宏开发】jside ffi: 跨语言调用机制

王子陶
王子陶

@金山办公

功能介绍

在js宏中调用外部的使用C、C++、Go、Zig、Rust等系统级编程语言编写的动态库

快速入门

1. 用C++语言(或其他native语言)编写导出函数,并编译成动态库

以windows平台为例

2. 用js宏加载函数

const {add, sum} = ffi.LoadLibrary("C:/path/to/library.dll",{
    add: { returnType: "int32", parameters: ["int32","int32"]},
    sum: { returnType: "int32", parameters: ["int32","pointer"]},
})

3. 调用函数

console.log(add(1,2))

let array = new Int32Array([1,2,3,4])
console.log(sum(array.length, array.buffer))

特性

  • js调用native函数

  • js动态合成C语言是函数,作为回调函数

  • 在js中合成和访问C语言结构体、联合体、定长数组、多重指针等, 甚至生成模板类型

  • 从js访问C语言的指针,进行指针运算、读写、数组访问、分配释放等操作

  • 跨操作系统。Windows、Gnu/Linux、MacOS、Ohos(鸿蒙都支持)。

  • 跨CPU架构。x86/x64、aarch64、mips、loongarch(龙芯)等CPU都支持。

  • 添加平台api,可以获取操作系统和CPU类型信息。

  • 支持将ArrayBuffer当作void*类型传递给C函数

使用场景

  • 调用sqlite,访问数据库

  • 调用openssl,对数据加密

  • 调用wgpu、opengl,进行3d渲染

  • 调用dbus,与linux系统和桌面的其他组件通信,调用linux系统的功能

  • 调用curl,用各种协议连接服务器

  • 调用User32.dll,调用windows系统的功能

例子

结构体

let Vector3 = ffi.Struct([
    { name: "x", type: "float" },
    { name: "y", type: "float" },
    { name: "z", type: "float" },
])
let vector3 = new Vector3 ({x:4,y:5,z:6})
vector3.x = 1.0
vector3.y = 2.0
vector3.z = 3.0
console.log(vector3.toObject().x)

结构体模板

let Vector3 = (T) => ffi.Struct([
    { name: "x", type: T },
    { name: "y", type: T },
    { name: "z", type: T },
])
let Vector3f = Vector3("float")
let vector3 = new Vector3f ({x:4,y:5,z:6})

文档

更多信息请参考文档。文档中使用Typescript来描述ffi的api

https://365.kdocs.cn/3rd/open/documents/dynamic.html?link=/app-integration-dev/wps365/client/wpsoffice/wps-integration-mode/wps-macro-editor-development/run-macro/jside-ffi.html

复杂例子

用ffi访问sqlite数据库

完整例子在文档中

// 数据库指针类型
let DbPtrType = "pointer"
// 数据库二重指针类型,传参时其值可以被转为二重指针
let DbPtrPtrType = ffi.Buffer(DbPtrType)
// 字段名列表类型
let FieldArray = ffi.Pointer("string")
// 值列表类型
let DataArray = ffi.Pointer("string")
let CallbackType = ffi.Function({ffi:"",returnType:"int32",parameters:[DbPtrType, "int32", FieldArray, DataArray]})
// 报错字符串类型,传参时其值可以被转为二重指针
let ErrorStringType = ffi.Buffer("string")

// 按照平台设置动态库路径
let os = Platform.OS()
let arch = Platform.Arch()
let sqlite_path = "TODO"
// 省略获取sqlite_path路径的代码

// 加载sqlite动态库和函数
const sqlite_lib = ffi.LoadLibrary(sqlite_path, {
    sqlite3_open: { returnType: "int32", parameters: ["string",DbPtrPtrType]},
    sqlite3_close: { returnType: "int32", parameters: [DbPtrType]},
    sqlite3_exec: {returnType: "int32",parameters: [DbPtrType,"string",CallbackType,"pointer",ErrorStringType ]},
})
// 枚举值定义
const SQLITE_OK = 0
const SQLITE_DONE = 101

// 对sqlite的封装
class Sqlite{
    constructor(path){
        let db_ptr_ptr = new DbPtrPtrType()
        // 打开数据库. db_ptr_ptr是个ArrayBuffer, 会被转为指针
        this.call_api(()=>sqlite_lib.sqlite3_open(path, db_ptr_ptr))
        // 取出db_ptr_ptr内部被写入的指针
        this.db = db_ptr_ptr.read()
        
    }
    // 对错误处理流程进行封装
    call_api(func) {
        let code = func.apply(this)
        if (code != SQLITE_OK && code != SQLITE_DONE){
            throw new Error("sqlite api error: "+code)
        }
    }
    // 关闭数据库
    close(){
        this.call_api(()=>sqlite_lib.sqlite3_close(this.db))
    }
    // 调用sql语句
    exec(sql, callback){
        console.log("exec: " + sql)
        let error_string_ptr = new ErrorStringType()
        // sqlite执行sql语句. 这里闭包类型(CallbackType)会被转为函数指针, Buffer类型(ErrorStringType)会被取指针
        let code = sqlite_lib.sqlite3_exec(this.db,sql,callback || null,null,error_string_ptr)
        // 错误处理
        if (code != SQLITE_OK && code != SQLITE_DONE){
            let message = error_string_ptr.read()
            throw new Error(`sqlite api error: ${code}, message: ${message}`)
        }
    }
    // 调用select语句, 并获取数据
    // types是字段类型的字符串解析函数的数组
    select(sql,types) {
        let table = []
        // 合成闭包
        let callback = CallbackType.createJsFunction((p, cols /*: number*/ , argv /*: PointerValue*/, colv /*: PointerValue*/)=>{
            // 闭包内部需要用try-catch , 因为js异常和C++异常无法被C语言处理
            try{
                let record = {}
                // 遍历C语言式数组指针
                for (let i =0; i< cols; i++){
                    // 读取字段名
                    let colName = colv.read(i)
                    // 读取字段值
                    let colData = argv.read(i)
                    record[colName] = types[i](colData)
                }
                table.push(record)
                return SQLITE_OK;
            } catch (e) {
                alert(e)
            }
            
        })
        this.exec(sql,callback)
        
        return table
    }
}
广东省
浏览 476
6
7
分享
7 +1
6
6 +1
全部评论 6
 
初心不忘
看着很牛逼
· 江苏省
回复
 
Again
期待wps365 mips 版本更新ffi
· 安徽省
回复
 
风清月霁
风清月霁

WPS产品体验官

点赞
· 河南省
回复
 
Again
给力!
· 安徽省
回复
 
wils
wils

创作者俱乐部成员

赞!起飞
· 海南省
回复
 
恰同学少年
强大的WPS
· 黑龙江省
回复