Skip to content

Instantly share code, notes, and snippets.

@starwing
Last active December 19, 2015 01:29
Show Gist options
  • Save starwing/5876042 to your computer and use it in GitHub Desktop.
Save starwing/5876042 to your computer and use it in GitHub Desktop.
#ifndef lua_bufflib_h
#define lua_bufflib_h
#define LUA_LIB
#include <lua.h>
#include <lauxlib.h>
#define BUFFER_LIBNAME "buffer"
#define BUFFER_TYPENAME "Lua StringBuffer"
typedef struct Buffer {
char *b;
size_t n;
size_t size;
lua_State *L; /* for alloc error */
char initb[LUAL_BUFFERSIZE];
} Buffer;
#define buffer_addchar(B,c) \
((void)((B)->n < (B)->size || buffer_prepare((B), 1)), \
((B)->b[(B)->n++] = (c)))
#define buffer_addsize(B,s) ((B)->n += (s))
Buffer *buffer_new(lua_State *L);
void buffer_init (Buffer *b, lua_State *L);
void buffer_reset (Buffer *b);
char *buffer_prepare (Buffer *b, size_t sz);
void buffer_addlstring (Buffer *b, const char *s, size_t sz);
void buffer_addstring (Buffer *b, const char *s);
void buffer_addvalue (Buffer *b, lua_State *L, int idx);
void buffer_addvalues (Buffer *b, lua_State *L, int i, int j);
void buffer_pushresult (Buffer *b, lua_State *L);
Buffer *buffer_check (lua_State *L, int idx);
Buffer *buffer_test (lua_State *L, int idx);
const char *buffer_testlstring (lua_State *L, int idx, size_t *psz);
const char *buffer_checklstring (lua_State *L, int idx, size_t *psz);
LUALIB_API int luaopen_buffer (lua_State *L);
#endif /* lua_bufflib_h */
/* lua5.1 compat */
#if LUA_VERSION_NUM < 502
#define luaL_newlibtable(L,l) \
lua_createtable(L, 0, sizeof(l)/sizeof((l)[0]) - 1)
#define luaL_newlib(L,l) (luaL_newlibtable(L,l), luaL_setfuncs(L,l,0))
static int lua_relindex (lua_State *L, int idx, int onstack) {
return (idx > 0 || idx <= LUA_REGISTRYINDEX)
? idx
: idx - onstack;
}
static void luaL_setmetatable(lua_State *L, const char *tname) {
lua_getfield(L, LUA_REGISTRYINDEX, tname);
lua_setmetatable(L, -2);
}
static int luaL_getsubtable(lua_State *L, int idx, const char *fname) {
lua_getfield(L, idx, fname);
if (lua_istable(L, -1)) return 1; /* table already there */
else {
lua_pop(L, 1); /* remove previous result */
lua_newtable(L);
lua_pushvalue(L, -1); /* copy to be left at top */
lua_setfield(L, lua_relindex(L, idx, 2), fname); /* assign new table to field */
return 0; /* false, because did not find table there */
}
}
static const char *luaL_tolstring (lua_State *L, int idx, size_t *len) {
if (!luaL_callmeta(L, idx, "__tostring")) { /* no metafield? */
switch (lua_type(L, idx)) {
case LUA_TNUMBER:
case LUA_TSTRING:
lua_pushvalue(L, idx);
break;
case LUA_TBOOLEAN:
lua_pushstring(L, (lua_toboolean(L, idx) ? "true" : "false"));
break;
case LUA_TNIL:
lua_pushliteral(L, "nil");
break;
default:
lua_pushfstring(L, "%s: %p", luaL_typename(L, idx),
lua_topointer(L, idx));
break;
}
}
return lua_tolstring(L, -1, len);
}
static void *luaL_testudata (lua_State *L, int ud, const char *tname) {
void *p = lua_touserdata(L, ud);
if (p != NULL) { /* value is a userdata? */
if (lua_getmetatable(L, ud)) { /* does it have a metatable? */
luaL_getmetatable(L, tname); /* get correct metatable */
if (!lua_rawequal(L, -1, -2)) /* not the same? */
p = NULL; /* value is a userdata with wrong metatable */
lua_pop(L, 2); /* remove both metatables */
return p;
}
}
return NULL; /* value is not a userdata with a metatable */
}
static void luaL_setfuncs (lua_State *L, const luaL_Reg *l, int nup) {
luaL_checkstack(L, nup, "too many upvalues");
for (; l->name != NULL; l++) { /* fill the table with given functions */
int i;
for (i = 0; i < nup; i++) /* copy upvalues to the top */
lua_pushvalue(L, -nup);
lua_pushcclosure(L, l->func, nup); /* closure with those upvalues */
lua_setfield(L, -(nup + 2), l->name);
}
lua_pop(L, nup); /* remove upvalues */
}
#endif
/* implement */
#include <assert.h>
#include <stdlib.h>
#include <string.h>
#define MAX_SIZET ((size_t)(~(size_t)0)-2)
void buffer_pushresult(Buffer *b, lua_State *L) {
lua_pushlstring(L, b->b, b->n);
}
static Buffer *buffer_new_nocheck(lua_State *L) {
Buffer *b = (Buffer*)lua_newuserdata(L, sizeof(Buffer));
luaL_setmetatable(L, BUFFER_TYPENAME);
buffer_init(b, L);
return b;
}
Buffer *buffer_new(lua_State *L) {
Buffer *b = buffer_new_nocheck(L);
if (!lua_getmetatable(L, -1)) {
luaL_getsubtable(L, LUA_REGISTRYINDEX, "_LOADED");
luaopen_buffer(L); /* push two values to stack */
lua_setfield(L, -3, BUFFER_LIBNAME);
lua_setmetatable(L, -3);
}
lua_pop(L, 1); /* pop metatable or _LOADED table */
return b;
}
void buffer_init(Buffer *b, lua_State *L) {
b->b = b->initb;
b->n = 0;
b->size = LUAL_BUFFERSIZE;
}
void buffer_reset(Buffer *b) {
if (b->b != b->initb)
free(b->b);
buffer_init(b, b->L);
}
char *buffer_prepare(Buffer *b, size_t sz) {
if (b->size - b->n < sz) {
size_t expected = b->n + sz;
size_t newsize = b->size;
char *newptr = b->b != b->initb ? b->b : NULL;
while (newsize <= expected && newsize < MAX_SIZET/2)
newsize *= 2;
if (newsize > MAX_SIZET)
luaL_error(b->L, "buffer too big");
newptr = realloc(newptr, newsize);
assert(newsize != 0);
if (newptr == NULL)
luaL_error(b->L, "out of memory");
b->b = newptr;
b->size = newsize;
}
return &b->b[b->n];
}
void buffer_addlstring(Buffer *b, const char *s, size_t sz) {
char *p = buffer_prepare(b, sz + 1);
memcpy(p, s, sz);
p[sz] = '\0';
b->n += sz;
}
void buffer_addstring(Buffer *b, const char *s) {
buffer_addlstring(b, s, strlen(s));
}
void buffer_addvalue(Buffer *b, lua_State *L, int idx) {
size_t n;
const char *s = luaL_tolstring(L, idx, &n);
buffer_addlstring(b, s, n);
lua_pop(L, 1);
}
void buffer_addvalues(Buffer *b, lua_State *L, int i, int j) {
if (i < 0 || j < 0) {
int top = lua_gettop(L);
i = i < 0 ? top + i + 1 : i;
j = j < 0 ? top + j + 1 : j;
}
while (i <= j)
buffer_addvalue(b, L, i++);
}
/* lua interface */
static int typeerror(lua_State *L, int idx, const char *tname) {
lua_pushfstring(L, "%s expected, got %s", tname, luaL_typename(L, idx));
return luaL_argerror(L, idx, lua_tostring(L, -1));
}
static const char *buffer_or_string(lua_State *L, int idx, size_t *psz, int check) {
const char *s = lua_tolstring(L, idx, psz);
if (s != NULL)
return s;
if (buffer_test(L, idx)) {
Buffer *b = (Buffer*)lua_touserdata(L, idx);
if (psz) *psz = b->n;
return b->b;
}
if (check)
typeerror(L, idx, "buffer/string");
return NULL;
}
Buffer *buffer_check (lua_State *L, int idx) {
return (Buffer*)luaL_checkudata(L, idx, BUFFER_TYPENAME);
}
Buffer *buffer_test (lua_State *L, int idx) {
return (Buffer*)luaL_testudata(L, idx, BUFFER_LIBNAME);
}
const char *buffer_testlstring (lua_State *L, int idx, size_t *psz) {
return buffer_or_string(L, idx, psz, 0);
}
const char *buffer_checklstring (lua_State *L, int idx, size_t *psz) {
return buffer_or_string(L, idx, psz, 1);
}
static int Lisbuffer(lua_State *L) {
lua_pushboolean(L, buffer_test(L, 1) != NULL);
return 1;
}
static int Ladd(lua_State *L) {
Buffer *b = buffer_check(L, 1);
buffer_addvalues(b, L, 2, -1);
lua_settop(L, 1);
return 1;
}
static int getrange(lua_State *L, int idx, size_t n, int *i, int *j) {
int oi = luaL_optint(L, idx, 1);
int oj = luaL_optint(L, idx+1, -1);
if (oi < 0) *i = oi + n + 1;
if (oj < 0) *j = oj + n + 1;
if (*i <= 0 || *j <= 0 || *i > n || *j > n) {
lua_pushfstring(L, "index out of bound (%d, %d)", oi, oj);
return luaL_argerror(L, 3, lua_tostring(L, -1));
}
return *i <= *j;
}
static int Laddpart(lua_State *L) {
Buffer *b = buffer_check(L, 1);
size_t n;
const char *s = buffer_checklstring(L, 2, &n);
int i, j;
if (getrange(L, 3, n, &i, &j))
buffer_addlstring(b, s + i - 1, j - i + 1);
lua_settop(L, 1);
return 1;
}
static int Ljoin(lua_State *L) {
Buffer *b = buffer_check(L, 1);
size_t n;
const char *s = buffer_checklstring(L, 2, &n);
int i, top = lua_gettop(L);
for (i = 3; i <= top; ++i) {
if (i != 3)
buffer_addlstring(b, s, n);
buffer_addvalue(b, L, i);
}
lua_settop(L, 1);
return 1;
}
static int Lpart(lua_State *L) {
size_t n;
const char *s = buffer_checklstring(L, 1, &n);
int i, j;
if (getrange(L, 2, n, &i, &j))
lua_pushlstring(L, s + i - 1, j - i + 1);
else
lua_pushliteral(L, "");
lua_settop(L, 1);
return 1;
}
static int Lnew(lua_State *L) {
Buffer *b = buffer_new_nocheck(L);
buffer_addvalues(b, L, 1, -2);
return 1;
}
static int Lgc(lua_State *L) {
Buffer *b = buffer_test(L, 1);
if (b != NULL)
buffer_reset(b);
return 0;
}
static int Lreset(lua_State *L) {
int i, top = lua_gettop(L);
for (i = 1; i <= top; ++i) {
Buffer *b = buffer_check(L, i);
buffer_reset(b);
}
return top;
}
static int Ltostring(lua_State *L) {
Buffer *b = buffer_test(L, 1);
if (b != NULL)
buffer_pushresult(b, L);
else
luaL_tolstring(L, 1, NULL);
return 1;
}
static int Llength(lua_State *L) {
size_t n;
buffer_checklstring(L, 1, &n);
lua_pushinteger(L, n);
return 1;
}
static int Lequal(lua_State *L) {
size_t n;
const char *s = buffer_checklstring(L, 1, &n);
int i, top = lua_gettop(L);
for (i = 2; i <= top; ++i) {
size_t ni;
const char *si = buffer_checklstring(L, i, &ni);
if (n != ni || memcmp(s, si, ni) != 0)
return 0;
}
lua_pushboolean(L, 1);
return 1;
}
static int ipairs_helper(lua_State *L) {
Buffer *b = buffer_check(L, 1);
int idx = luaL_checkint(L, 2);
if (idx >= b->n)
return 0;
lua_pushinteger(L, idx+1);
lua_pushinteger(L, b->b[idx]);
return 2;
}
static int ipairs_closure(lua_State *L) {
Buffer *b = buffer_check(L, 1);
int idx = luaL_checkint(L, 2);
int max = lua_tonumber(L, lua_upvalueindex(1));
if (idx >= max || idx >= b->n)
return 0;
lua_pushinteger(L, idx+1);
lua_pushinteger(L, b->b[idx]);
return 2;
}
static int Lipairs(lua_State *L) {
if (lua_gettop(L) != 1) {
int i, j;
Buffer *b = buffer_check(L, 1);
getrange(L, 2, b->n, &i, &j);
lua_pushinteger(L, j);
lua_pushcclosure(L, ipairs_closure, 1);
lua_pushvalue(L, 1);
lua_pushinteger(L, i-1);
return 3;
}
lua_pushcfunction(L, ipairs_helper);
lua_pushvalue(L, 1);
lua_pushinteger(L, 0);
return 3;
}
static int out_of_bound(lua_State *L, int idx, const char *what, int value) {
lua_pushfstring(L, "%s out of bound (%d)", what, value);
return luaL_argerror(L, idx, lua_tostring(L, -1));
}
static int realidx(lua_State *L, size_t n, int idx) {
int realidx = idx < 0 ? idx + n + 1 : idx;
if (realidx <= 0 || realidx > n)
return out_of_bound(L, 2, "index", idx);
return realidx;
}
static int Lindex(lua_State *L) {
Buffer *b = buffer_check(L, 1);
if (lua_type(L, 2) == LUA_TNUMBER) {
int idx = lua_tonumber(L, 2);
lua_pushinteger(L, b->b[realidx(L, b->n, idx)-1]);
return 1;
}
lua_pushvalue(L, lua_upvalueindex(1));
lua_pushvalue(L, 2);
lua_rawget(L, -2);
return 1;
}
static int Lnewindex(lua_State *L) {
Buffer *b = buffer_check(L, 1);
int ch, idx = luaL_checkint(L, 2);
switch (lua_type(L, 3)) {
case LUA_TSTRING:
ch = lua_tostring(L, 3)[0];
break;
case LUA_TNUMBER:
ch = lua_tonumber(L, 3);
if (ch < 0 || ch > 255)
return out_of_bound(L, 3, "char", ch);
break;
default:
return typeerror(L, 3, "number/string");
}
if (idx == b->n + 1)
buffer_addchar(b, ch);
else
b->b[realidx(L, b->n, idx)-1] = ch;
return 0;
}
static int Lstringop(lua_State *L) {
size_t n;
const char *s = buffer_checklstring(L, 1, &n);
lua_pushvalue(L, lua_upvalueindex(1));
lua_insert(L, 1);
lua_pushlstring(L, s, n);
lua_replace(L, 2);
lua_call(L, lua_gettop(L)-1, LUA_MULTRET);
return lua_gettop(L);
}
static void initlibs(lua_State *L) {
luaL_Reg libs[] = {
#define ENTRY(name) { #name, L##name }
ENTRY(add),
ENTRY(addpart),
ENTRY(ipairs),
ENTRY(isbuffer),
ENTRY(join),
ENTRY(length),
ENTRY(new),
ENTRY(part),
ENTRY(reset),
ENTRY(tostring),
#undef ENTRY
{ NULL, NULL }
};
luaL_newlib(L, libs);
lua_pushinteger(L, LUAL_BUFFERSIZE);
lua_setfield(L, -2, "buffersize");
}
static void inittype(lua_State *L) {
/* stack: libtable */
luaL_Reg libmeta[] = {
{ "__call", Ladd },
{ "__concat", Lnew },
{ "__eq", Lequal },
{ "__gc", Lgc },
{ "__ipairs", Lipairs },
{ "__len", Llength },
{ "__newindex", Lnewindex },
{ "__tostring", Ltostring },
#define ENTRY(name) { #name, L##name }
ENTRY(add),
ENTRY(addpart),
ENTRY(join),
ENTRY(part),
ENTRY(reset),
ENTRY(tostring),
#undef ENTRY
{ NULL, NULL }
};
if (luaL_newmetatable(L, BUFFER_TYPENAME)) {
luaL_setfuncs(L, libmeta, 0);
lua_pushvalue(L, -2);
lua_pushcclosure(L, Lindex, 1);
lua_setfield(L, -2, "__index");
}
lua_insert(L, -2); /* swap libtable and metatable */
}
static void intern_stdmodule(lua_State *L) {
/* stack: metatable libtable */
lua_getglobal(L, "string");
if (lua_isnil(L, -1)) {
lua_pop(L, 1);
return;
}
lua_pushnil(L);
while (lua_next(L, -2)) {
if (lua_type(L, -2) != LUA_TSTRING || !lua_isfunction(L, -1)) {
lua_pop(L, 1);
continue;
}
lua_pushcclosure(L, Lstringop, 1);
/* stack: libtable metatable string key proxy */
lua_pushvalue(L, -2); /* key */
lua_pushvalue(L, -2); /* proxy-function */
lua_rawset(L, -7); /* set to libtable */
lua_pushvalue(L, -2); /* key */
lua_pushvalue(L, -2); /* proxy-function */
lua_rawset(L, -6); /* set to metatable */
lua_pop(L, 1); /* pop proxy function */
}
lua_pop(L, 1); /* pop string table */
}
LUALIB_API int luaopen_buffer(lua_State *L) {
initlibs(L);
inittype(L);
intern_stdmodule(L);
return 1;
}
/* cc: flags+='-O2 -fPIC -shared' output='buffer.so' */
-- Minimal test suite for lua_buffer.
local buffer = require("buffer")
local teststr = "abc123ABC"
local testformatstr = "%s %d %q"
local testsep = "SEP"
local testtab = {}
local testtabstr = tostring(testtab)
local testfunc = function() end
local testfuncstr = tostring(testfunc)
local testlongstr = teststr:rep(2000)
print("Testing lua_bufflib. Initial buffer size: " .. buffer.buffersize)
-- Basic creation tests
assert(type(buffer.new()) == "userdata", "buffer.new() failed to return a userdata")
assert(type(getmetatable(buffer.new())) == "table", "new buffer doesn't have a metatable")
print("Basic tests passed")
-- String conversion tests
assert(tostring(buffer.new(teststr)) == teststr, "string conversion (string) failed")
assert(tostring(buffer.new(testtab)) == testtabstr, "string conversion (table) failed")
assert(tostring(buffer.new(testfunc)) == testfuncstr, "string conversion (function) failed")
assert(tostring(buffer.new(testlongstr)) == testlongstr, "string conversion (long string) failed")
print("String conversion tests passed")
-- add/join tests
assert(tostring(buffer.new():add(teststr)) == tostring(buffer.new(teststr)), "add (string) failed")
assert(tostring(buffer.new():add(teststr, testtab)) == (teststr .. testtabstr), "add (string, table) failed")
assert(tostring(buffer.new():join(testsep, teststr, testtab, testfunc)) == (teststr .. testsep .. testtabstr .. testsep .. testfuncstr), "join failed")
print("add/join tests passed")
-- Length tests
assert(#buffer.new() == 0, "empty buffer has non-zero length")
assert(#buffer.new(teststr) == #teststr, "length (string) failed")
assert(#buffer.new(testtab) == #testtabstr, "length (table) failed")
assert(#buffer.new(testfunc) == #testfuncstr, "length (function) failed")
assert(#buffer.new(teststr, testtab, testfunc) == #(teststr .. testtabstr .. testfuncstr), "length (string, table, function) failed")
assert(#buffer.new(testlongstr, testtab, testfunc) == #(testlongstr .. testtabstr .. testfuncstr), "length (long string, table, function) failed")
print("Length tests passed")
-- Concatenatation tests
assert(tostring(buffer.new(teststr) .. buffer.new(teststr)) == (teststr .. teststr), "concatenation (buffer(string) .. buffer(string)) failed")
assert(tostring(buffer.new(testtab) .. testtabstr) == (testtabstr .. testtabstr), "concatenation (buffer(table) .. string) failed")
assert(tostring(buffer.new(teststr, testtab) .. buffer.new(testfunc)) == (teststr .. testtabstr .. testfuncstr), "concatenation (buffer(string, table) .. buffer(function)) failed")
do -- This test seems to be the most likely to fail when something goes wrong, so we give it a detailed error message
local buffstr = tostring(buffer.new(testlongstr, testfunc) .. buffer.new(teststr, testtab))
local resstr = testlongstr .. testfuncstr .. teststr .. testtabstr
if buffstr ~= resstr then
for i = 1, math.max(#buffstr, #resstr) do
local buffchar, reschar = buffstr:sub(i, i), resstr:sub(i, i)
if buffchar ~= reschar then
print(("Position %d: %s ~= %s"):format(i, tostring(buffchar), tostring(reschar)))
end
end
error(("concatenation (buffer(long string, function) .. buffer(string, table)) failed -- %q (len %d) ~= %q (len %d)"):format(buffstr:sub(1, 25) .. "..." .. buffstr:sub(-25), #buffstr, resstr:sub(1, 25) .. "..." .. resstr:sub(-25), #resstr))
end
end
print("Concatenatation tests passed")
-- Equality tests
assert(buffer.new(teststr) == buffer.new(teststr), "equality (string) failed")
assert(buffer.new(testfunc, testtab) == buffer.new(testfunc, testtab), "equality (function, table) failed")
assert(buffer.new(testlongstr, testfunc, testtab) == buffer.new(testlongstr, testfunc, testtab), "equality (long string, function, table) failed")
print("Equality tests passed")
-- Reset tests
assert(tostring(buffer.new(teststr):reset()) == "", "reset (string) failed")
assert(tostring(buffer.new(testlongstr, testlongstr, testlongstr, testfunc):reset()) == "", "reset (long string x3, function) failed")
print("Reset tests passed")
-- isbuffer tests
assert(buffer.isbuffer(buffer.new()), "isbuffer (buffer) failed")
assert(not buffer.isbuffer(1), "isbuffer (number) failed")
assert(not buffer.isbuffer(testtab), "isbuffer (table) failed")
print("isbuffer tests passed")
-- ipairs tests
if _VERSION == "Lua 5.2" then
local t = { string.byte(teststr, 1, -1) }
for i, ch in ipairs(buffer.new(teststr)) do
assert(ch == t[i], "ipairs failed")
end
print("ipairs tests passed")
end
-- index/newindex tests
do
local t = { string.byte(teststr, 1, -1) }
local testbuff = buffer.new(teststr)
assert(testbuff[1] == t[1], "index test failed")
assert(testbuff[-1] == t[#t], "negative index test failed")
testbuff[#testbuff+1] = "a"
assert(tostring(testbuff) == teststr .. "a", "append string char test failed")
testbuff[#testbuff+1] = 97
assert(tostring(testbuff) == teststr .. "aa", "append number char test failed")
end
-- We reuse the same buffer for most of these tests, since we've already established that the basic buffer operations work and we just want to test that the string methods call and return from the original function properly.
-- We don't test string.char or string.dump because they can't be used as string methods due to their arguments. We don't test string.gmatch because it can't be tested with a simple equality check.
local testbuff = buffer.new(teststr)
assert(testbuff:byte(2) == teststr:byte(2), "byte failed")
assert(testbuff:find("123", 1, true) == teststr:find("123", 1, true), "find (plain) failed")
assert(testbuff:find("%d+") == teststr:find("%d+"), "find (pattern) failed")
assert(buffer.new(testformatstr):format(teststr, 123, teststr) == testformatstr:format(teststr, 123, teststr), "format failed")
assert(testbuff:gsub("123", "000") == teststr:gsub("123", "000"), "gsub failed")
assert(testbuff:len() == teststr:len(), "len failed") -- This test is fairly similar to the length operator tests we performed above, but we perform it anyway
assert(testbuff:lower() == teststr:lower(), "lower failed")
assert(testbuff:match("%d+") == teststr:match("%d+"), "match failed")
assert(testbuff:rep(42) == teststr:rep(42), "rep failed")
assert(testbuff:reverse() == teststr:reverse(), "reverse failed")
assert(testbuff:sub(2, 4) == teststr:sub(2, 4), "sub failed")
assert(testbuff:upper() == teststr:upper(), "upper failed")
testbuff = nil
print("String method tests passed")
local initialGC = collectgarbage("count")
collectgarbage "collect"
collectgarbage "collect"
local gcdiff = initialGC - collectgarbage("count")
print(("lua_buffer tests completed successfully. Collected %.2f KB of garbage."):format(gcdiff))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment