/*
** FFI C library loader.
** Copyright (C) 2005-2014 Mike Pall. See Copyright Notice in luajit.h
*/

#include "lj_obj.h"

#if LJ_HASFFI

    #include "lj_gc.h"
    #include "lj_err.h"
    #include "lj_tab.h"
    #include "lj_str.h"
    #include "lj_udata.h"
    #include "lj_ctype.h"
    #include "lj_cconv.h"
    #include "lj_cdata.h"
    #include "lj_clib.h"

/* -- OS-specific functions ----------------------------------------------- */

    #if LJ_TARGET_DLOPEN

        #include <dlfcn.h>
        #include <stdio.h>

        #if defined(RTLD_DEFAULT)
            #define CLIB_DEFHANDLE RTLD_DEFAULT
        #elif LJ_TARGET_OSX || LJ_TARGET_BSD
            #define CLIB_DEFHANDLE ((void *)(intptr_t)-2)
        #else
            #define CLIB_DEFHANDLE NULL
        #endif

LJ_NORET LJ_NOINLINE static void clib_error_(lua_State *L)
{
    lj_err_callermsg(L, dlerror());
}

        #define clib_error(L, fmt, name) clib_error_(L)

        #if defined(__CYGWIN__)
            #define CLIB_SOPREFIX "cyg"
        #else
            #define CLIB_SOPREFIX "lib"
        #endif

        #if LJ_TARGET_OSX
            #define CLIB_SOEXT "%s.dylib"
        #elif defined(__CYGWIN__)
            #define CLIB_SOEXT "%s.dll"
        #else
            #define CLIB_SOEXT "%s.so"
        #endif

static const char *clib_extname(lua_State *L, const char *name)
{
    if (!strchr(name, '/')
        #ifdef __CYGWIN__
        && !strchr(name, '\\')
        #endif
    ) {
        if (!strchr(name, '.')) {
            name = lj_str_pushf(L, CLIB_SOEXT, name);
            L->top--;
        #ifdef __CYGWIN__
        }
        else {
            return name;
        #endif
        }
        if (!(name[0] == CLIB_SOPREFIX[0] && name[1] == CLIB_SOPREFIX[1] &&
              name[2] == CLIB_SOPREFIX[2])) {
            name = lj_str_pushf(L, CLIB_SOPREFIX "%s", name);
            L->top--;
        }
    }
    return name;
}

/* Check for a recognized ld script line. */
static const char *clib_check_lds(lua_State *L, const char *buf)
{
    char *p, *e;
    if ((!strncmp(buf, "GROUP", 5) || !strncmp(buf, "INPUT", 5)) &&
        (p = strchr(buf, '('))) {
        while (*++p == ' ')
            ;
        for (e = p; *e && *e != ' ' && *e != ')'; e++)
            ;
        return strdata(lj_str_new(L, p, e - p));
    }
    return NULL;
}

/* Quick and dirty solution to resolve shared library name from ld script. */
static const char *clib_resolve_lds(lua_State *L, const char *name)
{
    FILE *fp = fopen(name, "r");
    const char *p = NULL;
    if (fp) {
        char buf[256];
        if (fgets(buf, sizeof(buf), fp)) {
            if (!strncmp(buf, "/* GNU ld script", 16)) { /* ld script magic? */
                while (fgets(buf, sizeof(buf), fp)) {    /* Check all lines. */
                    p = clib_check_lds(L, buf);
                    if (p)
                        break;
                }
            }
            else { /* Otherwise check only the first line. */
                p = clib_check_lds(L, buf);
            }
        }
        fclose(fp);
    }
    return p;
}

static void *clib_loadlib(lua_State *L, const char *name, int global)
{
    void *h = dlopen(clib_extname(L, name),
                     RTLD_LAZY | (global ? RTLD_GLOBAL : RTLD_LOCAL));
    if (!h) {
        const char *e, *err = dlerror();
        if (*err == '/' && (e = strchr(err, ':')) &&
            (name =
                 clib_resolve_lds(L, strdata(lj_str_new(L, err, e - err))))) {
            h = dlopen(name, RTLD_LAZY | (global ? RTLD_GLOBAL : RTLD_LOCAL));
            if (h)
                return h;
            err = dlerror();
        }
        lj_err_callermsg(L, err);
    }
    return h;
}

static void clib_unloadlib(CLibrary *cl)
{
    if (cl->handle && cl->handle != CLIB_DEFHANDLE)
        dlclose(cl->handle);
}

static void *clib_getsym(CLibrary *cl, const char *name)
{
    void *p = dlsym(cl->handle, name);
    return p;
}

    #elif LJ_TARGET_WINDOWS

        #define WIN32_LEAN_AND_MEAN
        #include <windows.h>

        #ifndef GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS
            #define GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS 4
            #define GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT 2
