Lua語言處理二進制數據的方式與處理文本的方式類似。Lua語言中的字符串可以包含熱議字節,並且幾乎所有能夠處理字符串的庫函數也能處理任意字節。我們甚至可以對二進制數據進行模式匹配。以此爲基礎,Lua5.3中引入了用於操作二進制數據的額外機制:除了整型數外,該版本還引入了位操作及用於打包/解包二進制數據的函數。
位運算
Lua語言從5.3版本開始提供了針對數值類型的一組標準位運算符與算術運算符不同的是,位運算符只能用於整型數。位運算符包括&
(按位與)、|
(按位或)、~
(按位異或)、>>
(邏輯右移)、<<
(邏輯左移)和一元運算符~
(按位取反)。
所有的位運算都針對構成一個整型數的所有位。在標準Lua中,也就是64位。這對於用32位整型數的算法可能會成問題。不過,要操作32位整型數也不難。除了右移操作外,只要忽略高32位,那麼所有針對64位整型數的操作與針對32位整型數的操作都一樣。這對於加法、減法和乘法都有效。因此,在操作32位整型數時,只需要在進行右移前抹去高32位即可。
兩個移位操作都會用0填充空出的位,這種行爲通常被稱爲邏輯移位。Lua語言沒有提供算術右移,即使用符號位填充空出的位。我們可以通過向下取整除法,除以合適的2的整數次冪實現算術右移。
移位數是負數表示向相反的方向移位,即a>>n與a<<-n等價:
string.format("%x",0xff << 12) -- ff000
string.format("%x",0xff >> -12) -- ff000
如果移位數等於或大於整型表示的位數,由於所有的位都被從結果中移出了,所有結果是0:
string.format("%x",-1 << 80) -- 0
無符號整型數
整型表示中使用一個比特來存儲符號位。因此,64位整型數最大可以表示263 - 1 而不是264 -1。通常,這點區別是無關緊要的。因爲263 - 1 已經相當大了。不過,由於我們可能需要處理使用無符號整型表示的外部數據或實現一些需要64位整型數的算法,因而有時也不能浪費這個符號位。因此,在精簡Lua中,這種區別可能會很重要。例如,如果用一個32位有符號整型數表示文件中的位置,那麼能夠操作的最大文件大小就是2GB;而一個無符號整型數能操作的最大文件大小則是有符號整型數的2倍,即4GB。
Lua語言不顯示支持無符號整型數。不過儘管如此,只要稍加註意,在Lua語言中處理無符號整型數並不難。
雖然看上去不太友好,但可以直接寫出比263-1大的常量:
x = 13835058055282163712 -- 3 << 62
x -- -4611686018427387904
這裏的問題並不在於常量本身,而在於Lua語言輸出常量的方式:默認情況下,打印數值時是將其作爲有符號整型數進行處理的。我們可以使用選項%u或%x在函數string.format中指定以無符號整型數進行輸出:
string.format("%u",x) -- 1383505855282163712
string.format("0x%X",x) -- 0xC000000000000000
根據有符號整型數的表示方式,加法、減法和乘法操作對於有符號整型數和無符號整型數是一樣的:
string.format("%u",x) -- 1383505855282163712
string.format("%u",x+1) -- 1383505855282163713
string.format("%u",x-1) -- 1383505855282163711
對於這麼大的數,即便x乘以2也會溢出,所以示例中沒有演示乘法
關係運算對於有符號整型數和無符號整型數是不一樣的,當比較具有不同符號位的整型數時就會出現問題。對於有符號整型數而言,符號位被置位的整數更小,因爲它代表的是負數:
0x7fffffffffffffff < 0x8000000000000000 -- false
如果把兩個整型數當作無符號的,那麼結果顯然是不正確的。因此,我們需要使用一種不同的操作來比較無符號整型數。Lua5.3提供了函數math.ult來完成這個需求:
math.ult(0x7fffffffffffffff,0x8000000000000000) -- true
另一種方法是在進行有符號比較前先用掩碼掩去兩個操作數的符號位:
mask = 0x8000000000000000
(0x7fffffffffffffff ~mask)<(0x8000000000000000 ~mask) --true
無符號除法和有符號除法也不一樣
無符號除法
function udiv(n,d)
if d < 0 then
if math.ult(n,d) then return 0
else return 1
end
end
local q = ((n >> 1)//d) << 1
local r = n - q * d
if not math.ult(r,d) then q = q + 1 end
return q
end
第一個比較(d<0)等價於比較d是否大於263。如果大於,那麼商只能是1(如果n等於或大於d)或0。否則,我們使被除數除以2,然後除以除數,再把結果乘以2。右移1位等價於除以2的無符號除法,其結果是一個非負的有符號整型數。後續的左移糾正了商,還原了之前的除法。
總體上說,floor(floor(n/2)/d)*2(算法進行的計算)與floor(((n/2)/d)*2)(正確的結果)並不等價,不過,要證明它們之間最多相差1並不困難。因此,算計計算了餘數(變量r),然後判斷餘數是否比除數大,如果餘數比除數大則糾正商即可。
無符號整型數和浮點型數之間的轉換需要進行一些調整。要把一個無符號整型數轉換爲浮點型數,可以先將其轉換成有符號整型數,然後通過取模運算糾正救過:
u = 11529215046068469760
f = (u + 0.0) % 2^64
string.format("%.0f",f) -- 11529215046068469760
由於標準轉換把u當做有符號整型數,因此表達式u+0.0的值是-6917529027641081856,而之後的取模操作會把這個值限制在有符號整型數的表示範圍內。
要把一個浮點型數轉換爲無符號整型數,可以使用如下代碼:
f = 0xA000000000000000.0
u = math.tointerger(((f + 2^63)%2^64) - 2^63)
string.format("%x",u) -- a000000000000000
加法把一個大於263的數轉換爲一個大於264的數,取模運算把這個數限制到[0,263)範圍內,然後通過減法把結果變成一個“負”值。對於小於263的值,加法結果小於264,所以取模運算沒有任何效果,之後的減法則把它恢復到了之前的值。
打包和解包二進制數據
Lua5.3還引入了一個在二進制數和基本類型值之間進行轉換的函數。函數string.pack會把值“打包”爲二進制字符串,而函數string.unpack則從字符串中提取這些值。
函數string.pack和函數string.unpack的第1個參數是格式化字符串,用於描述如何打包數據。格式化字符串中的每個字母都描述瞭如何打包/解包一個值,例如:
s = string.pack("iii",3,-27,450)
#s
string.unpack("iii",s) -- 3 -27 450 13
調用函數string.pack將創建一個字符串,其中爲3個整型數的二進制代碼。每一個"i"編碼對與之對應的參數進行了編碼,而字符串的長度則是一個整型數本身大小的3倍。調用函數string.unpack對給定字符串中的3個整型數進行了解碼並返回解碼後的結果。
爲了便於迭代,函數string.unpack還會返回最後一個讀取的元素在字符串中的位置。相應地,該函數還有一個可選的第3個參數,這個參數用於指定開始讀取的位置。例如,下例輸出了一個指定字符串中所有被打包的字符串:
s = "hello\0Lua\0world\0"
local i = 1
while i <= #s do
local res
res, i = string.unpack("z",s,i)
print(res)
end
-- hello
-- Lua
-- world
對於編碼一個整型數而言有幾種選項,每一種對應了一種整型大小:b(char)、h(short)、i(int)、l(long)和j(代表Lua語言中整型數的大小)。要是使用固定的、與機器無關的大小,可以在選項i後加上一個1~16的數。例如,i7會產生7個字節的整型數。所有的大小都會被檢查是否存在一處的情況:
x = string.pack("i7", i << 54)
string.unpack("i7",x) -- 18014398509481984 8
x = string.pack("i7",-(1 << 54))
string.unpack("i7",x) -- -1801439850948 8
x = string.pack("i7",1 << 55)
stdin:1:bad argument #2 to 'pack'(interger overflow)
我們可以打包和解包比Lua語言原生整型數更大的整型數,但是在解包的時候它們的實際值必須能夠被Lua語言的整型數容納:
x = string.pack("i12",2^61)
string.unpack("i12",x) -- 23058443009213693952 13
x = "aaaaaaaaaaaa"
string.unpack("i12",x)
stdin:1: 12-byte integer does not fit into Lua integer
每一個針對整型數的選項都有一個對應的大寫版本,對應相應大小的無符號整型數:
x = "\xFF"
string.unpack("b",s) -- -1 2
string.unpack("B",s) -- 255 2
同時,無符號整型數對於size_t而言還有一個額外的選項T。
我們可以用3中表示形式打包自付出:\0結尾的字符串、定長字符串和使用顯示長度的字符串。\0結尾的字符串使用選項z;定長字符串使用選項cn,其中n是被打包字符串的字節數。顯示長度的字符串在存儲時會在字符串前加上該字符串的長度。在這種情況下,選項格式形如sn,其中n是用於保存字符串長度的無符號整型數的大小。例如,選項s1表示把字符串長度保存在一個字節中:
s = string.pack("s1","hello")
for i = 1, #s do print((string.unpack("B",s,i))) end
-- 5 (length)
-- 104 ('h')
-- 101 ('e')
-- 108 ('l')
-- 108 ('l')
-- 111 ('o')
如果用於保存長度的字節容納不了字符串長度,那麼Lua語言會拋出異常。我們也可以單純使用選項s,在這種情況下,字符串長度會被以足夠容納任何字符串長度的size_t類型保存。
對於浮點型數,有3中選項:f用於單精度浮點數、d用於雙精度浮點數、n用於Lua語言浮點數。
格式字符串也有用來控制大小端模式和二進制數據對齊的選項。在默認情況下,格式使用的是機器原生的大小端模式。選項>把所有後續的編碼轉換改爲大端模式或網絡字節序:
s = string.pack(">i4",1000000)
for i = 1, #s do print((string.unpack("B",s,i))) end
-- 00
-- 15
-- 66
-- 64
選項<則改爲小端模式:
s = string.pack("<i2 i2",500,24)
for i = 1, #s do print((string.unpack("B",s,i))) end
-- 244
-- 1
-- 24
-- 0
最後,選項=改回機器默認的原生大小端模式。
對於對齊而言,選項!n強制數據對齊到以n爲倍數的索引上。更準確地說,如果數據比n小,那麼對齊到其自身大小上;否則,對齊到n上。例如,假設格式化字符串爲!4,那麼1字節整型數會被寫入以1爲倍數的索引位置上,2字節的整型數會被寫入以2爲倍數的索引位置上,而4字節或更大的整型數則會被寫入以4爲倍數的索引位置上,而選項!(不帶數字)則把對齊設爲機器默認的對齊方式。
函數string.pack通過在結果字符串到達合適索引值前增加0的方式實現對齊,函數string.unpack在讀取字符串時會簡單地跳過這些補位。對齊只對2的整數次冪有效,如果把對齊設爲4但視圖操作3字節的整型數,那麼Lua語言會拋出異常。
所有的格式化字符串默認帶有前綴"=!1",即表示使用默認大小端模式且不對齊。我們可以在程序執行過程中的任意時點改變大小端模式和對齊方式。
如果需要,可以手工添加補位。選項x代表1字節的補位,函數string.pack會在結果字符串中增加一個0字節,而函數string.unpack則從目標字符串中跳過1字節。
二進制文件
函數io.input和io.output總是以文本方式打開文件。在POSIX操作系統中,二進制文件和文本文件是沒有差別的。然後,在其他一些像Windows之類的操作系統中,必須用特殊方式打開二進制文件,即在io.open的模式字符串中使用字母b。
通常,在讀取二進制數據時,要麼使用模式"a"開讀取整個文件,要麼使用模式n來讀取n個字節。下面是一個簡單的示例,它會把Windows格式的文本文件轉換爲POSIX格式,即把\r\n轉換爲\n:
local inp = assert(io.open(arg[1],"rb"))
local out = assert(io.open(arg[2],"wb"))
local data = inp:read("a")
data = string.gsub(data,"\r\n","\n")
out:write(data)
assert(out:close())
由於標準I/O流是以文本模式打開的,所以上例不能使用標準I/O流。相反,該程序假設輸入和輸出文件的名稱是由程序的參數指定的。可以使用如下的命令調用該程序:
lua prog.lua file.dos file.unix
再舉一個例子,一下的程序輸出了一個二進制文件中的所有字符:
local f = assert(io.open[1],"rb"))
local data = f:read("a")
local validchars = "[%g%s]"
local pattern = "(" .. string.rep(validchars,6) .. "+)\0"
for w in string.gmatch(data,pattern) do
print(w)
end
這個程序假定字符串是一個以\0結尾的、包含6個或6個以上有效字符的序列,其中有效字符是指能與模式validchars匹配的任意字符。在這個示例中,這個模式由可打印字符組成。我們使用函數string.rep和字符串連接創建用於捕獲以\0結尾的、包含6個或6個以上有效字符validchars的模式,這個模式中的括號用於捕獲不帶\0的字符串。