
--Binding to the DynASM encoding engine.
--Written by Cosmin Apreutesei. Public Domain.

local ffi = require'ffi'
local bit = require'bit'
local arch = ffi.arch
if arch == 'x64' then arch = 'x86' end --same linker for x64
local C = ffi.load('dasm_'..arch)
local M = {C = C}

M._VERSION = 10400

ffi.cdef[[
enum {
	DASM_S_OK         = 0x00000000,
	DASM_S_NOMEM      = 0x01000000,
	DASM_S_PHASE      = 0x02000000,
	DASM_S_MATCH_SEC  = 0x03000000,
	DASM_S_RANGE_I    = 0x11000000,
	DASM_S_RANGE_SEC  = 0x12000000,
	DASM_S_RANGE_LG   = 0x13000000,
	DASM_S_RANGE_PC   = 0x14000000,
	DASM_S_RANGE_VREG	= 0x15000000,
	DASM_S_UNDEF_L    = 0x21000000,
	DASM_S_UNDEF_PC   = 0x22000000,
};

/* Internal DynASM encoder state. */
typedef struct dasm_State_Ref { struct dasm_State *p; } dasm_State_Ref;
typedef dasm_State_Ref *Dst_DECL;

/* Initialize and free DynASM state. */
void dasm_init(Dst_DECL, int maxsection);
void dasm_free(Dst_DECL);

/* Setup global array. Must be called before dasm_setup(). */
void dasm_setupglobal(Dst_DECL, void **gl, unsigned int maxgl);

/* Grow PC label array. Can be called after dasm_setup(), too. */
void dasm_growpc(Dst_DECL, unsigned int maxpc);

/* Setup encoder. */
void dasm_setup(Dst_DECL, const void *actionlist);

/* Feed encoder with actions. Calls are generated by pre-processor. */
void dasm_put(Dst_DECL, int start, ...);

/* Link sections and return the resulting size. */
int dasm_link(Dst_DECL, size_t *szp);

/* Encode sections into buffer. */
int dasm_encode(Dst_DECL, void *buffer);

/* Get PC label offset. */
int dasm_getpclabel(Dst_DECL, unsigned int pc);

/* Optional sanity checker to call between isolated encoding steps. */
int dasm_checkstep(Dst_DECL, int secmatch);

typedef int (*DASM_EXTERN_TYPE) (void *ctx, unsigned char *addr, int idx, int type);
DASM_EXTERN_TYPE DASM_EXTERN_FUNC;
]]

local function err(...)
  io.stderr:setvbuf'no'
  io.stderr:write('dasm error: ', ...)
  io.stderr:write'\n'
  os.exit(1)
end

--status check helper

local status_map = {
	[C.DASM_S_NOMEM]      = 'out of memory',
	[C.DASM_S_PHASE]      = 'phase error',
	[C.DASM_S_MATCH_SEC]  = 'section not found',
	[C.DASM_S_RANGE_I]    = 'immediate value out of range',
	[C.DASM_S_RANGE_SEC]  = 'too many sections',
	[C.DASM_S_RANGE_LG]   = 'too many global labels',
	[C.DASM_S_RANGE_PC]   = 'too many pclabels',
	[C.DASM_S_RANGE_VREG] = 'variable register out of range',
	[C.DASM_S_UNDEF_L]    = 'undefined global label',
	[C.DASM_S_UNDEF_PC]   = 'undefined pclabel',
}

local function checkst(st)
	if st == C.DASM_S_OK then return end
	local status, arg = status_map[bit.band(st, 0xff000000)], bit.band(st, 0x00ffffff)
	if status then
		err(status, '. :', arg)
	else
		err(string.format('0x%08X', st))
	end
end

--low level API

M.init = C.dasm_init
M.free = C.dasm_free
M.setupglobal = C.dasm_setupglobal
M.growpc = C.dasm_growpc
M.setup = C.dasm_setup

local int_ct = ffi.typeof'int'
local function convert_arg(arg)   --dasm_put() accepts only int32 varargs.
	if type(arg) == "number" then  --but we make it accept uint32 too by normalizing the arg.
		arg = bit.tobit(arg)        --non-number args are converted to int32 according to ffi rules.
	end
	return ffi.cast(int_ct, arg)
end
local function convert_args(...) --not a tailcall but at least it doesn't make any garbage
	if select('#', ...) == 0 then return end
	return convert_arg(...), convert_args(select(2, ...))
end
function M.put(state, start, ...)
	C.dasm_put(state, start, convert_args(...))
end

