1 个不稳定版本

0.1.0 2021年6月6日

#12 in #pb


用于 pbni-rs

BSD-2-Clause

30KB
503 代码行数,不包括注释

pbni-rs

github
docs.rs version BSD-2-Clause licensed

pbni-rs是PBNI的Rust绑定,使开发者可以使用Rust语言进行PowerBuilder扩展开发。
注意 pbni-rs只支持PowerBuilder 10及以上版本。

功能标志

标志 描述 默认
global_function 全局函数导出 启用
nonvisualobject 不可视对象导出 启用
visualobject 可视对象导出 启用
decimal Decimal类型处理,将引入rust_decimal 启用
datetime 日期类型处理,将引入chrono 启用
vm 加载虚拟机以及创建Session等功能,将引入libloading 禁用

什么是PBNI?

PBNI是PowerBuilder虚拟机的C++扩展接口(PowerBuilder Native Interface)。

Figure

通过PBNI接口我们可以使用底层语言与PBVM进行集成交互,极大的扩展了PowerBuilder的能力。

其他托管语言类似的技术有JNIC++/CLI等。

环境要求

  • rustc: 最低1.51 (支持stable)
  • toolchain: stable-x86_64-pc-windows-msvc
  • target: i686-pc-windows-msvc

开始使用

  1. 添加32位目标平台
> rustup target add i686-pc-windows-msvc
  1. pbni-rs添加到Cargo.toml
[lib]
crate-type = ["cdylib"]

[dependencies]
pbni-rs = "0.1.0"

注意crate-type需要为cdylib

  1. 编译
> cargo build --target i686-pc-windows-msvc

你还可以在工程目录下创建.cargo/config文件

  • 配置默认编译目标,免除输入--target i686-pc-windows-msvc参数
[build]
target = "i686-pc-windows-msvc"
  • 配置静态链接CRT
[target.i686-pc-windows-msvc]
rustflags = ["-C", "target-feature=+crt-static"]

错误排查

  • 编译出现_PBX_GetVersion@0此类链接错误
    产生原因是因为你的项目代码没有引用pbni-rs,所以被编译器优化掉了pbni-rs库生成的导出符号,解决方法是项目中引入pbni-rs代码。
//lib.rs
use pbni::*;

引入全部名称不是必须的,只要你的代码中使用了pbni即可

数据类型映射

PowerBuilder Rust
int pbinti16
uint pbuintu16
长整型 pblongi32
无符号长整型 pbulongu32
长长整型 pblonglongi64
实数 pbrealf32
双精度浮点数 pbdoublef64
decimal Decimal(需要开启decimal特性)
字节 pbbyteu8
布尔型 bool
字符 PBChar
字符串 &PBStrPBStringString
二进制大对象 &[u8]Vec<u8>
日期 NaiveDate(需要开启datetime特性)
时间 NaiveTime(需要开启datetime特性)
datetime NaiveDateTime(需要开启datetime特性)
任意类型
任意对象 对象
任意数组 数组

PowerBuilder的所有类型都是可空的,在Rust中使用Option<T>表示。

字符串

PowerBuilder字符编码是UTF-16LE,而Rust字符串编码采用的是UTF-8编码,这使得字符串操作时可能会有一点的性能损失。如果对性能有较高要求,请使用&PBStr进行交互,避免发生内存拷贝和编码转换。

pbni-rs提供了pbstr!宏在编译时生成&'static PBStr

let rstr: &'static str = "hell world!";
let pstr: &'static PBStr = pbstr!("hell world!");

pbni-rs使用widestring进行UTF-16编码转换。

内存安全

pbni-rs的Safe代码提供100%类型和内存安全保证,对于无法提供100%的内存安全保证的接口都使用了unsafe标记。最常见的就是获取引用,比如&PBStr

impl<'obj> Object<'obj> {
    pub unsafe fn get_var_str(&self, fid: impl VarId) -> Option<&'obj PBStr> { ... }
    pub fn get_var_string(&self, fid: impl VarId) -> Option<PBString> { ... }
    pub fn set_var_str(&mut self, fid: impl VarId, value: impl AsPBStr) -> Result<()> { ... }
}

可以看到Objectget_var_strunsafe方法,而get_var_string则是Safe的,这是因为像set_var_str这样的方法可能会修改get_var_str返回引用的内存,导致悬垂引用(Dangling Reference)。
pbni-rs无法避免这种情况,因为对象的内部状态不完全由Rust维护,有很多途径会导致内存被修改,所以pbni-rs中所有返回引用的方法都将是Unsafe的,需要开发者自己保证对其正确使用。

线程安全

Session及其所有分配的资源都不能跨线程访问(包括Object/Array),因此它们都不是SendSync的,跨线程访问建议结合消息队列实现。

代码生成

pbni-rs可以非常方便地将Rust对象或函数与PowerBuilder建立映射,全部由pbni-rs生成代码,省去手写繁琐的样板代码的同时保证了类型安全。

映射PowerBuilder全局函数

  • PowerBuilder
global type gf_bit_or from function_object native "pbrs.dll"
end type

forward prototypes
global function long gf_bit_or (readonly long a,readonly long b)
end prototypes
  • C++