BOOL WINAPI GetModuleHandleExA(DWORD, LPCSTR, HMODULE *);
        #endif

        #define CLIB_DEFHANDLE ((void *)-1)

/* Default libraries. */
enum {
    CLIB_HANDLE_EXE,
    CLIB_HANDLE_DLL,
    CLIB_HANDLE_CRT,
    CLIB_HANDLE_KERNEL32,
    CLIB_HANDLE_USER32,
    CLIB_HANDLE_GDI32,
    CLIB_HANDLE_MAX
};

static void *clib_def_handle[CLIB_HANDLE_MAX];

LJ_NORET LJ_NOINLINE static void clib_error(lua_State *L, const char *fmt,
                                            const char *name)
{
    DWORD err = GetLastError();
    char buf[128];
    if (!FormatMessageA(FORMAT_MESSAGE_IGNORE_INSERTS |
                            FORMAT_MESSAGE_FROM_SYSTEM,
                        NULL, err, 0, buf, sizeof(buf), NULL))
        buf[0] = '\0';
    lj_err_callermsg(L, lj_str_pushf(L, fmt, name, buf));
}

static int clib_needext(const char *s)
{
    while (*s) {
        if (*s == '/' || *s == '\\' || *s == '.')
            return 0;
        s++;
    }
    return 1;
}

static const char *clib_extname(lua_State *L, const char *name)
{
    if (clib_needext(name)) {
        name = lj_str_pushf(L, "%s.dll", name);
        L->top--;
    }
    return name;
}

static void *clib_loadlib(lua_State *L, const char *name, int global)
{
    DWORD oldwerr = GetLastError();
    void *h = (void *)LoadLibraryA(clib_extname(L, name));
    if (!h)
        clib_error(L, "cannot load module " LUA_QS ": %s", name);
    SetLastError(oldwerr);
    UNUSED(global);
    return h;
}

static void clib_unloadlib(CLibrary *cl)
{
    if (cl->handle == CLIB_DEFHANDLE) {
        MSize i;
        for (i = CLIB_HANDLE_KERNEL32; i < CLIB_HANDLE_MAX; i++) {
            void *h = clib_def_handle[i];
            if (h) {
                clib_def_handle[i] = NULL;
                FreeLibrary((HINSTANCE)h);
            }
        }
    }
    else if (cl->handle) {
        FreeLibrary((HINSTANCE)cl->handle);
    }
}

static void *clib_getsym(CLibrary *cl, const char *name)
{
    void *p = NULL;
    if (cl->handle == CLIB_DEFHANDLE) { /* Search default libraries. */
        MSize i;
        for (i = 0; i < CLIB_HANDLE_MAX; i++) {
            HINSTANCE h = (HINSTANCE)clib_def_handle[i];
            if (!(void *)h) { /* Resolve default library handles (once). */
                switch (i) {
                case CLIB_HANDLE_EXE:
                    GetModuleHandleExA(
                        GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, NULL, &h);
                    break;
                case CLIB_HANDLE_DLL:
                    GetModuleHandleExA(
                        GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS |
                            GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
                        (const char *)clib_def_handle, &h);
                    break;
                case CLIB_HANDLE_CRT:
                    GetModuleHandleExA(
                        GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS |
                            GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
                        (const char *)&_fmode, &h);
                    break;
                case CLIB_HANDLE_KERNEL32:
                    h = LoadLibraryA("kernel32.dll");
                    break;
                case CLIB_HANDLE_USER32:
                    h = LoadLibraryA("user32.dll");
                    break;
                case CLIB_HANDLE_GDI32:
                    h = LoadLibraryA("gdi32.dll");
                    break;
                }
                if (!h)
                    continue;
                clib_def_handle[i] = (void *)h;
            }
            p = (void *)GetProcAddress(h, name);
            if (p)
                break;
        }
    }
    else {
        p = (void *)GetProcAddress((HINSTANCE)cl->handle, name);
    }
    return p;
}

    #else

        #define CLIB_DEFHANDLE NULL

LJ_NORET LJ_NOINLINE static void clib_error(lua_State *L, const char *fmt,
                                            const char *name)
{
    lj_err_callermsg(L, lj_str_pushf(L, fmt, name, "no support for this OS"));
}

static void *clib_loadlib(lua_State *L, const char *name, int global)
{
    lj_err_callermsg(L, "no support for loading dynamic libraries for this OS");
    UNUSED(name);
    UNUSED(global);
    return NULL;
}

static void clib_unloadlib(CLibrary *cl) { UNUSED(cl); }

