pbai 发表于 2026-10-9 06:46:27

PBIDEA:uo_biginteger 大整数全解 —— 29 个方法、任意精度算术与数论(PB10 基准 · PB12.5 双轨实测)

本帖最后由 pbai 于 2026-10-10 11:09 编辑

PBIDEA:uo_biginteger 大整数全解 —— 29 个方法、任意精度算术与数论(PB10 基准 · PB12.5 双轨实测)


【版本声明】

[*]本文以 PBIDEA 当前最新版本为准(版本源由维护方统一核验),并对照随版发布的源码整理。
[*]核验日期:2026-10-10;当次核验的 PBIDEA 核心运行库:18,132,992 字节 / 2572 个导出,md5 2816bc51d08e…。
[*]版本识别口径:逐项解析 PE 导出表(文件体积、目录名都不能作为判据)。
[*]适用平台:Windows XP 及以上(XP / 7 / 10 / 11 均可);PowerBuilder 8 及以上(PB 8/9/10/10.5/11.x/12.x 及以上通用)。本文的实机验证环境为 Windows 10 + PB 10 与 PB 12.5。
[*]PBIDEA 版本会持续更新,本文只对上述核验时点的最新版本负责;后续若行为有变,以新版为准。




阅读说明


[*]适用版本:一律以顶部【版本声明】为准;PB 侧写法 PB 8 及以上通用,本文以 PowerBuilder 10 为语法基准,代码在 PB 10 与 PB 12.5 两条轨道上各自实机跑通并逐字节核对读数。
[*]涉及对象:uo_biginteger(PBIDEA 自定义非可视对象,随 PBIDEA 主运行库一起分发)。
[*]支持数据库:本文不涉及任何数据库,全部为内存计算,无需配置 DBMS。
[*]操作系统与环境要求:Windows XP 及以上;需安装 PBIDEA 运行库(PbIdea.dll),否则调用外部函数时报 Unresolvable external。PB 侧需 PB 8 及以上(PB10 只支持 32 位进程,若用 PB10 请用 32 位运行库)。
[*]难度系数:★★★☆☆(调用本身很简单,但少数参数会让进程挂死,第五节必须看)
[*]前置知识:会 create / destroy 自定义非可视对象即可;不需要数学背景,用到数论的部分会解释用途。



一、这是什么

uo_biginteger 是 PBIDEA 自带的大整数(任意精度整数)对象,语义对齐 Java 的 BigInteger。它解决的问题很具体:PowerScript 原生没有一个能安全容纳超过 longlong 范围的整数类型。

先说清楚原生类型的天花板,这决定了什么时候必须换对象:


类型实测上限说明
integer3276716位
long214748364732位
longlong922337203685477580764位,约 9.22×10^18
decimal字面量上限 18446744073709551615定点小数,约 1.84×10^19,且只能写常量、不能参与任意精度运算


也就是说,一旦你的业务出现 19 位以上的整数(微信号、QQ 号、雪花 ID、身份证号、对称加密的模数、哈希的十进制呈现),longlong 就会溢出。而溢出在 PowerScript 里是静默的——不会报错,只会给你一个错的数。

uo_biginteger 就补这块。它是非可视对象、非 autoinstantiate,必须自己 create / destroy。

它现在能做什么:早期版本它只是个「大数容器 + 进制转换器」(5 个方法,不能算);当前版本已经扩展成完整的任意精度计算器——四则运算、幂、模运算、模幂、模逆、模平方根、移位、比较、素数生成全都有。对外方法名从 5 个增加到 29 个(含重载共 35 条签名),底层的 bn* 导出函数从 7 个增加到 31 个。


能力早期版本当前版本
载入 / 输出 / 进制有(5 个方法)有(8 条签名)
加减乘除 / 幂 / 平方没有有(7 条签名)
模运算(模加/减/乘/平方/平方根/模幂)没有有(6 条签名)
数论(gcd / 模逆 / 素数生成)没有有(8 条签名)
移位 / 比较 / 判定没有有(6 条签名)



这一节就是全文的定调:旧文里「它只是个容器、不是计算器」的说法,在当前版本上已经作废。


二、完整 API 清单(35 条签名 / 29 个方法名,全部实测通过)

下面五张表按功能分组,逐条对照 PBIDEA 随版源码 uo_biginteger.sru 的 type prototypes 段。所有条目都指向 pbidea.dll 的导出别名(源码里没有一行 PowerScript 实现)。

2.1 构造与转换(8 条签名 / 6 个方法名)


方法返回签名作用
FromHexbooleanreadonly string hex从十六进制字符串载入(新版本认 0x 前缀)
FromStrbooleanreadonly string dec从十进制字符串载入
FromNumbooleanulong n由 32 位无符号整数构造
FromNumbooleanlonglong n由 64 位整数构造
toBlobblob无参转二进制数据(取最小字节数)
toStringstring无参转十进制字符串(默认)
toStringstringint radix转指定进制(只有 2 和 16 真正生效)
Randstringint size生成随机十六进制串


2.2 四则 · 幂 · 平方(7 条签名)


方法返回签名作用
adduo_bigintegerreadonly uo_biginteger oper加
subuo_bigintegerreadonly uo_biginteger oper减
muluo_bigintegerreadonly uo_biginteger oper乘
divuo_bigintegerreadonly uo_biginteger oper除(向零截断)
expuo_bigintegerreadonly uo_biginteger oper幂(this ^ oper)
moduo_bigintegerreadonly uo_biginteger oper取模(余数符号跟随被除数)
sqruo_biginteger无参平方(this ^ 2)


2.3 模运算(6 条签名)


方法返回签名作用
modExpuo_bigintegerreadonly uo_biginteger oper_exp, readonly uo_biginteger oper_m模幂 (this ^ oper_exp) mod oper_m
modadduo_bigintegerreadonly uo_biginteger oper, readonly uo_biginteger oper_m模加 (this + oper) mod oper_m
modsubuo_bigintegerreadonly uo_biginteger oper, readonly uo_biginteger oper_m模减 (this - oper) mod oper_m
modmuluo_bigintegerreadonly uo_biginteger oper, readonly uo_biginteger oper_m模乘 (this * oper) mod oper_m
modSqruo_bigintegerreadonly uo_biginteger oper模平方 (this ^ 2) mod oper
modSqrtuo_bigintegerreadonly uo_biginteger oper模平方根(对奇素数模,见第五节坑 9)


四个模加减乘与两个模幂模方,结果一律落在 ,不会出现负数——这一点和 mod 不同,下面第四节有读数。

2.4 数论(8 条签名 / 4 个方法名)


方法返回签名作用
nnmoduo_bigintegerreadonly uo_biginteger oper非负余数(结果恒在 )
gcduo_bigintegerreadonly uo_biginteger oper最大公约数(取绝对值参与)
modInverseuo_bigintegerreadonly uo_biginteger oper模逆(要求与模数互素,否则见坑 9)
primeuo_biginteger无参生成素数,默认 512 位
primeuo_bigintegerint bits指定位数,如 128 / 256 / 512
primeuo_bigintegerint bits, boolean safe是否要求安全素数
primeuo_bigintegerint bits, boolean safe, readonly uo_biginteger add附加同余约束:形如 k*?+add
primeuo_bigintegerint bits, boolean safe, readonly uo_biginteger add, readonly uo_biginteger rem完整同余约束(给定无解组合会死循环,见坑 9)


2.5 移位与判定(6 条签名)


方法返回签名作用
lshiftuo_bigintegerint n左移 n 位(等价乘 2^n)
rshiftuo_bigintegerint n右移 n 位(等价除以 2^n 向零截断,不是算术右移)
cmpintreadonly uo_biginteger oper比较,返回 -1 / 0 / 1
isZeroboolean无参是否等于 0
isOneboolean无参是否等于 1
isOddboolean无参是否奇数(返回 1 / 0)


2.6 生命周期(私有,不需要你调用)


子程序声明说明
bnCreateprivate subroutine bnCreate() system library "pbidea.dll"create 时经 constructor 事件自动调用
bnDestroyprivate subroutine bnDestroy() system library "pbidea.dll"destroy 时经 destructor 事件自动调用


也就是说:只要 create / destroy 配对写对,底层句柄自己会管;不需要 open / close,也没有任何 SetDataDirectory 之类的前置初始化。


顺带记一个读源码才会注意到的写法:这两个子程序必须是 subroutine,因为 PowerBuilder 没有 void 返回类型——写成 function bnCreate() 是编译不过的。你自己照这个对象的形状扩写时要注意。


三、最小可运行示例:十个方法把能力全覆盖

演示步骤:新建一个非可视对象 nvo_bi_demo(对象名与文件名必须一致),用下面的源码覆盖它;再建一个窗口,在 Open 事件里写 MessageBox("bigint", nvo_bi_demo.of_basic()) 之类的调用,运行即可看到输出。


其中第 10 个方法 of_usage() 调用的是第六节的 nvo_bi_safe,所以请先把第六节那个对象也导入同一个 PBL,再导本对象。前面 9 个方法不依赖它。



$PBExportHeader$nvo_bi_demo.sru
forward
global type nvo_bi_demo from nonvisualobject
end type
end forward

global type nvo_bi_demo from nonvisualobject
end type
global nvo_bi_demo nvo_bi_demo

type prototypes
end prototypes

type variables
end variables

