Windows中增加StringCbPrintf2函数以使用资源字符串
StringCbPrintf函数(旧版本的printf、wprintf等函数)是字符串格式化函数,使用该函数可以将一组字符串和数值按指定的格式进行显示。但该函数的字符串参数不能使用资源字符串ID,这对于编写国际化软件不太友好。本文介绍如何增加一个StringCbPrintf2格式化显示函数,使格式化函数能接受资源字符串ID。
在《Windows菜单本地化方法》一文中提到过,软件的本地化主要是字符串本地化,常规的做法是通过修改资源消息表和字符串表来实现的。在《使用FormatMessage函数实现多语言消息》一文中介绍过消息表的使用方法,资源消息表的字符串是用于格式化显示的,但消息表不能替代字符串表,字符串表同样不能替代消息表。增加一个StringCbPrintf2格式化显示函数,使格式化函数能接受资源字符串ID,这样就不需要消息表,减少了很多的麻烦。
1. 安全字符串的格式化显示函数
如果使用C/C++语言,就不需要了解本节内容。因为本文代码是使用Masm64语言写的,而Masm64中并没有安全字符串函数,所以需要自己编写安全字符串函数,这其实是很简单的事,可以有多种方法,以下介绍使用Strsafe.lib库编写安全字符串的格式化显示函数的方法。
先要获取两个文件Strsafe.lib和Strsafe.h,本人在Microsoft Visual Studio的安装目录中获取,该两个文件分别位于SDK\ScopeCppSDK\vc15\SDK\lib和SDK\ScopeCppSDK\vc15\SDK\include\shared目录内。
Strsafe.lib是静态库,它只有安全字符串的工作函数,安全字符串函数是通过调用工作函数来实现的(具本见Strsafe.h)。使用Strsafe.lib库来编写Masm64的安全字符串函数,需要一个包含文件StrSafe.inc,列出Strsafe.lib库中的工作函数。
StrSafe.inc文件的内容如下:
includelib StrSafe.lib
;---工作函数---
externdef StringLengthWorkerA:PROC
externdef StringLengthWorkerW:PROC
externdef UnalignedStringLengthWorkerW:PROC
externdef StringExValidateSrcA:PROC
externdef StringExValidateSrcW:PROC
externdef StringValidateDestA:PROC
externdef StringValidateDestAndLengthA:PROC
externdef StringValidateDestW:PROC
externdef StringValidateDestAndLengthW:PROC
externdef StringExValidateDestA:PROC
externdef StringExValidateDestAndLengthA:PROC
externdef StringExValidateDestW:PROC
externdef StringExValidateDestAndLengthW:PROC
externdef StringCopyWorkerA:PROC
externdef StringCopyWorkerW:PROC
externdef StringVPrintfWorkerA:PROC
externdef StringVPrintfWorkerW:PROC
externdef StringGetsWorkerA:PROC
externdef StringGetsWorkerW:PROC
externdef StringExHandleFillBehindNullA:PROC
externdef StringExHandleFillBehindNullW:PROC
externdef StringExHandleOtherFlagsA:PROC
externdef StringExHandleOtherFlagsW:PROC
以下是StringCbVPrintfW和StringCbPrintfW函数的例子。其它函数的写法类同,具体见Strsafe.h中的宏函数。
;=======================================================
;格式化字符串
;入: pszDest=接收缓冲区地址
; cbDest=接收缓冲区长度(以WORD为单位)
; pszFormat=格式化按制串地址
; pArgList=插入值列表数组地址
;出: RAX=S_OK: 成功
;=======================================================
StringCbVPrintfW proc pszDest:QWORD,cbDest:QWORD,\
pszFormat:QWORD,pArgList:QWORD
invoke StringValidateDestW,pszDest,cbDest,STRSAFE_MAX_CCH ;查检参数的安全性
cmp eax,S_OK
jnz ss_er ;不安全
invoke StringVPrintfWorkerW,pszDest,cbDest,NULL,pszFormat,pArgList
ret
ss_er:
mov rcx,pszDest
test rcx,rcx
jz ss_0
cmp cbDest,0
jle ss_0
xor dx,dx
mov [rcx],dx
ss_0:
ret
StringCbVPrintfW endp
;=======================================================
;格式化字符串
;入: pszDest=接收缓冲区地址
; cbDest=接收缓冲区长度(以WORD为单位)
; pszFormat=格式化按制串地址
; ArgList=插入值列表(可变参数)
;出: RAX=S_OK: 成功
;=======================================================
StringCbPrintfW proc pszDest:QWORD,cbDest:QWORD,\
pszFormat:QWORD,ArgList:QWORD
invoke StringCbVPrintfW,rcx,rdx,r8,ADDR ArgList
ret
StringCbPrintfW endp
注意,在StringCbPrintfW函数中的ArgList参数为可变参数,在《Masm64中函数可变参数VARARG的实现方法》一文中曾经讲过,在Masm64中可变参数类型使用QWORDS自定义宏,但如果函数的参数个数为4个或4个以上,则变可参数会存入到栈中,这种情况下可以不使用QWORDS自定义宏,而直接使用QWORD类型即可。
在StringCbVPrintfW函数中使用了两个函数StringValidateDestW和StringVPrintfWorkerW,这两个函数为Strsafe.lib库中的工作函数。
字符串格式化函数有两种形式,函数名带"V"的(如StringCbVPrintfW)函数的格式化插入参数为QWORD型数组地址,函数名不带"V"的(如StringCbPrintfW)函数的格式化插入参数为QWORD型值列表。
2. 需要的几个基本函数
所需的基本函数多数已在《Windows菜单本地化方法》一文中介绍过,这里新增3个函数Rsrc_GetStringToMemA、Rsrc_GetStringToMemW和Printf2_Free,一并列出。
;=============================================================
;装载一个指定语言的字符串资源---Unicode
;入: hInst=包含字符串资源的实例句柄
; =0: 当前进程
; uId=字符串资源ID
; uLangId=语言ID
; pBuf=字符接收缓冲区地址
; zSize=字符接收缓冲区长度(以WORD为单位)
; 如果缓冲区太小,则返回的串被截断。
; =0: 只返回所需内存数
;出: RAX=字符串的字符个数,不包括结尾符NULL。
; 如果zSize=0,则为所需内存数,包括结尾符NULL,
; (以WORD为单位)。
; =0: 未发现
;=============================================================
Rsrc_LoadStringExW proc hInst:QWORD,uId:QWORD,uLangId:QWORD,\
pBuf:QWORD,zSize:DWORD
test r9,r9 ;pBuf
jnz ss_1
mov zSize,0
jmp ss_2
ss_1:
xor eax,eax
mov [r9],ax ;pBuf初始化
ss_2:
;--------------
; 搜索组
;--------------
;---将字符串ID转换为组序号---
and rdx,0ffffh
shr rdx,4 ;/16
inc rdx
mov r8,rdx
invoke FindResourceExW,hInst,RT_STRING,r8,uLangId
test rax,rax
jz ss_0
invoke LoadResource,hInst,rax ;装入字符串组数组
invoke LockResource,rax ;锁定内存(不需要解锁)
;-----------------
;在组内搜索字符串
;-----------------
mov rdx,uId
and edx,0fh ;该字符串在组内的序号
jz ss_ok ;组首地址
ss_lp1:
xor rcx,rcx
mov cx,[rax] ;Unicide串的字符个数
inc ecx ;长度单元的WORD
shl ecx,1
add rax,rcx ;指向下一个串
dec edx
jnz ss_lp1
ss_ok:
xor r9,r9
mov r9w,[rax]
test r9w,r9w
jz ss_no
;---复制字符串---
cmp zSize,0
jz ss_retn
add rax,2
mov edx,zSize
invoke StringCbCopyNW,pBuf,rdx,rax,r9 ;串复制
mov rax,rcx
shr rax,1
ret
ss_retn: ;返回所需内存长度
xor rcx,rcx
mov cx,[rax]
mov rax,rcx
inc rax
ret
ss_no: ;未发现
xor rax,rax
mov rdx,pBuf
test rdx,rdx
jz ss_0
mov [rdx],ax
ss_0:
ret
Rsrc_LoadStringExW endp
;===========================================================
;装载一个指定语言的字符串资源---ANSI
;入: hInst=包含字符串资源的实例句柄
; =0: 当前进程
; uId=字符串资源ID
; uLangId=语言ID
; pBuf=字符接收缓冲区地址
; zSize=字符接收缓冲区字节长度
; =0: 只返回所需内存数
;出: RAX=字符串的字节长度,不包括结尾符NULL。
; 如果 zSize=0,则为所需内存字节长度,包括结尾符NULL。
; =0: 未发现或缓冲区溢出
;===========