#include <pbext.h>

PBXRESULT bit_or(PBCallInfo *ci)
{
    pblong a = ci->pArgs->GetAt(0)->GetLong();
    pblong b = ci->pArgs->GetAt(1)->GetLong();
    return ci->returnValue->SetLong(a|b);
}
PBXRESULT PBXCALL PBX_InvokeGlobalFunction(
    IPB_Session *pbsession,
    LPCWSTR functionName,
    PBCallInfo *ci)
{
    if(::wcscmp(functionName,L"gf_bit_or") == 0)
        return bit_or(ci);
    return PBX_E_NO_REGISTER_FUNCTION;
}
  • Rust(pbni-rs)
use pbni::*;

#[global_function(name="gf_bit_or")]
fn bit_or(a: pblong, b: pblong) -> pblong {
    a | b
}

映射PowerBuilder对象

  • PowerBuilder
forward
global type n_pbni from nonvisualobject
end type
end forward

global type n_pbni from nonvisualobject native "pbrs.dll"
public function string of_hello (string world)
end type
global n_pbni n_pbni

on n_pbni.create
call super::create
TriggerEvent( this, "constructor" )
end on

on n_pbni.destroy
TriggerEvent( this, "destructor" )
call super::destroy
end on
  • C++
#include <pbext.h>

class CppObject: public IPBX_NonVisualObject
{
    IPB_Session *session;
    pbobject ctx;

    PBXRESULT handle_hello(PBCallInfo *ci)
    {
        LPCWSTR lpcsWorld = this->session->GetString(ci->pArgs->GetAt(0)->GetString());
        std::wostringstream ss;
        ss << L"hello " << lpcsWorld << L"!";
        return ci->returnValue->SetString(ss.str().c_str());
    }

public:
    CppObject(IPB_Session *pbsession,pbobject pbobj)
    :session(pbsession),
    ctx(pbobj)
    {}
    virtual ~CppObject() override {};

    virtual void Destroy() override { delete this; }

    virtual PBXRESULT Invoke(
        IPB_Session *session,
        pbobject obj,
        pbmethodID mid,
        PBCallInfo *ci) override
   {
        if(mid == 0)
            return this->handle_hello(ci);
        return PBX_E_NO_REGISTER_FUNCTION;
   }
};

PBXRESULT PBXCALL PBX_CreateNonVisualObject(
    IPB_Session *pbsession,
    pbobject pbobj,
    LPCWSTR className,
    IPBX_NonVisualObject **obj)
{
    if(::wcscmp(className,L"n_pbni") == 0)
    {
        *obj = new CppObject(pbsession,pbobj);
        return PBX_OK;
    }
    return PBX_E_NO_SUCH_CLASS;
}
  • Rust(pbni-rs)
use pbni::*;

struct RustObject {
    session: Session,
    ctx: ContextObject
}

#[nonvisualobject(name = "n_pbni")]
impl RustObject {
    #[constructor]
    fn new(session: Session, ctx: ContextObject) -> RustObject {
        RustObject {
            session,
            ctx
        }
    }
    #[method(name="of_Hello")]
    fn hello(&self, world: String) -> String {
        format!("hello {}!",world)
    }
}

参数提取

pbni-rs代码生成宏会自动提取PB参数为Rust映射的数据类型,参数的提取顺序与PB端定义的顺序保持一致。其中有几个特殊的参数:SessionCallInfoRefArgumentsRef,这几个参数对位置没有要求并且数量任意。

use pbni::*;

#[global_function(name="gf_bit_or")]
fn bit_or(session: Session,a: pblong, b: pblong) -> pblong {
    a | b
}

//等同于

#[global_function(name="gf_bit_or")]
fn bit_or(session: Session,args: ArgumentsRef,a: pblong) -> pblong {
    a | args.get(1).get_long().unwrap()
}

注意 Rust端参数列表须与PB端定义的类型数量以及顺序一致,任何不匹配的情况都会在运行时触发异常。

  • Rust端参数如果为非空类型(Option),而PB端提供的参数为NULL,那么框架自动返回NULL给PB调用端,兼容PB标准库的做法,也就是说任何参数传递为NULL那么返回值就为NULL,除非Rust端显式用Option<T>接收。
  • 当参数列表通过CallInfoRef/ArgumentsRef接收后,将不再匹配参数数量,因为这两个参数已经隐式表示接收了所有的参数。CallInfoRef/ArgumentsRef一般用于处理引用传递参数以及变长参数列表。

可选参数列表匹配

以下示例为重载可选参数列表的匹配映射

  • PowerBuilder
global type gf_test from function_object native "pbrs.dll"
end type

forward prototypes
global function long gf_test (readonly long a,readonly long b)
global function long gf_test (readonly long a,readonly long b,readonly long c)
global function long gf_test (readonly long a,readonly long b,readonly long c,readonly long d)
end prototypes
  • Rust(pbni-rs)
use pbni::*;

#[global_function(name="gf_test")]
fn test(a: pblong, b: pblong, c: Option<long>, d: Option<long>) -> pblong {
    a | b
}

依赖

~1.5MB
~34K SLoC