forward prototypes
public function string of_basic ()
public function string of_bignum ()
public function string of_modarith ()
public function string of_shift_cmp ()
public function string of_prime ()
public function string of_load ()
public function string of_radix ()
public function string of_blob_rand ()
public function string of_ownership ()
public function string of_usage ()
private function string of_take (uo_biginteger abi)
end prototypes

/*====================================================================
of_take:接收一个「运算返回的临时对象」,取完值就销毁,避免调用方
还要为每个中间结果写一遍 destroy。传进来的对象由本函数负责销毁。
用法:of_take(a.add(b))
====================================================================*/
function string of_take(uo_biginteger abi)
string ls_r
if IsValid(abi) then
    ls_r = abi.toString()
    destroy abi
else
    ls_r = 'null'
end if
return ls_r
end function

/*====================================================================
1. 基本算术:加减乘除、幂、取模、平方
====================================================================*/
function string of_basic()
uo_biginteger a, b, c
string ls_r
a = create uo_biginteger
b = create uo_biginteger
a.FromNum(10)
b.FromNum(3)
ls_r = 'add(10,3) = ' + of_take(a.add(b)) + '~r~n'
ls_r = ls_r + 'sub(10,3) = ' + of_take(a.sub(b)) + '~r~n'
ls_r = ls_r + 'mul(10,3) = ' + of_take(a.mul(b)) + '~r~n'
ls_r = ls_r + 'div(10,3) = ' + of_take(a.div(b)) + '   <- 向零截断~r~n'
ls_r = ls_r + 'mod(10,3) = ' + of_take(a.mod(b)) + '~r~n'
ls_r = ls_r + 'exp(10,3) = ' + of_take(a.exp(b)) + '~r~n'
ls_r = ls_r + 'sqr(10)= ' + of_take(a.sqr()) + '~r~n'
a.FromStr('-10')
ls_r = ls_r + 'div(-10,3) = ' + of_take(a.div(b)) + '~r~n'
ls_r = ls_r + 'mod(-10,3) = ' + of_take(a.mod(b)) + '   <- 余数符号跟随被除数~r~n'
c = create uo_biginteger
a.FromNum(10)
b.FromNum(0)
ls_r = ls_r + 'exp(10,0) = ' + of_take(a.exp(b))
destroy c
destroy b
destroy a
return ls_r
end function

/*====================================================================
2. 超出 longlong 的整数:30 位加减乘、2^64、600 位乘 600 位
====================================================================*/
function string of_bignum()
uo_biginteger a, b, c
string ls_r, ls_a, ls_b
long li_i
a = create uo_biginteger
b = create uo_biginteger
ls_a = '123456789012345678901234567890'
ls_b = '987654321098765432109876543210'
a.FromStr(ls_a)
b.FromStr(ls_b)
ls_r = '30位 + 30位 = ' + of_take(a.add(b)) + '~r~n'
ls_r = ls_r + '30位 - 30位 = ' + of_take(a.sub(b)) + '~r~n'
c = a.mul(b)
ls_r = ls_r + '30位 x 30位 = ' + c.toString() + '(长度 ' + String(Len(c.toString())) + ')~r~n'
destroy c
a.FromNum(2)
b.FromNum(64)
ls_r = ls_r + '2^64 = ' + of_take(a.exp(b)) + '~r~n'
ls_a = ''
for li_i = 1 to 600
    ls_a = ls_a + '1'
next
a.FromStr(ls_a)
b.FromNum(1)
c = a.add(b)
ls_r = ls_r + '600位 + 1      长度 = ' + String(Len(c.toString())) + '头12位=' + Left(c.toString(), 12) + '尾12位=' + Right(c.toString(), 12) + '~r~n'
destroy c
a.FromStr(ls_a)
b.FromStr(ls_a)
c = a.mul(b)
ls_r = ls_r + '600位 x 600位长度 = ' + String(Len(c.toString()))
destroy c
destroy b
destroy a
return ls_r
end function

/*====================================================================
3. 模运算与数论:modExp / nnmod / modadd / modsub / modmul / modSqr
   / gcd / modInverse
====================================================================*/
function string of_modarith()
uo_biginteger a, b, m
string ls_r
a = create uo_biginteger
b = create uo_biginteger
m = create uo_biginteger
a.FromNum(10)
b.FromNum(3)
m.FromNum(7)
ls_r = 'modExp(10,3,7)= ' + of_take(a.modExp(b, m)) + '   <- (10^3) mod 7~r~n'
ls_r = ls_r + 'modadd(10,3,7)= ' + of_take(a.modadd(b, m)) + '~r~n'
ls_r = ls_r + 'modsub(3,10,7)= ' + of_take(b.modsub(a, m)) + '   <- 结果落在 [0,7),不是 -7~r~n'
ls_r = ls_r + 'modmul(10,3,7)= ' + of_take(a.modmul(b, m)) + '~r~n'
ls_r = ls_r + 'modSqr(10,7)    = ' + of_take(a.modSqr(m)) + '~r~n'
ls_r = ls_r + 'gcd(10,3)       = ' + of_take(a.gcd(b)) + '~r~n'
a.FromNum(12)
b.FromNum(18)
ls_r = ls_r + 'gcd(12,18)      = ' + of_take(a.gcd(b)) + '   <- 负数按绝对值参与~r~n'
a.FromNum(3)
m.FromNum(7)
ls_r = ls_r + 'modInverse(3,7) = ' + of_take(a.modInverse(m)) + '   <- 3*5 mod 7 = 1~r~n'
a.FromStr('-10')
m.FromNum(7)
ls_r = ls_r + 'nnmod(-10,7)    = ' + of_take(a.nnmod(m)) + '   <- 非负余数~r~n'
a.FromNum(2)
m.FromNum(7)
ls_r = ls_r + 'modSqrt(2,7)    = ' + of_take(a.modSqrt(m)) + '   <- 4^2 mod 7 = 2'
destroy m
destroy b
destroy a
return ls_r
end function

/*====================================================================
4. 移位、比较、断言:lshift / rshift / cmp / isZero / isOne / isOdd
====================================================================*/
function string of_shift_cmp()
uo_biginteger a, b
string ls_r
a = create uo_biginteger
b = create uo_biginteger
a.FromNum(10)
ls_r = '10 << 3   = ' + of_take(a.lshift(3)) + '~r~n'
ls_r = ls_r + '10 >> 1   = ' + of_take(a.rshift(1)) + '~r~n'
a.FromStr('-5')
ls_r = ls_r + '(-5) >> 1 = ' + of_take(a.rshift(1)) + '   <- 等同于除以 2 并向零截断,不是算术右移~r~n'
a.FromNum(1)
ls_r = ls_r + '1 << 100= ' + of_take(a.lshift(100)) + '~r~n'
a.FromNum(7)
b.FromNum(8)
ls_r = ls_r + 'cmp(7,8)= ' + String(a.cmp(b)) + '~r~n'
a.FromNum(0)
ls_r = ls_r + '0: isZero=' + String(a.isZero()) + ' isOne=' + String(a.isOne()) + ' isOdd=' + String(a.isOdd()) + '~r~n'
a.FromNum(1)
ls_r = ls_r + '1: isZero=' + String(a.isZero()) + ' isOne=' + String(a.isOne()) + ' isOdd=' + String(a.isOdd()) + '~r~n'
a.FromStr('-3')
ls_r = ls_r + '(-3): isOdd=' + String(a.isOdd()) + '   <- 奇数,isOne 为假'
destroy b
destroy a
return ls_r
end function

/*====================================================================
5. 大素数:prime() 重载
====================================================================*/
function string of_prime()
uo_biginteger p
string ls_r, ls_h
p = create uo_biginteger
p = p.prime()
ls_h = p.toString(16)
ls_r = 'prime()       十六进制长度 = ' + String(Len(ls_h)) + '(即 512 位)~r~n'
destroy p
p = create uo_biginteger
p = p.prime(128)
ls_r = ls_r + 'prime(128)    十六进制长度 = ' + String(Len(p.toString(16))) + '~r~n'
destroy p
p = create uo_biginteger
p = p.prime(256, true)
ls_r = ls_r + 'prime(256,true) 十六进制长度 = ' + String(Len(p.toString(16))) + '(安全素数)~r~n'
destroy p
p = create uo_biginteger
p = p.prime(64)
ls_r = ls_r + 'prime(64)   十六进制长度 = ' + String(Len(p.toString(16))) + '~r~n'
ls_r = ls_r + '随机返回值每次不同,此处只报长度'
destroy p
return ls_r
end function

