使用solidity实现一个printf函数
字符串格式化函数在应用开发时经常用到,而在合约中使用场景似乎没有那么多,然而要实现这个函数,则需要先解决一些问题,本文就探讨一下如何来解决这些问题。先看其用法:
printf("name=%s, age=%u, height=%u", n, a, h);
第一个问题,就是 printf 函数的参数类型和个数是动态变化的,然而 solidity 编译器目前并没有提供这种支持,如何解决这个问题呢?
方法一使用数组。使用数组是一种比较直接的想法,但是数组中的元素类型必须相同,这样的话,怎么传字符串呢?在计算机中,一切都是数据,可以考虑将字符串转为数值来传递,对于以太坊,一个 uint 是 256 位,32 个字节,拿出一位来保存长度,可以用 uint 表示最长 31 个字符的字符串,代码如下:
方法二利用内置函数。虽然 solidity 不支持定义可变参数的函数,但是一些内置函数可以,例如 abi.encode(),可以传入可变参数,并将这些参数编码成字节数组。然后在 printf 函数里面,按照对应的方式解码就可以了。下面是解码 uint 和 string 的代码。
readAbiUInt()用于从 abi.encode()编码后的字节数组的指定位置读取一个 uint,其中被注释掉的代码是基本实现,通过循环读取数据按规则解码实现,但是此方法效率较低,因此可以更改勇敢下面的方式实现,提高效率。
readAbiString()则是从 abi.encode()编码后的字节数组的指定位置读取一个 string。至于为什么要这么实现,则是由于 abi.encode()的编码规则确定的,如果有需要,我后面再写一篇文章详细介绍其编码规则。
通过以上两种方法,可以解决传参数的问题了,接下来就是要解析格式化字符串了,这涉及到一个算法,可以考虑使用“有限状态机”的方式来实现。
有限状态机看起来很神秘,但其实逻辑非常简单,在解析时,按照需要解析的逻辑定义一些状态,然后确定每种状态遇到什么条件就会进入另外一个状态,如此就可以将一个字符串按照指定的逻辑进行解析。
下面是格式化字符串需要定义的几个状态:0:初始状态,解析开始,或者完成一个格式描述串的处理,就回到初始状态。1:遇到描述符开始%。在 0 状态下,遇到%就进入此状态。2:描述符结束。在 1 状态下,如果遇到 u,d,s,%,就算描述符结束,这四个描述符分别对应:无符号整数、整数、字符串、%转义。分别按照对应的位置从前面传入的参数表中按照指定的含义读出数据,写入到输出,然后状态回到 0。
下面是实现代码
/// @dev Format to memory buffer
/// @param buffer Target buffer
/// @param index Start index in buffer
/// @param format Format string
/// @param abiArgs byte array of arguments encoded by abi.encode()
/// @return New index in buffer
function sprintf(
bytes memory buffer,
uint index,
bytes memory format,
bytes memory abiArgs
) internal pure returns (uint) {
uint i = 0;
uint pi = 0;
uint ai = 0;
uint state = 0;
uint w = 0;
while (i < format.length) {
uint c = uint(uint8(format[i]));
// 0. Normal
if (state == 0) {
// %
if (c == 37) {
while (pi < i) {
buffer[index++] = format[pi++];
}
state = 1;
}
++i;
}
// 1. Check if there is -
else if (state == 1) {
// %
if (c == 37) {
buffer[index++] = bytes1(uint8(37));
pi = ++i;
state = 0;
} else {
state = 3;
}
}
// 3. Find width
else if (state == 3) {
while (c >= 48 && c <= 57) {
w = w * 10 + c - 48;
c = uint(uint8(format[++i]));
}
state = 4;
}
// 4. Find format descriptor
else if (state == 4) {
uint arg = readAbiUInt(abiArgs, ai);
// d
if (c == 100) {
if (arg >> 255 == 1) {
buffer[index++] = bytes1(uint8(45));
arg = uint(-int(arg));
} else {
buffer[index++] = bytes1(uint8(43));
}
c = 117;
}
// u
if (c == 117) {
index = writeUIntDec(buffer, index, arg, w == 0 ? 1 : w);
}
// x/X
else if (c == 120 || c == 88) {
index = writeUIntHex(buffer, index, arg, w == 0 ? 1 : w, c == 88);
}
// s/S
else if (c == 115 || c == 83) {
index = writeAbiString(buffer, index, abiArgs, arg, w == 0 ? 31 : w, c == 83 ? 1 : 0);
}
// f
else if (c == 102) {
if (arg >> 255 == 1) {
buffer[index++] = bytes1(uint8(45));
arg = uint(-int(arg));
}
index = writeFloat(buffer, index, arg, w == 0 ? 8 : w);
}
pi = ++i;
state = 0;
w = 0;
ai += 32;
}
}
while (pi < i) {
buffer[index++] = format[pi++];
}
return index;
}
这样,就可以实现一个 printf 函数了,又找回了 C 编程的感觉,虽说使用场景不多,但是并不代表没有,比如当我们需要按照某些规则来给一系列合约创建的代币生成名字的时候,就可以用这个方法了。
本文引用的代码来自:https://github.com/FORT-Protocol/FORT-V1.1/blob/nest4.0/contracts/libs/StringHelper.sol#L443[2]
猜你喜欢
比特币提现会被银行查吗?
比特币提现会被银行查吗? 首先,根据《中华人民共和国反洗钱法》、《金融机构大额交易和可疑交易报告管理办法》、《金融机构报告涉嫌恐怖融资的可疑交易管理办法》等法律法规的相关规定,银行会对大额资金的流动做监控,主要是审查来源是否合法,是否涉嫌洗钱。
2022-05-21
比特币暴跌50%!30岁老公玩比特币输了好多钱
比特币暴跌50%!30岁老公玩比特币输了好多钱 过去的一周里,作为一个游曳在币圈边缘的键盘侠,见识了币圈度日如年的跌宕后,仍可以笑看潮起潮落。
2022-05-21
UST爆雷之后,USDT也要爆雷了?
这几天的行情,证明了良心哥的推测非常准确。 首先是5月10日分析luna背后是被人开了黑枪,并且持续看空luna。 次日消息实锤,luna再次跌了个99%。 昨天分析说,luna的死亡螺旋会带崩大盘。
2022-05-21
Luna币7天蒸发2000亿,但更怕的是熊市即将到来!
心哥昨天虽然不知道这里边的细节,但依然非常确定的告诉大家,这是一场狙击战,找的就是这个空档,打出来的子弹是要人命的。 另外排队枪毙这个场景,估计今天很多人也领教了。
2022-05-21
一天蒸发400亿人民币,Luna是如何被狙击的?
你们也都知道良心哥炒币是个渣渣,但良心哥的判断大体还是准确的。 可能这就是从业时间久了的盘感吧。 有人说luna的暴跌,ust抛锚,都他吗赖孙宇晨。 从5月5号孙宇晨宣布进军算法稳定币之后,大盘就崩了
2022-05-21
上一篇
搞定EVM中的内存数据区
下一篇
创建并部署ERC20代币
评论