function M.link(state, sz)
	sz = sz or ffi.new'size_t[1]'
	checkst(C.dasm_link(state, sz))
	return tonumber(sz[0])
end

function M.encode(state, buf)
	checkst(C.dasm_encode(state, buf))
end
jit.off(M.encode) --calls the DASM_EXTERN_FUNC callback

local voidptr_ct = ffi.typeof'void*'
local byteptr_ct = ffi.typeof'int8_t*'
function M.getpclabel(state, pc, buf)
	local offset = C.dasm_getpclabel(state, pc)
	if buf then
		return ffi.cast(voidptr_ct, ffi.cast(byteptr_ct, buf) + offset)
	end
	return offset
end

function M.checkstep(state, section)
	checkst(C.dasm_checkstep(state, section or -1))
end

--get the address of a standard symbol.
--TODO: ask Mike to expose clib_getsym() in ffi so we can get the address
--of symbols without having to declare them first.
local function getsym(name)
	return ffi.C[name]
end
local getsym = function(name)
	local ok, sym = pcall(getsym, name)
	if not ok then --not found or not defined: define it and try again
		ffi.cdef(string.format('void %s();', name))
		return getsym(name)
	else
		return sym
	end
end

--DASM_EXTERN callback plumbing

local extern_names    --t[idx] -> name
local extern_get      --f(name) -> ptr

local byteptr_ct = ffi.typeof'uint8_t*'

local function DASM_EXTERN_FUNC(ctx, addr, idx, type)
	if not extern_names or not extern_get then
		err'extern callback not initialized.'
	end
	local name = extern_names[idx]
	local ptr = extern_get(name)
	if ptr == nil then
	  err('extern not found: ', name, '.')
	end
	if type ~= 0 then
		return ffi.cast(byteptr_ct, ptr) - addr - 4
	else
		return ptr
	end
end

function M.setupextern(_, names, getter)
	extern_names = names
	extern_get = getter or getsym
	if C.DASM_EXTERN_FUNC == nil then
		C.DASM_EXTERN_FUNC = DASM_EXTERN_FUNC
	end
end

--hi-level API

function M.new(actionlist, externnames, sectioncount, globalcount, externget, globals)
	local state = ffi.new'dasm_State_Ref'
	M.init(state, sectioncount or 1)
	globalcount = globalcount or 256
	globals = globals or ffi.new('void*[?]', globalcount)
	ffi.gc(state, function(state)
		local _ = actionlist, externnames, globals, externget --anchor those: don't rely on the user doing so
		M.free(state)
	end)
	M.setupglobal(state, globals, globalcount)
	M.setupextern(state, externnames, externget)
	M.setup(state, actionlist)
	return state, globals
end

function M.build(state)
	state:checkstep(-1)
	local sz = state:link()
	if sz == 0 then err'no code?' end
	local mm = require'dasm_mm' --runtime dependency
	local buf = mm.new(sz)
	state:encode(buf)
	mm.protect(buf, sz)
	return buf, sz
end

function M.dump(addr, size, out)
	local disass = require('jit.dis_'..jit.arch).disass
	disass(ffi.string(addr, size), tonumber(ffi.cast('uintptr_t', addr)), out)
end

--given the globals array from dasm.new() and the globalnames list
--from the `.globalnames` directive, return a map {global_name -> global_addr}.
function M.globals(globals, globalnames)
	local t = {}
	for i = 0, #globalnames do
		if globals[i] ~= nil then
			t[globalnames[i]] = globals[i]
		end
	end
	return t
end

--object interface

ffi.metatype('dasm_State_Ref', {__index = {
	--low-level API
	init = M.init,
	free = M.free,
	setupglobal = M.setupglobal,
	setupextern = M.setupextern,
	growpc = M.growpc,
	setup = M.setup,
	put = M.put,
	link = M.link,
	encode = M.encode,
	getpclabel = M.getpclabel,
	checkstep = M.checkstep,
	--hi-level API
	build = M.build,
}})

if not ... then --demo
	local dasm = M
	local actions = ffi.new('const uint8_t[19]',
		{254,0,102,184,5,0,254,1,102,187,3,0,254,2,102,187,3,0,255})
	local Dst, globals = dasm.new(actions, nil, 3)
	--|.code
	dasm.put(Dst, 0)
	--| mov ax, 5
	--|.sub1
	dasm.put(Dst, 2)
	--| mov bx, 3
	--|.sub2
	dasm.put(Dst, 8)
	--| mov bx, 3
	dasm.put(Dst, 14)
	local addr, size = Dst:build()
	dasm.dump(addr, size)
end

return M