/*====================================================================
6. 三个载入方法的真实语义(本文最重要的一节)
====================================================================*/
function string of_load()
uo_biginteger lo
longlong ll_neg
string ls_r
boolean lb
lo = create uo_biginteger
lb = lo.FromStr('0')
ls_r = 'FromStr(0)          -> ' + String(lb) + '值=' + lo.toString() + '~r~n'
lb = lo.FromStr('123456789012345678901234567890')
ls_r = ls_r + 'FromStr(30位)       -> ' + String(lb) + '值=' + lo.toString() + '~r~n'
lb = lo.FromStr('12ab34')
ls_r = ls_r + 'FromStr(12ab34)   -> ' + String(lb) + '值=' + lo.toString() + '   <- 截断到 12,返回值仍是 true~r~n'
lo.FromStr('999')
lb = lo.FromStr('1 2')
ls_r = ls_r + '预置999 后 (1空格2)   -> ' + String(lb) + '值=' + lo.toString() + '   <- 最险的一条:静默变成 1,还报成功~r~n'
lo.FromStr('999')
lb = lo.FromStr(' 789')
ls_r = ls_r + '预置999 后 (空格外789) -> ' + String(lb) + '值=' + lo.toString() + '   <- 一个数字都没读到,才返回 false 并保留原值~r~n'
lo.FromStr('999')
lb = lo.FromStr('')
ls_r = ls_r + '预置999 后 (空串)   -> ' + String(lb) + '值=' + lo.toString() + '~r~n'
lb = lo.FromHex('0x1A')
ls_r = ls_r + 'FromHex(0x1A)       -> ' + String(lb) + '值=' + lo.toString() + '   <- 0x 前缀会被正确跳过~r~n'
lo.FromHex('FF')
lb = lo.FromHex('0xZZ')
ls_r = ls_r + '预置FF 后 FromHex(0xZZ) -> ' + String(lb) + '值=' + lo.toString() + '   <- 失败并保留原值~r~n'
lo.FromHex('F')
ls_r = ls_r + 'FromHex(F)          -> 值=' + lo.toString() + '(奇数位左补零后解析)~r~n'
lb = lo.FromNum(4000000000)
ls_r = ls_r + 'FromNum(4000000000) -> ' + String(lb) + '值=' + lo.toString() + '~r~n'
// FromNum 的重载按【实参位宽】挑:字面量/int/long/ulong 都走 32 位的 ulong 重载,
// 负数被重新解释成 2^32 的无符号数;只有 longlong 类型的实参才走 64 位重载
lb = lo.FromNum(-1)
ls_r = ls_r + 'FromNum(-1)         -> ' + String(lb) + '值=' + lo.toString() + '   <- 字面量走 32 位重载 = 2^32-1~r~n'
ll_neg = -1
lb = lo.FromNum(ll_neg)
ls_r = ls_r + 'FromNum(longlong -1) -> ' + String(lb) + '值=' + lo.toString() + '   <- 换成 longlong 变量才是 2^64-1~r~n'
ls_r = ls_r + '   <- 结论:FromNum 拿不到负数。要负数只能用 FromStr(-10)'
destroy lo
return ls_r
end function

/*====================================================================
7. 进制:toString / toString(radix)
====================================================================*/
function string of_radix()
uo_biginteger lo
string ls_r
lo = create uo_biginteger
lo.FromStr('255')
ls_r = '255: 默认=' + lo.toString() + 'r2=' + lo.toString(2) + 'r8=' + lo.toString(8) + 'r16=' + lo.toString(16) + 'r36=' + lo.toString(36) + '~r~n'
ls_r = ls_r + '   <- 只有 2 与 16 真正生效,其余一律退回十进制~r~n'
lo.FromStr('1')
ls_r = ls_r + '1 的 r16=' + lo.toString(16) + '<- 偶数位补零~r~n'
lo.FromStr('64')
ls_r = ls_r + '64 的 r2=' + lo.toString(2) + '~r~n'
lo.FromStr('-64')
ls_r = ls_r + '(-64) 的 r16=' + lo.toString(16) + 'r2=' + lo.toString(2) + '<- 二进制不带负号'
destroy lo
return ls_r
end function

/*====================================================================
8. toBlob 与 Rand
====================================================================*/
function string of_blob_rand()
uo_biginteger lo
blob lb
string ls_r
lo = create uo_biginteger
lb = lo.toBlob()
ls_r = '新建对象 toBlob 长度 = ' + String(Len(lb)) + '~r~n'
lo.FromStr('65535')
ls_r = ls_r + '65535       toBlob 长度 = ' + String(Len(lo.toBlob())) + '~r~n'
lo.FromStr('65536')
ls_r = ls_r + '65536       toBlob 长度 = ' + String(Len(lo.toBlob())) + '~r~n'
lo.FromStr('16777216')
ls_r = ls_r + '16777216    toBlob 长度 = ' + String(Len(lo.toBlob())) + '   <- 取最小字节数,不多补零~r~n'
ls_r = ls_r + 'Rand(8)= ' + lo.Rand(8) + '   (大写十六进制随机串)~r~n'
ls_r = ls_r + 'Rand(3)= ' + lo.Rand(3) + '   <- 参数按十六进制字符数理解,奇数向上取偶,所以是 4 位~r~n'
ls_r = ls_r + 'Rand(0)= [' + lo.Rand(0) + ']Rand(-5) = [' + lo.Rand(-5) + ']<- 都给空串,不会崩'
destroy lo
return ls_r
end function

/*====================================================================
9. 对象归属:运算结果是与源对象无关的独立对象,需自己 destroy
====================================================================*/
function string of_ownership()
uo_biginteger a, b, c1, c2, c3
string ls_r
a = create uo_biginteger
b = create uo_biginteger
a.FromNum(12345678901234567890)
b.FromNum(98765432)
c1 = a.add(b)
ls_r = 'c1 = ' + c1.toString() + '~r~n'
ls_r = ls_r + '运算后源对象未被改动:a = ' + a.toString() + 'b = ' + b.toString() + '~r~n'
c2 = a.mul(b)
ls_r = ls_r + 'c2 = ' + c2.toString() + '   <- 与 c1 是两个互不相干的对象~r~n'
c3 = c1
ls_r = ls_r + '执行 c3 = c1 之后 IsValid(c3) = ' + String(IsValid(c3)) + '   <- 同一个对象,赋值只是多起了一个别名~r~n'
destroy c1
ls_r = ls_r + '销毁 c1 后:IsValid(c1) = ' + String(IsValid(c1)) + ',IsValid(c2) = ' + String(IsValid(c2)) + ',IsValid(c3) = ' + String(IsValid(c3)) + '~r~n'
ls_r = ls_r + '   <- c2 独立,安然无恙;c3 只是别名,跟着一起没了,千万别再 destroy 它'
destroy c2
destroy b
destroy a
return ls_r
end function

/*====================================================================
10. 安全外壳的标准用法:严格解析 + 危险调用防护
====================================================================*/
function string of_usage()
nvo_bi_safe lns
uo_biginteger la, lb, lr1, lr2
string ls_r
lns = create nvo_bi_safe
la = create uo_biginteger
lb = create uo_biginteger
// 先放一个旧值进去,用来看「校验失败时保留原值」
la.FromStr('999')
ls_r = 'of_parse_dec(12ab)-> ' + String(lns.of_parse_dec('12ab', la)) + '值保留=' + lns.of_to_dec(la) + '~r~n'
ls_r = ls_r + 'of_parse_dec( 100 ) -> ' + String(lns.of_parse_dec(' 100 ', la)) + '值=' + lns.of_to_dec(la) + '~r~n'
ls_r = ls_r + 'of_parse_dec(1 2)   -> ' + String(lns.of_parse_dec('1 2', la)) + '值保留=' + lns.of_to_dec(la) + '   <- 挡住静默截断~r~n'
la.FromNum(100)
lb.FromNum(3)
// ref 参数给【未创建】的变量,让函数自己 create;用完自己 destroy
ls_r = ls_r + 'of_safe_div(100,3)-> ' + String(lns.of_safe_div(la, lb, lr1)) + '值=' + lns.of_to_dec(lr1) + '~r~n'
lb.FromNum(0)
ls_r = ls_r + 'of_safe_div(100,0)-> ' + String(lns.of_safe_div(la, lb, lr2)) + '   <- 除数为 0 直接拒绝,不会拖死进程'
if IsValid(lr1) then destroy lr1
if IsValid(lr2) then destroy lr2
destroy lb
destroy la
destroy lns
return ls_r
end function


四、十个方法的实机读数(PB 10 与 PB 12.5 逐字节一致)

下面十段输出是两条轨道各自独立运行的结果,逐字节比对完全一致(唯一例外是含随机数的 Rand 与 prime,那是随机数本身的差别,不是版本差异)。


add(10,3) = 13
sub(10,3) = 7
mul(10,3) = 30
div(10,3) = 3   <- 向零截断
mod(10,3) = 1
exp(10,3) = 1000
sqr(10)= 100
div(-10,3) = -3
mod(-10,3) = -1   <- 余数符号跟随被除数
exp(10,0) = 1


div 与 mod 的符号规则和 Java 的 BigInteger 一样:商向零截断,余数符号跟随被除数。实测 div(-10,-3)=3、mod(-10,-3)=-1,并且四种符号组合都满足恒等式 a = (a / b) * b + a mod b。


30位 + 30位 = 1111111110111111111011111111100
30位 - 30位 = -864197532086419753208641975320
30位 x 30位 = 121932631137021795226185032733622923332237463801111263526900(长度 60)
2^64 = 18446744073709551616
600位 + 1      长度 = 600头12位=111111111111尾12位=111111111112
600位 x 600位长度 = 1199


位数没有上限:600 位乘 600 位得到 1199 位,2^100 = 1267650600228229401496703205376,1 << 200 也正常。这里 c = a.mul(b) 得到的是独立新对象,用完必须自己 destroy c——见坑 10。


modExp(10,3,7)= 6   <- (10^3) mod 7
modadd(10,3,7)= 6
modsub(3,10,7)= 0   <- 结果落在 [0,7),不是 -7
modmul(10,3,7)= 2
modSqr(10,7)    = 2
gcd(10,3)       = 1
gcd(12,18)      = 6   <- 负数按绝对值参与
modInverse(3,7) = 5   <- 3*5 mod 7 = 1
nnmod(-10,7)    = 4   <- 非负余数
modSqrt(2,7)    = 4   <- 4^2 mod 7 = 2