static void *clib_getsym(CLibrary *cl, const char *name)
{
    UNUSED(cl);
    UNUSED(name);
    return NULL;
}

    #endif

/* -- C library indexing -------------------------------------------------- */

    #if LJ_TARGET_X86 && LJ_ABI_WIN
/* Compute argument size for fastcall/stdcall functions. */
static CTSize clib_func_argsize(CTState *cts, CType *ct)
{
    CTSize n = 0;
    while (ct->sib) {
        CType *d;
        ct = ctype_get(cts, ct->sib);
        if (ctype_isfield(ct->info)) {
            d = ctype_rawchild(cts, ct);
            n += ((d->size + 3) & ~3);
        }
    }
    return n;
}
    #endif

/* Get redirected or mangled external symbol. */
static const char *clib_extsym(CTState *cts, CType *ct, GCstr *name)
{
    if (ct->sib) {
        CType *ctf = ctype_get(cts, ct->sib);
        if (ctype_isxattrib(ctf->info, CTA_REDIR))
            return strdata(gco2str(gcref(ctf->name)));
    }
    return strdata(name);
}

/* Index a C library by name. */
TValue *lj_clib_index(lua_State *L, CLibrary *cl, GCstr *name)
{
    TValue *tv = lj_tab_setstr(L, cl->cache, name);
    if (LJ_UNLIKELY(tvisnil(tv))) {
        CTState *cts = ctype_cts(L);
        CType *ct;
        CTypeID id = lj_ctype_getname(cts, &ct, name, CLNS_INDEX);
        if (!id)
            lj_err_callerv(L, LJ_ERR_FFI_NODECL, strdata(name));
        if (ctype_isconstval(ct->info)) {
            CType *ctt = ctype_child(cts, ct);
            lua_assert(ctype_isinteger(ctt->info) && ctt->size <= 4);
            if ((ctt->info & CTF_UNSIGNED) && (int32_t)ct->size < 0)
                setnumV(tv, (lua_Number)(uint32_t)ct->size);
            else
                setintV(tv, (int32_t)ct->size);
        }
        else {
            const char *sym = clib_extsym(cts, ct, name);
    #if LJ_TARGET_WINDOWS
            DWORD oldwerr = GetLastError();
    #endif
            void *p = clib_getsym(cl, sym);
            GCcdata *cd;
            lua_assert(ctype_isfunc(ct->info) || ctype_isextern(ct->info));
    #if LJ_TARGET_X86 && LJ_ABI_WIN
            /* Retry with decorated name for fastcall/stdcall functions. */
            if (!p && ctype_isfunc(ct->info)) {
                CTInfo cconv = ctype_cconv(ct->info);
                if (cconv == CTCC_FASTCALL || cconv == CTCC_STDCALL) {
                    CTSize sz = clib_func_argsize(cts, ct);
                    const char *symd = lj_str_pushf(
                        L, cconv == CTCC_FASTCALL ? "@%s@%d" : "_%s@%d", sym,
                        sz);
                    L->top--;
                    p = clib_getsym(cl, symd);
                }
            }
    #endif
            if (!p)
                clib_error(L, "cannot resolve symbol " LUA_QS ": %s", sym);
    #if LJ_TARGET_WINDOWS
            SetLastError(oldwerr);
    #endif
            cd = lj_cdata_new(cts, id, CTSIZE_PTR);
            *(void **)cdataptr(cd) = p;
            setcdataV(L, tv, cd);
        }
    }
    return tv;
}

/* -- C library management ------------------------------------------------ */

/* Create a new CLibrary object and push it on the stack. */
static CLibrary *clib_new(lua_State *L, GCtab *mt)
{
    GCtab *t = lj_tab_new(L, 0, 0);
    GCudata *ud = lj_udata_new(L, sizeof(CLibrary), t);
    CLibrary *cl = (CLibrary *)uddata(ud);
    cl->cache = t;
    ud->udtype = UDTYPE_FFI_CLIB;
    /* NOBARRIER: The GCudata is new (marked white). */
    setgcref(ud->metatable, obj2gco(mt));
    setudataV(L, L->top++, ud);
    return cl;
}

/* Load a C library. */
void lj_clib_load(lua_State *L, GCtab *mt, GCstr *name, int global)
{
    void *handle = clib_loadlib(L, strdata(name), global);
    CLibrary *cl = clib_new(L, mt);
    cl->handle = handle;
}

/* Unload a C library. */
void lj_clib_unload(CLibrary *cl)
{
    clib_unloadlib(cl);
    cl->handle = NULL;
}

/* Create the default C library object. */
void lj_clib_default(lua_State *L, GCtab *mt)
{
    CLibrary *cl = clib_new(L, mt);
    cl->handle = CLIB_DEFHANDLE;
}

#endif
