From 43c97cbc2904d2bac87c61515bbc16c38a091548 Mon Sep 17 00:00:00 2001 From: Benoit Giannangeli Date: Fri, 14 Apr 2017 08:59:00 +0200 Subject: hooks --- README.md | 5 +--- src/lapi.js | 22 +++++++++++++++- src/ldblib.js | 82 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/ldebug.js | 49 ++++++++++++++++++++++++++++++++++ src/ldo.js | 38 +++++++++++++++++++++++++- src/lua.js | 27 +++++++++++++++++++ src/lvm.js | 9 ++++++- tests/ldblib.js | 44 +++++++++++++++++++++++++++++++ 8 files changed, 269 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index e32f8f5..01fcbde 100644 --- a/README.md +++ b/README.md @@ -40,12 +40,9 @@ - [ ] lua_pushfstring - [ ] lua_pushlightuserdata - [ ] lua_pushvfstring - - [ ] lua_rawgetp - [ ] lua_rawseti - - [ ] lua_rawsetp - [ ] lua_register - [ ] lua_setallocf - - [ ] lua_sethook - [ ] lua_tocfunction - [ ] Auxiliary library - [x] ... @@ -87,6 +84,7 @@ - [x] debug.getregistry - [x] debug.getupvalue - [x] debug.getuservalue + - [x] debug.sethook - [x] debug.setlocal - [x] debug.setmetatable - [x] debug.setupvalue @@ -95,7 +93,6 @@ - [x] debug.upvalueid - [x] debug.upvaluejoin - [ ] debug.gethook - - [ ] debug.sethook - [ ] Run [Lua test suite](https://github.com/lua/tests) - [ ] DOM API binding diff --git a/src/lapi.js b/src/lapi.js index caafc0a..bf9b381 100644 --- a/src/lapi.js +++ b/src/lapi.js @@ -403,6 +403,15 @@ const lua_rawset = function(L, idx) { L.top -= 2; }; +const lua_rawsetp = function(L, idx, p) { + assert(1 < L.top - L.ci.funcOff, "not enough elements in the stack"); + let o = index2addr(L, idx); + assert(L, o.ttistable(), "table expected"); + let k = p; + o.__newindex(o, k, L.stack[L.top - 1]); + L.top--; +}; + /* ** get functions (Lua -> stack) */ @@ -436,6 +445,15 @@ const lua_rawgeti = function(L, idx, n) { return L.stack[L.top - 1].ttnov(); }; +const lua_rawgetp = function(L, idx, p) { + let t = index2addr(L, idx); + assert(t.ttistable(), "table expected"); + let k = p; + L.stack[L.top++] = t.__index(t, k); + assert(L.top <= L.ci.top, "stack overflow"); + return L.stack[L.top - 1].ttnov(); +}; + const lua_rawget = function(L, idx) { let t = index2addr(L, idx); @@ -875,7 +893,7 @@ const lua_pcallk = function(L, nargs, nresults, errfunc, ctx, k) { ci.extra = c.funcOff; ci.u.c.old_errfunc = L.errfunc; L.errfunc = func; - // TODO: setoah(ci->callstatus, L->allowhook); + ci.callstatus &= ~lstate.CIST_OAH | L.allowhook; ci.callstatus |= lstate.CIST_YPCALL; /* function can do error recovery */ ldo.luaD_call(L, c.funcOff, nresults); /* do the call */ ci.callstatus &= ~lstate.CIST_YPCALL; @@ -1049,8 +1067,10 @@ module.exports.lua_pushvalue = lua_pushvalue; module.exports.lua_rawequal = lua_rawequal; module.exports.lua_rawget = lua_rawget; module.exports.lua_rawgeti = lua_rawgeti; +module.exports.lua_rawgetp = lua_rawgetp; module.exports.lua_rawlen = lua_rawlen; module.exports.lua_rawset = lua_rawset; +module.exports.lua_rawsetp = lua_rawsetp; module.exports.lua_remove = lua_remove; module.exports.lua_replace = lua_replace; module.exports.lua_rotate = lua_rotate; diff --git a/src/ldblib.js b/src/ldblib.js index 27aca96..81f27fb 100644 --- a/src/ldblib.js +++ b/src/ldblib.js @@ -263,6 +263,87 @@ const db_upvaluejoin = function(L) { return 0; }; +/* +** The hook table at registry[HOOKKEY] maps threads to their current +** hook function. (We only need the unique address of 'HOOKKEY'.) +*/ +const HOOKKEY = lua.to_luastring("__hooks__"); + +const hooknames = ["call", "return", "line", "count", "tail call"].map(e => lua.to_luastring(e)); + +/* +** Call hook function registered at hook table for the current +** thread (if there is one) +*/ +const hookf = function(L, ar) { + lapi.lua_rawgetp(L, lua.LUA_REGISTRYINDEX, HOOKKEY); + lapi.lua_pushthread(L); + if (lapi.lua_rawget(L, -2) === lua.CT.LUA_TFUNCTION) { /* is there a hook function? */ + lapi.lua_pushstring(L, hooknames[ar.event]); /* push event name */ + if (ar.currentline >= 0) + lapi.lua_pushinteger(L, ar.currentline); /* push current line */ + else lapi.lua_pushnil(L); + assert(ldebug.lua_getinfo(L, [char["l"], char["S"]], ar)); + lapi.lua_call(L, 2, 0); /* call hook function */ + } +}; + +/* +** Convert a string mask (for 'sethook') into a bit mask +*/ +const makemask = function(smask, count) { + let mask = 0; + if (smask.indexOf(char["c"]) > -1) mask |= lua.LUA_MASKCALL; + if (smask.indexOf(char["r"]) > -1) mask |= lua.LUA_MASKRET; + if (smask.indexOf(char["l"]) > -1) mask |= lua.LUA_MASKLINE; + if (count > 0) mask |= lua.LUA_MASKCOUNT; + return mask; +}; + +/* +** Convert a bit mask (for 'gethook') into a string mask +*/ +const unmakemask = function(mask, smask) { + let i = 0; + if (mask & lua.LUA_MASKCALL) smask[i++] = char["c"]; + if (mask & lua.LUA_MASKRET) smask[i++] = char["r"]; + if (mask & lua.LUA_MASKLINE) smask[i++] = char["l"]; + smask[i] = 0; + return smask; +}; + +const db_sethook = function(L) { + let mask, count, func; + let thread = getthread(L); + let L1 = thread.thread; + let arg = thread.arg; + if (lapi.lua_isnoneornil(L, arg+1)) { /* no hook? */ + lapi.lua_settop(L, arg+1); + func = null; mask = 0; count = 0; /* turn off hooks */ + } + else { + const smask = lauxlib.luaL_checkstring(L, arg + 2); + lauxlib.luaL_checktype(L, arg+1, lua.CT.LUA_TFUNCTION); + count = lauxlib.luaL_optinteger(L, arg + 3, 0); + func = hookf; mask = makemask(smask, count); + } + if (lapi.lua_rawgetp(L, lua.LUA_REGISTRYINDEX, HOOKKEY) === lua.CT.LUA_TNIL) { + lapi.lua_createtable(L, 0, 2); /* create a hook table */ + lapi.lua_pushvalue(L, -1); + lapi.lua_rawsetp(L, lua.LUA_REGISTRYINDEX, HOOKKEY); /* set it in position */ + lapi.lua_pushstring(L, [char["k"]]); + lapi.lua_setfield(L, -2, lua.to_luastring("__mode")); /** hooktable.__mode = "k" */ + lapi.lua_pushvalue(L, -1); + lapi.lua_setmetatable(L, -2); /* setmetatable(hooktable) = hooktable */ + } + checkstack(L, L1, 1); + lapi.lua_pushthread(L1); lapi.lua_xmove(L1, L, 1); /* key (thread) */ + lapi.lua_pushvalue(L, arg + 1); /* value (hook function) */ + lapi.lua_rawset(L, -3); /* hooktable[L1] = new Lua hook */ + ldebug.lua_sethook(L1, func, mask, count); + return 0; +}; + const db_traceback = function(L) { let thread = getthread(L); let L1 = thread.thread; @@ -284,6 +365,7 @@ const dblib = { "getregistry": db_getregistry, "getupvalue": db_getupvalue, "getuservalue": db_getuservalue, + "sethook": db_sethook, "setlocal": db_setlocal, "setmetatable": db_setmetatable, "setupvalue": db_setupvalue, diff --git a/src/ldebug.js b/src/ldebug.js index 44410a6..a73b7f7 100644 --- a/src/ldebug.js +++ b/src/ldebug.js @@ -38,6 +38,19 @@ const swapextra = function(L) { } }; +const lua_sethook = function(L, func, mask, count) { + if (func === null || mask === 0) { /* turn off hooks? */ + mask = 0; + func = null; + } + if (L.ci.callstatus & lstate.CIST_LUA) + L.oldpc = L.ci.pcOff; + L.hook = func; + L.basehookcount = count; + L.hookcount = L.basehookcount; + L.hookmask = mask; +}; + const lua_getstack = function(L, level, ar) { let ci; let status; @@ -577,6 +590,40 @@ const luaG_tointerror = function(L, p1, p2) { luaG_runerror(L, lua.to_luastring(`number${lobject.jsstring(varinfo(L, p2))} has no integer representation`)); }; +const luaG_traceexec = function(L) { + let ci = L.ci; + let mask = L.hookmask; + let counthook = (--L.hookcount === 0 && (mask & lua.LUA_MASKCOUNT)); + if (counthook) + L.hookcount = L.basehookcount; /* reset count */ + else if (!(mask & lua.LUA_MASKLINE)) + return; /* no line hook and count != 0; nothing to be done */ + if (ci.callstatus & lstate.CIST_HOOKYIELD) { /* called hook last time? */ + ci.callstatus &= ~lstate.CIST_HOOKYIELD; /* erase mark */ + return; /* do not call hook again (VM yielded, so it did not move) */ + } + if (counthook) + ldo.luaD_hook(L, lua.LUA_HOOKCOUNT, -1); /* call count hook */ + if (mask & lua.LUA_MASKLINE) { + let p = ci.func.p; + let npc = ci.pcOff; // pcRel(ci.u.l.savedpc, p); + let newline = p.lineinfo ? p.lineinfo[npc] : -1; + if (npc === 0 || /* call linehook when enter a new function, */ + ci.pcOff <= L.oldpc || /* when jump back (loop), or when */ + newline !== p.lineinfo ? p.lineinfo[L.oldpc] : -1) /* enter a new line */ + ldo.luaD_hook(L, lua.LUA_HOOKLINE, newline); /* call line hook */ + } + L.oldpc = ci.pcOff; + if (L.status === TS.LUA_YIELD) { /* did hook yield? */ + if (counthook) + L.hookcount = 1; /* undo decrement to zero */ + ci.u.l.savedpc--; /* undo increment (resume will increment it again) */ + ci.callstatus |= lstate.CIST_HOOKYIELD; /* mark that it yielded */ + ci.func = L.top - 1; /* protect stack below results */ + ldo.luaD_throw(L, TS.LUA_YIELD); + } +}; + module.exports.luaG_addinfo = luaG_addinfo; module.exports.luaG_concaterror = luaG_concaterror; module.exports.luaG_errormsg = luaG_errormsg; @@ -584,8 +631,10 @@ module.exports.luaG_opinterror = luaG_opinterror; module.exports.luaG_ordererror = luaG_ordererror; module.exports.luaG_runerror = luaG_runerror; module.exports.luaG_tointerror = luaG_tointerror; +module.exports.luaG_traceexec = luaG_traceexec; module.exports.luaG_typeerror = luaG_typeerror; module.exports.lua_getinfo = lua_getinfo; module.exports.lua_getlocal = lua_getlocal; module.exports.lua_getstack = lua_getstack; +module.exports.lua_sethook = lua_sethook; module.exports.lua_setlocal = lua_setlocal; diff --git a/src/ldo.js b/src/ldo.js index 6a74cad..0fcbf0e 100644 --- a/src/ldo.js +++ b/src/ldo.js @@ -73,7 +73,8 @@ const luaD_precall = function(L, off, nresults) { ci.funcOff = off; ci.top = L.top + lua.LUA_MINSTACK; ci.callstatus = 0; - // TODO: hook + if (L.hookmask & lua.LUA_MASKCALL) + luaD_hook(L, lua.LUA_HOOKCALL, -1); let n = f(L); /* do the actual call */ assert(n < L.top - L.ci.funcOff, "not enough elements in the stack"); @@ -129,6 +130,13 @@ const luaD_precall = function(L, off, nresults) { const luaD_poscall = function(L, ci, firstResult, nres) { let wanted = ci.nresults; + + if (L.hookmask & (lua.LUA_MASKRET | lua.LUA_MASKLINE)) { + if (L.hookmask & lua.LUA_MASKRET) + luaD_hook(L, lua.LUA_HOOKRET, -1); + L.oldpc = ci.previous.pcOff; /* 'oldpc' for caller function */ + } + let res = ci.funcOff; L.ci = ci.previous; L.ciOff--; @@ -171,6 +179,33 @@ const moveresults = function(L, firstResult, res, nres, wanted) { return true; }; +/* +** Call a hook for the given event. Make sure there is a hook to be +** called. (Both 'L->hook' and 'L->hookmask', which triggers this +** function, can be changed asynchronously by signals.) +*/ +const luaD_hook = function(L, event, line) { + let hook = L.hook; + if (hook && L.allowhook) { /* make sure there is a hook */ + let ci = L.ci; + let top = L.top; + let ci_top = ci.top; + let ar = new lua.lua_Debug(); + ar.event = event; + ar.currentline = line; + ar.i_ci = ci; + ci.top = L.top + lua.LUA_MINSTACK; + L.allowhook = 0; /* cannot call hooks inside a hook */ + ci.callstatus |= lstate.CIST_HOOKED; + hook(L, ar); + assert(!L.allowhook); + L.allowhook = 1; + ci.top = ci_top; + L.top = top; + ci.callstatus &= ~lstate.CIST_HOOKED; + } +}; + const adjust_varargs = function(L, p, actual) { let nfixargs = p.numparams; /* move fixed parameters to final position */ @@ -566,6 +601,7 @@ module.exports.SParser = SParser; module.exports.adjust_varargs = adjust_varargs; module.exports.luaD_call = luaD_call; module.exports.luaD_callnoyield = luaD_callnoyield; +module.exports.luaD_hook = luaD_hook; module.exports.luaD_pcall = luaD_pcall; module.exports.luaD_poscall = luaD_poscall; module.exports.luaD_precall = luaD_precall; diff --git a/src/lua.js b/src/lua.js index d4f620b..1423750 100644 --- a/src/lua.js +++ b/src/lua.js @@ -198,6 +198,24 @@ const to_luastring = function(str, maxBytesToWrite) { return outU8Array; }; +/* +** Event codes +*/ +const LUA_HOOKCALL = 0; +const LUA_HOOKRET = 1; +const LUA_HOOKLINE = 2; +const LUA_HOOKCOUNT = 3; +const LUA_HOOKTAILCALL = 4; + + +/* +** Event masks +*/ +const LUA_MASKCALL = (1 << LUA_HOOKCALL); +const LUA_MASKRET = (1 << LUA_HOOKRET); +const LUA_MASKLINE = (1 << LUA_HOOKLINE); +const LUA_MASKCOUNT = (1 << LUA_HOOKCOUNT); + module.exports.CT = CT; module.exports.FENGARI_AUTHORS = FENGARI_AUTHORS; module.exports.FENGARI_COPYRIGHT = FENGARI_COPYRIGHT; @@ -209,8 +227,17 @@ module.exports.FENGARI_VERSION_NUM = FENGARI_VERSION_NUM; module.exports.FENGARI_VERSION_RELEASE = FENGARI_VERSION_RELEASE; module.exports.LUA_AUTHORS = LUA_AUTHORS; module.exports.LUA_COPYRIGHT = LUA_COPYRIGHT; +module.exports.LUA_HOOKCALL = LUA_HOOKCALL; +module.exports.LUA_HOOKCOUNT = LUA_HOOKCOUNT; +module.exports.LUA_HOOKLINE = LUA_HOOKLINE; +module.exports.LUA_HOOKRET = LUA_HOOKRET; +module.exports.LUA_HOOKTAILCALL = LUA_HOOKTAILCALL; module.exports.LUA_INITVARVERSION = LUA_INITVARVERSION; module.exports.LUA_INIT_VAR = LUA_INIT_VAR; +module.exports.LUA_MASKCALL = LUA_MASKCALL; +module.exports.LUA_MASKCOUNT = LUA_MASKCOUNT; +module.exports.LUA_MASKLINE = LUA_MASKLINE; +module.exports.LUA_MASKRET = LUA_MASKRET; module.exports.LUA_MINSTACK = LUA_MINSTACK; module.exports.LUA_MULTRET = -1; module.exports.LUA_NUMTAGS = LUA_NUMTAGS; diff --git a/src/lvm.js b/src/lvm.js index ad1e807..6febe4b 100644 --- a/src/lvm.js +++ b/src/lvm.js @@ -123,8 +123,15 @@ const luaV_execute = function(L) { base = ci.u.l.base; i = ci.u.l.savedpc[ci.pcOff++]; + + if (L.hookmask & (lua.LUA_MASKLINE | lua.LUA_MASKCOUNT)) { + ldebug.luaG_traceexec(L); + base = ci.u.l.base; + } + + ra = RA(L, base, i); - opcode = i.opcode + opcode = i.opcode; } if (i.breakpoint) // TODO: remove, used until lapi diff --git a/tests/ldblib.js b/tests/ldblib.js index 7dd35c3..8d00952 100644 --- a/tests/ldblib.js +++ b/tests/ldblib.js @@ -7,6 +7,50 @@ const lauxlib = require("../src/lauxlib.js"); const lua = require('../src/lua.js'); const linit = require('../src/linit.js'); +test('debug.sethook', function (t) { + let luaCode = ` + result = "" + + debug.sethook(function (event) + result = result .. event .. " " + end, "crl", 1) + + local l = function() end + + l() + l() + l() + + return result + `, L; + + t.plan(3); + + t.doesNotThrow(function () { + + L = lauxlib.luaL_newstate(); + + linit.luaL_openlibs(L); + + lauxlib.luaL_loadstring(L, lua.to_luastring(luaCode)); + + }, "Lua program loaded without error"); + + t.doesNotThrow(function () { + + lapi.lua_call(L, 0, -1); + + }, "Lua program ran without error"); + + t.strictEqual( + lapi.lua_tojsstring(L, -1), + "return count line count line count line count return count line count line count return count line count line count return count line count line ", + "Correct element(s) on the stack" + ); + +}); + + test('debug.getlocal', function (t) { let luaCode = ` local alocal = "alocal" -- cgit v1.2.3-54-g00ecf