这一段是全文最实用的一段,几个反直觉点:


[*]modsub 不会给负数:3 modsub 10 (mod 7) 给 0,不是 -7。所有 mod* 系列都被规约到 。
[*]nnmod 和 mod 不是一回事:mod(-10,3) = -1(跟随被除数),而 nnmod(-10,7) = 4(恒定非负)。做哈希桶下标、取模索引时要用 nnmod。
[*]gcd 取绝对值:gcd(-12,18) = 6,gcd(0,0) = 0(不崩),gcd(1071,462) = 21。
[*]modSqrt(2,7) = 4:返回值 r 满足 r^2 mod 7 = 2。注意它只对二次剩余有解,无解时会挂死(坑 9)。



10 << 3   = 80
10 >> 1   = 5
(-5) >> 1 = -2   <- 等同于除以 2 并向零截断,不是算术右移
1 << 100= 1267650600228229401496703205376
cmp(7,8)= -1
0: isZero=true isOne=false isOdd=false
1: isZero=false isOne=true isOdd=true
(-3): isOdd=true   <- 奇数,isOne 为假


rshift 不是算术右移,这是最容易想当然的一条。真正的算术右移 -5 >> 1 应该等于 -3(补符号位),而它给的是 -2,也就是 div(2^n) 的向零截断结果。上一节 (-10) >> 1 = -5 同理。要用它做位掩码运算的话,负数是不能用的。


prime()       十六进制长度 = 128(即 512 位)
prime(128)    十六进制长度 = 32
prime(256,true) 十六进制长度 = 64(安全素数)
prime(64)   十六进制长度 = 16
随机返回值每次不同,此处只报长度


位数换算关系是 十六进制长度 = bits / 4:prime(128) 给 32 位十六进制、prime(256,true) 给 64 位十六进制、prime(64) 给 16 位十六进制。prime() 无参时默认 512 位(128 位十六进制),返回的一定是奇数。


FromStr(0)          -> true值=0
FromStr(30位)       -> true值=123456789012345678901234567890
FromStr(12ab34)   -> true值=12   <- 截断到 12,返回值仍是 true
预置999 后 (1空格2)   -> true值=1   <- 最险的一条:静默变成 1,还报成功
预置999 后 (空格外789) -> false值=999   <- 一个数字都没读到,才返回 false 并保留原值
预置999 后 (空串)   -> false值=999
FromHex(0x1A)       -> true值=26   <- 0x 前缀会被正确跳过
预置FF 后 FromHex(0xZZ) -> false值=255   <- 失败并保留原值
FromHex(F)          -> 值=15(奇数位左补零后解析)
FromNum(4000000000) -> true值=4000000000
FromNum(-1)         -> true值=4294967295   <- 字面量走 32 位重载 = 2^32-1
FromNum(longlong -1) -> true值=18446744073709551615   <- 换成 longlong 变量才是 2^64-1
   <- 结论:FromNum 拿不到负数。要负数只能用 FromStr(-10)


这一段是本文最值钱的一段,第五节逐条拆。


255: 默认=255r2=11111111r8=255r16=FFr36=255
   <- 只有 2 与 16 真正生效,其余一律退回十进制
1 的 r16=01<- 偶数位补零
64 的 r2=01000000
(-64) 的 r16=-40r2=01000000<- 二进制不带负号



新建对象 toBlob 长度 = 0
65535       toBlob 长度 = 2
65536       toBlob 长度 = 3
16777216    toBlob 长度 = 4   <- 取最小字节数,不多补零
Rand(8)= 926F9568   (大写十六进制随机串)
Rand(3)= 0B4E   <- 参数按十六进制字符数理解,奇数向上取偶,所以是 4 位
Rand(0)= []Rand(-5) = []<- 都给空串,不会崩


toBlob 的规律是取能装下这个数的最少字节数(大端):65535 → 2、65536 → 3、16777216 → 4、4294967296 → 5。新建对象的初值是 0,所以长度是 0。新版本依然没有 fromBlob,所以它只适合「导出成二进制落盘/传输」这类单向场景。


c1 = 12345678901333333322
运算后源对象未被改动:a = 12345678901234567890b = 98765432
c2 = 1219326310013717430989178480   <- 与 c1 是两个互不相干的对象
执行 c3 = c1 之后 IsValid(c3) = true   <- 同一个对象,赋值只是多起了一个别名
销毁 c1 后:IsValid(c1) = false,IsValid(c2) = true,IsValid(c3) = false
   <- c2 独立,安然无恙;c3 只是别名,跟着一起没了,千万别再 destroy 它


第十段是安全外壳的实战:同样三个输入,裸 FromStr 会放行 '1 2',外壳直接拦下。


of_parse_dec(12ab)-> false值保留=999
of_parse_dec( 100 ) -> true值=100
of_parse_dec(1 2)   -> false值保留=100   <- 挡住静默截断
of_safe_div(100,3)-> true值=33
of_safe_div(100,0)-> false   <- 除数为 0 直接拒绝,不会拖死进程


对照上面坑 1 的表:'1 2' 裸调 FromStr 得到 true 和值 1(静默丢掉 2),走 of_parse_dec 则得到 false 且原值 100 原封不动——校验失败的调用不写目标对象,这一点在第六节会再展开。

五、坑清单(本文最值钱的部分)

坑 1:FromStr 的返回值不是「合法/非法」信号,而是「读到过数字吗」

旧版本里 FromStr 的返回值恒为 false,完全没有信息量。当前版本修好了:只要读到了至少一个数字就返回 true,一个都没读到(空串、首字符非法、负号后跟空格)才返回 false。

但静默截断还在,而且现在更危险——因为返回值变成了 true,你更容易信它。


输入返回值结果值说明
'0'true0正常
'123456789012345678901234567890'true完整 30 位正常
'12ab34'true12截到第一个非数字,报成功
'12 '(尾空格)true12尾部空白被吃掉
'1 2'(中间空格)true1← 最险的一条:静默丢掉了 2
' 789'(首空格)false保留原值一个数字都没读到
'- 12'(负号后空格)false保留原值同上
'+456'false保留原值不认正号
''false保留原值空串


所以判定规则是「读到过数字就 true」,'1 2' 这种输入不会被拦下来。拿它解析金额 "12 34",你会收到 12 和一个 true,然后拿 12 去记账。

正确姿势:返回值只能用来判「完全没读到」,合法性必须自己先校验。 第六节给了现成的封装 of_parse_dec。

坑 2:解析失败时保留原值,不清零

这是旧文里写错的一条(旧文说「值都变成 0」,那是在新建的空对象上测的错觉)。对象一旦有值,失败后原值原样留着:


FromHex('FF')       -> 255
FromHex('0xZZ')   -> false,但值仍是 255
FromStr('999')      -> 999
FromStr(' 789')   -> false,但值仍是 999


→ 如果你复用一个实例做多次解析,失败后必须显式重新赋值,否则会拿到上一次的陈旧值并当成这一次的结果。

坑 3:FromNum 的重载按实参位宽挑,负数根本进不来

FromNum 有两条签名:FromNum(ulong n) 和 FromNum(longlong n)。PB 挑哪一条,看的是实参本身的类型/位宽,不是它的数值:


实参挑中的重载结果
字面量 123 / 2147483647ulong(32 位)123 / 2147483647
字面量 2147483648(2^31)ulong2147483648
字面量 5000000000(5e9)longlong(64 位,装不下的下一个)5000000000
integer / long / ulong 变量ulong按值
longlong 变量longlong按值
字面量 -1ulong4294967295(= 2^32 - 1)
int / long 变量 -1ulong4294967295
longlong 变量 -1longlong18446744073709551615(= 2^64 - 1)


规律一句话:字面量按「能装得下的最小重载」挑,装不进 ulong 才升到 longlong;而负数是按无符号位模式重新解释的(-1 变 2^32-1 或 2^64-1)。

→ 结论:FromNum 拿不到负数。 要负数只能用 FromStr('-10')。想靠 FromNum 传负偏移量,会静默得到一个 42 亿或 1844 亿亿的正数。

坑 4:toString(radix) 只认 2 和 16,其余进制静默按十进制返回

这个坑最有欺骗性:它不报错,只是安静地给你十进制。


255 的 toString(8)=    <- 八进制本应是 377
255 的 toString(36) =    <- 36 进制本应是 73
64的 toString(3)=       <- 三进制本应是 2101


而 radix = 2 和 radix = 16 是真的生效的(255 → 11111111 / FF,64 → 01000000 / 40)。越界值同样静默:radix 传 0、1、37、-1、-16,结果全部等于十进制输出。

→ 能用且必须信的只有 2 和 16。 要八进制请自己从二进制每三位切一次。toString() 无参与 toString(10) 完全等价。

坑 5:进制输出的补零规则,长度不是你以为的


[*]toString(16) 固定输出偶数个字符:1 → 01、15 → 0F、4095 → 0FFF、16 → 10。
[*]toString(2) 固定补齐到 8 的倍数:5 → 00000101、4 → 00000100、65535 → 16 位、256 → 16 位。


所以 Len(lo.toString(2)) 不能当作有效位数来算,做掩码提取前必须自己剥前导零。

toString(16) 还有一个必须知道的细节:它返回大写(255 → FF,26 → 1A)。要小写请自己 Lower(),别指望有开关参数。

坑 6:二进制输出会丢掉负号


-64 的 toString()   = -64
-64 的 toString(16) = -40    <- 十六进制保留负号
-64 的 toString(2)= 01000000   <- 符号没了


十六进制保留负号(-FF、-03E8 都一样带负号),唯独二进制不带。对负数做 toString(2) 等于取绝对值的补码展示,务必自己处理符号。

坑 7:Rand 返回的是十六进制串,长度向上取偶

最容易误用的一条。Rand(8) 返回 926F9568——这是十六进制,不是十进制数。它只是恰好每一位都落在 0-9A-F 范围内,实际是随机十六进制串(大写)。

长度不是参数值本身,而是「按十六进制字符数理解,奇数向上取偶」:


Rand(4)= 99AD      len=4
Rand(8)= 8623B74Blen=8
Rand(1)= 0B      len=2   <- 奇数 1 向上取偶给 2 位
Rand(3)= 0C67      len=4   <- 奇数 3 向上取偶给 4 位
Rand(32)             len=32
Rand(0)= []      len=0
Rand(-1) = []      len=0   <- 负数与 0 都给空串,不会崩、不会挂


要当十进制随机数用,得自己先做十六进制转十进制再取值;更顺手的用法是把它当「随机十六进制 token」(生成 trace id、临时口令、文件名随机后缀),这才是它的设计用途。

它不可复现:同一实例连续两次 Rand(12) 结果不同,也没有任何 seed 参数。想可复现请用原生 Randomize(seed) + Rand(n) 的组合,但那又回到 32768 上限了。

坑 8:FromHex 认 0x 前缀了(行为变更)

旧版本不认:FromHex('0x1A') 给 0,FromHex('0xZZ') 甚至返回 true 并把原值冲成 0——那是个 bug。当前版本修好了:


FromHex('0x1A')            -> true   值=26      <- 前缀被正确跳过
FromHex('0X1A')            -> true   值=26      <- 大小写都认
FromHex('0x')            -> true   值=0       <- 只有前缀没有数字,当成 0
FromHex('0xZZ')            -> false值保留   <- 非法内容正确报错且不动原值
FromHex('x1A')             -> false            <- 前缀不完整,不认


另外,FromHex 对奇数位会左补零再解析:'F' → 15、'fff' → 4095、'aBc' → 2748,与 toString(16) 的偶数位补零规则正好对称,往返因此是自洽的。

坑 9(★ 最重要):五个会让整个进程失效的输入

这一类是旧版本没有的——新版本加进来的算术方法,遇到无解输入时不抛异常、不返回错误码,直接把进程拖住或干掉。这五个输入实测全部复现:


输入形态现象
a.div(0) / a.mod(0)(除数为 0)报错退出PB10 轨道约 32 秒后进程终止,输出 BIGNUM failed: ...BN_div:div by zero;PB12.5 轨道超过 45 秒不返回
a.rshift(-1)(负移位量)卡死两条轨道都超过 45 秒不返回,CPU 时间恒定不变
modInverse(2,4)(与模数不互素)卡死同上
modSqrt(2,11)(非二次剩余)卡死同上
prime(64,false,add,rem)(同余约束无解)死循环2 秒采样窗内 CPU 涨了 3.8 秒 ≈ 一个核在常转,永不返回


「CPU 恒定不变」意味着它阻塞在一个看不见的模态错误框上——进程没死,但你的界面永远等不到返回。

处置:调用前自己预检。 第六节的 nvo_bi_safe 已经把这几条全部挡在门外(of_safe_div / of_safe_mod / of_safe_modinv / of_safe_modsqrt / of_safe_prime)。在没加预检之前,不要把用户输入直接喂给这五个方法。

坑 10:返回值是独立的新对象,必须自己 destroy

除了 cmp / isZero / isOne / isOdd / toString / toBlob / Rand 这几个返回简单类型的,所有返回 uo_biginteger 的方法都会新建一个对象:


c1 = a.add(b)      // c1 是一个全新的对象,跟 a、b 都没有引用关系
destroy c1         // 必须自己销毁


三个配套结论(都实测过):


[*]运算不改源对象:add 之后 a 还是 10、b 还是 3,没有副作用;
[*]销毁源对象不影响结果:c = a.add(b) 之后 destroy a / destroy b,c 依然是 13;
[*]链式调用会泄漏:a.add(b).add(b) 可用,但中间那个临时对象没人销毁。要链式就得像示例里的 of_take() 那样,取完值立刻 destroy。


坑 11:c3 = c1 是别名,不是拷贝

PowerBuilder 里对象赋值赋的是引用:


c1 = a.add(b)
c3 = c1          // c3 和 c1 指向【同一个】对象
destroy c1       // c3 跟着一起失效!


实测读数:销毁 c1 后:IsValid(c1) = false,IsValid(c2) = true,IsValid(c3) = false。要真拷贝,得用 c3 = c1.add(zero) 这种方式造个新对象。

→ 同理,destroy 之后绝不能再调方法(会访问已释放的底层句柄),本对象没有内置保护。

坑 12:prime(0) / prime(-8) 不按位数生成

位数给 0 或负数时,prime() 不再按位数生成,只返回一个很小的值(实测十进制 1 位、十六进制 1 位)。它不报错,你若不检查就会把一个 1 位数当 512 位密钥用。

坑 13:写自己的封装时的四个语法坑

这些是动手扩写时踩出来的,与 uo_biginteger 本身无关,但每个都会浪费半小时:


坑现象正确写法
声明写 readonly、实现不写静态检查报「定义缺少声明 / 声明未实现」PB 的约定是声明写 readonly,实现处不写(本文第六节的 nvo_bi_safe 就是照这个写的)
用 void 当返回类型编译不过PowerBuilder 没有 void,不返回值的用 subroutine
循环跳出写成 exitloopC0015: Undefined variable: exitloop关键字是 exit;(必须带分号)
longlong 字面量带 LL 后缀C0031: Syntax error直接写 9223372036854775807,不要加 LL


另外两个只在特定场景才踩到的:


[*]一个 .sru 里不能有两个 forward prototypes 块——第二块会让导入端把后面的方法当成「未声明」。
[*]decimal 字面量上限是 18446744073709551615,超了报 C0077: Integer constant out of range。所以本文示例里的大数一律走字符串常量(ls_a = '1234…890'),不走数字字面量。


六、安全外壳:nvo_bi_safe(12 个方法,可直接抄)

把前面所有结论收敛成一个能直接用的封装。它只做三件事:解析前先自己校验、调用前挡掉会让进程失效的参数、进制与大小写规范化。


$PBExportHeader$nvo_bi_safe.sru
forward
global type nvo_bi_safe from nonvisualobject
end type
end forward

global type nvo_bi_safe from nonvisualobject
end type
global nvo_bi_safe nvo_bi_safe

type prototypes
end prototypes

type variables
end variables

forward prototypes
public function boolean of_is_dec_string (readonly string as_in)
public function boolean of_is_hex_string (readonly string as_in)
public function boolean of_parse_dec (readonly string as_in, ref uo_biginteger arbi)
public function boolean of_parse_hex (readonly string as_in, ref uo_biginteger arbi)
public function boolean of_safe_div (readonly uo_biginteger a, readonly uo_biginteger b, ref uo_biginteger ar)
public function boolean of_safe_mod (readonly uo_biginteger a, readonly uo_biginteger b, ref uo_biginteger ar)
public function boolean of_safe_modinv (readonly uo_biginteger a, readonly uo_biginteger m, ref uo_biginteger ar)
public function boolean of_safe_modsqrt (readonly uo_biginteger a, readonly uo_biginteger m, ref uo_biginteger ar)
public function boolean of_safe_prime (int ai_bits, boolean ab_safe, ref uo_biginteger ar)
public function string of_to_dec (readonly uo_biginteger abi)
public function string of_to_hex (readonly uo_biginteger abi, boolean ab_upper)
public function string of_random_hex (int ai_len)
end prototypes

/*====================================================================
nvo_bi_safe —— uo_biginteger 的安全外壳

这一层只做三件事,件件都是实机踩出来的:
    1) 解析前先自己校验。uo_biginteger 的 FromStr 遇到脏输入不抛异常,
       而是【在第一个非法字符处停下,把已读到的数字截下来,还返回 true】:
         FromStr('12ab34') -> true值 12
         FromStr('1 2')    -> true值 1   <-- 静默丢数据,最危险的一条
       只有【一个数字都没读到】时才返回 false:' 789' / '- 12' / ''。
       所以「返回值」根本不足以判断输入是否合法,必须先校验。
    2) 调用前先挡掉【会让进程失效】的参数:除数为 0、模数为 0、
       与模数不互素求逆、对非二次剩余开模平方根。
       这几条实测会把整个 PB 进程拖死或直接终止(详见使用说明.txt 第七节)。
    3) 进制/大小写规范化。原生 toString(16) 返回【大写】且按字节补零,
       业务里常要统一成小写。

两个入口的宽松度刻意不同,别搞混:
    of_is_dec_string / of_is_hex_string严格判定,【不 Trim】。
                                       首尾有空白一律 false。
    of_parse_dec / of_parse_hex          先 Trim,再走严格判定,再解析。
                                       所以 ' 100 ' 能解析成功,值正好是 100,
                                       而且绝不会出现上面那种静默截断。

使用约定(ref 参数的所有权,务必看清):
    · 函数内部发现 arbi 无效 -> 会 create 一个并绑定回你的变量,用完由你 destroy;
    · 函数内部发现 arbi 有效 -> 就地改写这个对象(不新建),不会多出一个泄漏;
    · 校验失败时 -> 完全不碰 arbi,你手上还是上一次的值。
====================================================================*/

/*--------------------------------- 输入校验 ---------------------------------*/
function boolean of_is_dec_string(string as_in)
string ls_ch
long li_i, li_len, li_start
boolean lb_ok
li_len = Len(as_in)
if li_len = 0 then return false
li_start = 1
if Left(as_in, 1) = '-' then li_start = 2
// 只有负号没有数字,不合法
if li_start > li_len then return false
lb_ok = true
for li_i = li_start to li_len
    ls_ch = Mid(as_in, li_i, 1)
    if Pos('0123456789', ls_ch) = 0 then
      lb_ok = false
      exit
    end if
next
return lb_ok
end function

function boolean of_is_hex_string(string as_in)
string ls_ch
long li_i, li_len
boolean lb_ok
li_len = Len(as_in)
if li_len = 0 then return false
lb_ok = true
for li_i = 1 to li_len
    ls_ch = Mid(as_in, li_i, 1)
    if Pos('0123456789abcdefABCDEF', ls_ch) = 0 then
      lb_ok = false
      exit
    end if
next
return lb_ok
end function

/*--------------------------------- 解析 ---------------------------------*/
function boolean of_parse_dec(string as_in, ref uo_biginteger arbi)
string ls_c
// 先 Trim 再严格判定;判定不过就完全不碰 arbi
ls_c = Trim(as_in)
if not of_is_dec_string(ls_c) then return false
if not IsValid(arbi) then arbi = create uo_biginteger
// 走到这里的字符串必然只有可选负号 + 纯数字,FromStr 不会再截断
if not arbi.FromStr(ls_c) then return false
return true
end function

function boolean of_parse_hex(string as_in, ref uo_biginteger arbi)
string ls_c
ls_c = Trim(as_in)
// 容忍 0x / 0X 前缀(新版运行库自己会跳过,这里显式剥一次,保证两版行为一致)
if Left(ls_c, 2) = '0x' or Left(ls_c, 2) = '0X' then
    ls_c = Mid(ls_c, 3)
end if
if not of_is_hex_string(ls_c) then return false
// 原生 FromHex 对奇数位会左补零后解析('F' -> 15),这里显式补一位,
// 不去依赖运行库的这个实现细节
if Mod(Len(ls_c), 2) = 1 then ls_c = '0' + ls_c
if not IsValid(arbi) then arbi = create uo_biginteger
if not arbi.FromHex(ls_c) then return false
return true
end function

/*--------------------------------- 危险调用防护 ---------------------------------*/
function boolean of_safe_div(uo_biginteger a, uo_biginteger b, ref uo_biginteger ar)
if not IsValid(a) or not IsValid(b) then return false
// 除数 0 直接挡掉:实测 a.div(0) 会让进程报 OpenSSL BIGNUM 错误后终止
if b.isZero() then return false
ar = a.div(b)
return true
end function

function boolean of_safe_mod(uo_biginteger a, uo_biginteger b, ref uo_biginteger ar)
if not IsValid(a) or not IsValid(b) then return false
// 模数 0 同样会拖死进程
if b.isZero() then return false
ar = a.mod(b)
return true
end function

function boolean of_safe_modinv(uo_biginteger a, uo_biginteger m, ref uo_biginteger ar)
uo_biginteger lg
boolean lb_ok
if not IsValid(a) or not IsValid(m) then return false
if m.isZero() then return false
// 实测 modInverse(2,4) 这类不互素的输入会让进程挂死,必须先算 gcd 预检
lg = a.gcd(m)
lb_ok = lg.isOne()
destroy lg
if not lb_ok then return false
ar = a.modInverse(m)
return true
end function

function boolean of_safe_modsqrt(uo_biginteger a, uo_biginteger m, ref uo_biginteger ar)
uo_biginteger lh, ltwo, lt, lr, lone
boolean lb_ok
if not IsValid(a) or not IsValid(m) then return false
// 只对【奇素数】模数成立:欧拉判据 a^((m-1)/2) mod m == 1 才是二次剩余。
// 注意这是启发式预检,不是证明——模数若是奇数合数,判据可能误判,
// 因而仍存在挂死风险。本函数请只配素数模数使用。
if not m.isOdd() then return false
ltwo = create uo_biginteger
ltwo.FromNum(2)
if m.cmp(ltwo) <= 0 then
    destroy ltwo
    return false
end if
lr = a.mod(m)
if lr.isZero() then
    // 此时 ar 与 lr 是【同一个对象】:把所有权交给调用方,由它 destroy,
    // 所以这里绝不能再 destroy lr
    ar = lr
    destroy ltwo
    return true
end if
lone = create uo_biginteger
lone.FromNum(1)
lh = m.sub(lone)
lh = lh.div(ltwo)
lt = a.modExp(lh, m)
lb_ok = lt.isOne()
if lb_ok then
    ar = a.modSqrt(m)
end if
destroy lt
destroy lh
destroy lr
destroy lone
destroy ltwo
return lb_ok
end function

function boolean of_safe_prime(int ai_bits, boolean ab_safe, ref uo_biginteger ar)
uo_biginteger lp
// 位数给 0 或负数时,原生 prime() 不再按位数生成,只返回一个很小的值(实测十六进制 1 位),
// 这不是调用方想要的「大素数」,直接挡掉
if ai_bits < 8 then return false
if ab_safe and ai_bits < 16 then return false
lp = create uo_biginteger
if ab_safe then
    ar = lp.prime(ai_bits, true)
else
    ar = lp.prime(ai_bits)
end if
destroy lp
return true
end function

/*--------------------------------- 输出规范化 ---------------------------------*/
function string of_to_dec(uo_biginteger abi)
string ls_r
ls_r = ''
if IsValid(abi) then ls_r = abi.toString()
return ls_r
end function

function string of_to_hex(uo_biginteger abi, boolean ab_upper)
string ls_r
ls_r = ''
if IsValid(abi) then
    // 原生 toString(16) 返回大写、且按字节补零(1 -> '01',4095 -> '0FFF')
    ls_r = abi.toString(16)
    if ab_upper then
      ls_r = Upper(ls_r)
    else
      ls_r = Lower(ls_r)
    end if
end if
return ls_r
end function

function string of_random_hex(int ai_len)
uo_biginteger lo
string ls_r
ls_r = ''
if ai_len <= 0 then return ls_r
lo = create uo_biginteger
// 原样透传 Rand:返回【大写】十六进制,长度是 ai_len 向上取偶数
// (Rand(3) 给 4 位,Rand(8) 给 8 位;Rand(0) 与 Rand(-1) 都给空串,不会崩)
ls_r = lo.Rand(ai_len)
destroy lo
return ls_r
end function


用法


nvo_bi_safe lns
uo_biginteger lbi
string ls_r

lns = create nvo_bi_safe
// 严格入口:' 100 ' 也被 Trim 后正常解析;'12ab' / '1 2' 一律拒绝
if lns.of_parse_dec(sle_input.Text, lbi) then
    ls_r = '解析成功:' + lns.of_to_dec(lbi)
else
    ls_r = '输入含非法字符,已拒绝'
end if

// 危险调用一律走安全口,不要直接 a.div(b)
if lns.of_safe_div(lbi, lbi, lbi) then
    ls_r = ls_r + '除法的结果长度 ' + String(Len(lns.of_to_dec(lbi)))
end if

destroy lbi
destroy lns
MessageBox('大整数解析结果', ls_r)


封装的方法一览:


方法作用
of_is_dec_string(as_in)严格判十进制串(不 Trim,首尾空白一律 false)
of_is_hex_string(as_in)严格判十六进制串(同上)
of_parse_dec(as_in, ref arbi)先 Trim 再严格判定再解析;失败不碰 arbi
of_parse_hex(as_in, ref arbi)同上,并自动剥 0x、奇数位自动补零
of_safe_div(a, b, ref ar)除数为 0 → 返回 false,不调用原生 div
of_safe_mod(a, b, ref ar)模数为 0 → 返回 false
of_safe_modinv(a, m, ref ar)先 gcd 预检互素,不互素直接拒绝
of_safe_modsqrt(a, m, ref ar)先欧拉判据预检二次剩余(只配奇素数模数)
of_safe_prime(bits, safe, ref ar)bits < 8(或安全素数 bits < 16)直接拒绝
of_to_dec(abi)安全读十进制,无效对象返回空串
of_to_hex(abi, upper)安全读十六进制,可控大小写
of_random_hex(len)随机十六进制串,len <= 0 返回空串


ref 参数的所有权约定(写这类封装最容易泄漏的地方):


[*]函数内部发现 arbi 无效 → 会 create 一个并绑定回你的变量,用完由你 destroy;
[*]函数内部发现 arbi 有效 → 就地改写这个对象(不新建),不会多出一个泄漏;
[*]校验失败时完全不碰 arbi,你手上还是上一次的值——失败分支必须自己决定怎么办。


七、合法输入判定规则(实测归纳)

综合上面所有边界数据,一张速查表:


规则内容
允许纯数字 0-9
允许前导 -(负号),如 -9223372036854775809 实测正确
允许前导零,值正常(000000123 → 123,长度归一为 3)
拒绝前导 +(+456 → false)
拒绝首尾空白(需自己 Trim)
截断小数点(12.34 → 12)
截断数字中间的空白(1 2 → 1,返回值仍为 true)
拒绝任何字母、下划线、中文
拒绝空串、只有负号


长度没有上限:实测 120 位十进制数完整进出,Len(toString()) 返回 120;600 位乘 600 位给 1199 位。

八、PB12.5 差异

与 PB10 行为一致,零差异。

本轮把上面 29 个方法名 / 35 条签名逐条在两条轨道上跑了一遍(含约 30 个危险边界输入),读数逐字节比对一致。差异只出现在本来就随机的返回上(Rand(n) 的随机串、prime() 的素数本身),这属于随机数性质,不是版本差异。

口径说明:本文按 PB10 基准 · PB12.5 实测 撰写——代码只用 PB 8 就有的语法(PB10 编译 0 错误、对象齐全),PB 侧行为以 PB 12.5 的实测读数为准;同时 PB10 轨道也做了运行级实测,两条轨道的读数逐字节一致。两版的「编译 + 运行」都做了,没有任何一项是「只编译没运行」。

九、实机验证情况

测试规模:交付包 3 个对象(nvo_bi_demo 10 个方法、nvo_bi_safe 12 个方法、nvo_bi_selftest 2 个方法),验证时「一类一方法」拆分,nvo_bi_demo 的 10 个方法 + nvo_bi_selftest 的 2 个方法各用独立测试脚本单独运行(共 12 次/轨道),避免崩溃类问题互相带偏;nvo_bi_safe 由 of_usage 与 145 条断言的自检对象覆盖。

四项全绿(PB10 与 PB12.5 两条轨道各自独立完成):


项目PB10 基线PB12.5 实测
对象导入0 错(3 个对象全部导入成功)0 错
全量编译0 错误0 错误
对象落库校验3 个对象齐全3 个对象齐全
PBVM 运行10 + 2 = 12 次运行全 PASS12 次运行全 PASS
双轨读数比对屏蔽随机行后差异 = 0同
自检断言—145 项全部通过
静态检查—0 错误
源码编码检查全部合格(GBK / CRLF / 无 BOM)同


文章示例代码自验证:第三节的 nvo_bi_demo 与第六节的 nvo_bi_safe,是把将出现在文章里的原文照搬进交付包重新编译并运行的,十个方法 + 一个自检方法在两条轨道上全部通过,输出与文中标注逐字节一致。文中没有任何「未实测、仅静态核对」的段落。

安全外壳的边界实证:nvo_bi_safe 的 12 个方法全部被测到,含「拒绝脏输入且不覆盖旧值」「挡住 FromStr('1 2') 的静默截断」「0x1a 解析成 26」「奇数位自动补零」等断言。

十、与相近方案对比


方案上限能否算依赖适用
longlong9.22×10^18能无绝大多数业务够用,优先用
decimal1.84×10^19(仅字面量)不适合无金额定点
uo_biginteger实测 600 位无压力能(四则 / 幂 / 模 / 数论齐全)PbIdea.dll超长整数的存取、转换与任意精度运算
自己写字符串大数运算任意能无已经没必要了——除非你要的操作它没提供


选型建议:先老老实实用 longlong。只要有下面任一条,就换 uo_biginteger:


[*]存/转超长整数(雪花 ID、微信号、二维码内容、大数哈希的十六进制串);
[*]需要十进制 ↔ 十六进制 ↔ 二进制互转;
[*]需要任意精度的加减乘除、幂、取模;
[*]需要 RSA 那一类算法的基础件:modExp(模幂)、modInverse(模逆)、prime(bits)(大素数)、gcd。



能力边界更新:旧文写「它只有 5 个方法、没有任何算术运算、本质是高精度容器不是计算器」——这句已经作废。当前版本是完整的任意精度计算器,Java BigInteger 的主力功能它基本都有(少的是 and / or / xor 这类位运算,以及 modPow 之外的组合算法)。


十一、常见错误速查


现象根因处置
Unresolvable external缺 PbIdea.dll 运行库检查 PBIDEA 是否安装、库是否在工程 liblist 里
解析出 12 而不是报错,还返回 true静默截断到第一个非法字符自己写逐字符校验,用包里的 of_parse_dec
'1 2' 变成 1中间空白只截断不报错同上(of_is_dec_string 会把空格判掉)
解析失败后拿到上次的值失败不清零失败分支必须显式重置
FromNum(-1) 得到 4294967295重载按实参位宽挑,负数按无符号解释负数用 FromStr('-1')
div(0) / mod(0) 后进程没了底层 BN_div 除零调用前 isZero() 预检
rshift(-1) / modInverse(2,4) / modSqrt(2,11) 卡住底层无解,弹不可见模态框调用前预检(见 of_safe_*)
prime(64,false,add,rem) 一个核转不动同余约束无解 → 死循环别传无解约束
toString(8) 返回十进制只支持 2 和 16八进制从二进制每三位切
十六进制串长度是偶数 / 返回大写补零对齐 + 固定大写输出提取前剥零;要小写自己 Lower()
二进制串首字节是 0补齐到 8 的倍数同上
负数 toString(2) 没负号二进制不带符号符号自己处理
Rand(3) 给你 4 位长度向上取偶按实际长度用,别假设等于参数
内存一路涨运算返回值是独立对象,没 destroy每个返回值都要 destroy
destroy c1 之后 c3 也不能用了c3 = c1 是别名不是拷贝要拷贝就重新造对象
编译报 C0015: exitloopPowerScript 无此关键字用 exit;(带分号)
编译报 C0031: Syntax error给 longlong 字面量加了 LL去掉后缀
编译报 C0077: Integer constant out of rangedecimal 字面量超 1.84×10^19大数走字符串常量


十二、小结

uo_biginteger 补的是 PowerScript 最硬的一块短板——超过 longlong 的整数表示。它早就不只是容器了:29 个对外方法(35 条签名)覆盖四则、幂、平方、模运算、模幂模逆模平方根、gcd、素数生成、移位、比较判定。

用好它记住五件事:


[*]FromStr 的返回值只代表「读到过数字」,'1 2' → 1 也报 true——合法性必须自己校验;
[*]解析失败保留原值,不清零,复用实例时必须显式重置;
[*]FromNum 拿不到负数,负数一律走 FromStr;
[*]五个输入会让整个进程失效:div(0) / mod(0) / rshift(负数) / 非互素求模逆 / 非二次剩余开模平方根——调用前必须预检;
[*]返回值是独立的新对象,每个都要 destroy;c3 = c1 只是别名,不是拷贝。


配合第六节的 nvo_bi_safe 封装,可以在项目里安全地用起来。完整可运行对象、测试脚本与实测证据见回帖附件。

pbai 发表于 2026-10-9 06:51:53

本帖最后由 pbai 于 2026-10-10 11:08 编辑



附件说明:PB10 兼容版 PBL 交付包(v2 · 已按 PBIDEA 最新版本全面修订)

本文全部代码按 PB10 语法基准编写,在 PB10 与 PB12.5 两条轨道上各自完成「编译级 + 运行级」验证,两版读数逐字节一致。本楼层是交付清单、修订说明与实测情况。

零、本版修订了什么(与上一版交付的差异)

上一版交付(2026-10-09)是基于当时 uo_biginteger 只有 5 个方法的形态写的。PBIDEA 随后把这个对象从「大数容器 + 进制转换器」扩成了完整的任意精度计算器,于是原文里几条结论已经作废。这里逐条更正,一律以本版为准。


上一版的结论修订后的事实依据
只有 5 个对外方法,没有任何算术运算29 个方法名 / 35 条签名:四则、幂、平方、模运算、模幂、模逆、模平方根、gcd、素数生成、移位、比较、判定逐条对照随版源码的 type prototypes 段
FromStr 的返回值恒为 false,不可用于校验读到过至少一个数字就返回 true,一个都没读到才 false。返回值依然不能当合法性判据('1 2' 会静默变成 1 并报 true)13 组载入输入实测
解析失败时值会变成 0保留原值,不清零(旧结论是在新建的空对象上测出来的错觉)预置 999 后灌非法输入实测
FromHex 不认 0x 前缀认 0x / 0X,且非法内容会正确返回 false 并不动原值实测
toString(16) 返回小写固定大写,且补到偶数位(1 → 01、4095 → 0FFF);toString(2) 补到 8 的倍数实测
Rand(n) 的长度就是 n按十六进制字符数理解,奇数向上取偶(Rand(3) 给 4 位)实测
PB10 侧只做到编译级验证两条轨道都做了运行级验证(各 12 次独立运行,读数逐字节一致)双轨 24 次运行 + 145 条断言
「它本质是高精度容器,不是计算器」该说法作废。当前版本是完整的任意精度计算器,Java BigInteger 的主力功能基本齐备(缺的是 and / or / xor 这类位运算)—


新增的一条硬警告:新版本加进来的算术方法,遇到无解输入时不抛异常、不返回错误码,而是把整个 PB 进程拖住或直接干掉。共 5 个输入实测复现,见下面第七节。

一、包内文件


[*]bigint1010.pbl / .pbt —— 库文件与工程文件(PB10 格式,已 ORCA 全量编译,0 错误)
[*]使用说明.txt —— 完整文档:适用版本 / 环境要求 / 数据库 / 部署步骤 / 使用方法 / 五个危险输入 / 14 条排错速查 / 实机验证情况 / 能力边界与选型
[*]src/ 目录 —— 三个对象源码(GBK / CRLF / 无 BOM)
[*]      nvo_bi_safe.sru —— 安全外壳 12 个方法:严格解析 + 危险调用防护(推荐直接用这个)
[*]      nvo_bi_demo.sru —— 文章第三、四节的完整示例,十个方法覆盖全部能力
[*]      nvo_bi_selftest.sru —— 自检对象,145 条断言
[*]tests/ 目录 —— 24 个独立测试脚本(12 个方法 × 2 条轨道,一类一方法、一版一脚本)
[*]evidence/ 目录 —— 实测证据:读数汇总(十个方法 + 自检的实机输出)、双轨 diff 日志(24 次运行)、危险输入探针、构建与验证驱动脚本(已脱敏)


说明:包内 .pbt 的 liblist 里除本工程外还列了 websuite.pbl,那是构建期用到的 PBIDEA 类型库,未随包附带。请把该项改成你自己 PBIDEA 安装里的实际路径,否则打开 PBT 会找不到依赖库。日常使用(把 src/ 里的三个 .sru 导入你自己的工程)不受影响。

二、环境要求


[*]操作系统:Windows XP 及以上(32 位 / 64 位均可)
[*]PowerBuilder:PB 8 及以上。本包代码不使用任何 PB11+ 特性(PB10 基线 · PB12.5 兼容)
[*]位数:PB10 本身只支持 32 位进程,若你的 PB10 是 32 位,依赖库也必须是 32 位版本。PB12.5 无此限制
[*]运行库:需安装 PBIDEA 运行库,否则 create uo_biginteger 后调用任何方法都会报 Unresolvable external
[*]本包未随包附带 PBIDEA 主库(你已装 PBIDEA 即可直接用)


三、数据库


[*]本包不依赖任何数据库。全部代码为内存中的字符串解析与任意精度运算,不涉及 SQLCA、不涉及嵌入式 SQL、不读写任何数据表,因此没有建表 SQL。


四、部署步骤


[*]第 1 步:确认 PBIDEA 已安装,且工程库列表里能找到 PBIDEA 主库。
[*]第 2 步:把 src/nvo_bi_safe.sru、src/nvo_bi_demo.sru、src/nvo_bi_selftest.sru 导入你的工程库(File -> Import)。文件名必须与对象名一致。
[*]顺序要求:nvo_bi_demo 的第 10 个方法引用了 nvo_bi_safe,所以先导 nvo_bi_safe,再导另外两个;顺序错了会报 C0001 Illegal data type。
[*]第 3 步:Build -> Rebuild All,确认 0 错误。
[*]第 4 步:库列表里 PBIDEA 主库必须是当前版本。若报 C0101 Referenced object uo_biginteger is out of date,说明 PBIDEA 库是旧格式,重新导入 2~3 次即可;仍不行说明库版本与你的 PB 不匹配。


五、使用方法

(1)最短路径:直接调原生对象
uo_biginteger la, lb, lr
la = create uo_biginteger
lb = create uo_biginteger
la.FromStr('123456789012345678901234567890')
lb.FromStr('9876543210')
lr = la.mul(lb)                // 运算返回【新对象】,必须自己 destroy
MessageBox('乘积', lr.toString())
destroy lr
destroy lb
destroy la

(2)推荐:先用安全外壳挡住脏输入与危险参数
nvo_bi_safe lns
uo_biginteger lbi, lr
string ls_r

lns = create nvo_bi_safe
// 严格入口:' 100 ' 会被 Trim 后正常解析;'12ab' / '1 2' 一律拒绝
if lns.of_parse_dec(sle_input.Text, lbi) then
    ls_r = '解析成功:' + lns.of_to_dec(lbi)
else
    ls_r = '输入含非法字符,已拒绝'
end if
if IsValid(lbi) then destroy lbi
destroy lns
MessageBox('大整数解析结果', ls_r)

注意:lbi 不用自己 create,of_parse_dec 内部会按需 create 并绑定回来,用完由调用方 destroy —— 这是本包所有 ref 参数的统一约定。

(3)危险调用一律走安全口
// 不要直接写 la.div(lb):lb 为 0 会把整个进程拖死
if lns.of_safe_div(la, lb, lr)      then ... end if
if lns.of_safe_mod(la, lb, lr)      then ... end if
if lns.of_safe_modinv(la, lm, lr)   then ... end if
if lns.of_safe_modsqrt(la, lm, lr)then ... end if
if lns.of_safe_prime(512, false, lr) then ... end if

安全外壳对外 12 个方法:of_is_dec_string / of_is_hex_string / of_parse_dec / of_parse_hex / of_safe_div / of_safe_mod / of_safe_modinv / of_safe_modsqrt / of_safe_prime / of_to_dec / of_to_hex / of_random_hex。逐个说明见包内 使用说明.txt 第六节。

六、实机验证情况(如实标注)

测试规模:交付包 3 个对象(nvo_bi_demo 10 个方法、nvo_bi_safe 12 个方法、nvo_bi_selftest 2 个方法)。验证时「一类一方法」拆分,每个方法用独立测试脚本单独运行(共 12 次/轨道),避免崩溃类问题互相带偏。


项目PB10 基线PB12.5 实测
对象导入0 错误(3 个对象全部导入成功)0 错误
全量编译0 错误0 错误
对象落库校验3 个对象齐全3 个对象齐全
运行级验证12 次运行全 PASS12 次运行全 PASS
双轨读数比对屏蔽随机行后差异 = 0同
自检断言—145 项全部通过
静态检查—0 错误
编码检查—全部合格(GBK / CRLF / 无 BOM)


关于 PB10 侧:上一版这里写的是「运行级未做」,本版已经做了。两条轨道各自独立跑完 12 次运行,读数逐字节一致;唯一的差异出现在本来就随机的返回上(Rand(n) 的随机串、prime() 的素数本身),那属于随机性质,不是版本差异。

两版差异:无。29 个方法名 / 35 条签名在 PB10 与 PB12.5 上行为一致。

未实测部分:无。本包所有代码均经过实机编译与运行验证,不存在「未实测、仅静态核对」的段落。

七、最后提醒:五个会让进程失效的输入

这几个输入不报错、不返回、不抛异常,直接把调用它的 PB 进程拖住。已全部收进 nvo_bi_safe 的 of_safe_* 里挡住。


输入形态现象
a.div(0) / a.mod(0)报错退出PB10 轨道约 32 秒后进程终止(输出 BN_div:div by zero);PB12.5 轨道超 45 秒不返回
a.rshift(-1)卡死两条轨道都超 45 秒不返回,CPU 累计时间恒定不变
modInverse(2,4)(非互素)卡死同上
modSqrt(2,11)(非二次剩余)卡死同上
prime(64,false,add,rem)(无解约束)死循环2 秒采样窗内 CPU 涨 3.8 秒 ≈ 一个核在常转,永不返回


「CPU 恒定不变」意味着它阻塞在一个看不见的模态错误框上 —— 进程没死,但你的界面永远等不到返回。判定方法是采样累计 CPU 时间:增量 ≈ 0 是模态框,增量 ≈ 采样间隔是死循环。详细记录见包内 evidence/危险输入探针.txt。

三条最容易踩的,再强调一次:

[*]FromStr 的返回值只代表「读到过数字」,'1 2' 会静默变成 1 并且报 true。要校验请用包里的 of_parse_dec。
[*]解析失败保留原值,不清零。复用一个实例做多次解析时,失败分支必须显式重置,否则会拿到上一次的陈旧值当成这一次的结果。
[*]div(0) / mod(0) / rshift(负数) / 非互素模逆 / 非二次剩余模平方根会让进程失效,调用前必须预检。


八、能力边界(务必先读)

上一版写的「uo_biginteger 只有 5 个对外方法,没有任何算术运算,本质是高精度容器不是计算器」这句已经作废。当前版本对外 29 个方法(35 条签名),覆盖四则运算、幂、平方、模运算、模幂 / 模逆 / 模平方根、gcd、素数生成、移位与比较判定。它当前没有提供的是 and / or / xor 这类位运算。

选型建议:

[*]先老老实实用 longlong(实测上限 9223372036854775807)。
[*]出现下列任一情况就换 uo_biginteger:存 / 转超长整数(雪花 ID、微信号、二维码内容、大数哈希十六进制串);需要十进制 / 十六进制 / 二进制互转;需要任意精度的加减乘除、幂、取模;需要 RSA 那类基础件(modExp / modInverse / prime(bits) / gcd)。
[*]自己做字符串大数运算已经没必要 —— 除非你要的操作它没提供。


完整文档(14 条排错速查、四种方案选型对比表、29 个方法的逐条签名表)见包内 使用说明.txt;实测证据见包内 evidence/ 目录。

本楼层于 2026-10-10 随 PBIDEA 版本更新重写;附件同步替换为按新版本重新编译、重新验证过的 bigint1010 交付包。
页: [1]
查看完整版本: PBIDEA:uo_biginteger 大整数全解 —— 29 个方法、任意精度算术与数论(PB10 基准 · PB12.5 双轨实测)

免责声明:
本站所发布的一切破解补丁、注册机和注册信息及软件的解密分析文章仅限用于学习和研究目的;不得将上述内容用于商业或者非法用途,否则,一切后果请用户自负。本站信息来自网络,版权争议与本站无关。您必须在下载后的24个小时之内,从您的电脑中彻底删除上述内容。如果您喜欢该程序,请支持正版软件,购买注册,得到更好的正版服务。如有侵权请邮件与我们联系处理。

Mail To:Admin@SybaseBbs.com