Version 2 of LuaJIT

Updated 2017-06-04 04:38:11 by dbohdan

LuaJIT is a remarkably fast JIT compiler that is source- and binary extension-compatible with Lua 5.1.

LuaJIT bindings with Critcl

dbohdan 2017-06-04: This module lets you define procs in JIT-compiled Lua. With it you can write fast numerical or binary data-processing commands in a high-level, garbage-collected language instead of C. The bindings translate Lua tables to Tcl lists (when Lua procs return those), but not vice versa. To use the module you will need Tcl 8.5 or 8.6, LuaJIT 2.x (Lua 5.1-5.3 are also supported, but won't give you the same dramatic speed boost), pkg-config and Critcl 3.1.11 or later. Earlier version of Critcl will not work.

Unlike tcl-duktape, this extension does not manage multiple instances of the guest language per host interpreter (it was easier to prototype this way and based on my past experience I think YAGNI). If you need multiple LuaJIT instances you can create a Tcl interpreter for each.

# Tcl bindings for LuaJIT and Lua 5.1-5.3.
# This module requires a recent version of Critcl, pkg-config and a LuaJIT 2.x
# or a Lua 5.1-5.3 development package.
# Copyright (c) 2017 dbohdan.
# License: MIT.
package require critcl 3.1.11

if {![::critcl::compiling]} {
    error {critcl found no compiler}
}

namespace eval ::luajit {
    variable bindingsVersion 0.1.0
    # Set the variable ::luajit::luaPackage in your code (set or
    # ::luajit::luaHeaders and ::luajit::luaLib directly) before sourcing or
    # requiring this module to use a different version of Lua.
    if {![info exists luaPackage]} {
        # The default Lua package.

        # variable luaPackage lua5.1
        # variable luaPackage lua5.2
        # variable luaPackage lua5.3
        variable luaPackage luajit
    }
    if {![info exists luaHeaders]} {
        variable luaHeaders [exec pkg-config --cflags $luaPackage]
    }
    if {![info exists luaLib]} {
        variable luaLib [exec pkg-config --libs $luaPackage]
    }
}

critcl::cheaders $::luajit::luaHeaders
critcl::ccode {
    #include <lua.h>
    #include <lauxlib.h>
    #include <lualib.h>
    #define LUAJIT_CDATA ((lua_State *) cdata)
}

critcl::clibraries $::luajit::luaLib

critcl::cinit {
    lua_State *L = luaL_newstate();
    luaL_openlibs(L);
} {}

critcl::ccommand luajit::eval {cdata interp objc objv} {
    size_t len;
    int rc;
    const char *lua_res;

    if (objc != 2) {
        Tcl_WrongNumArgs(interp, 1, objv, "code");
        return TCL_ERROR;
    }

    rc = luaL_loadstring(LUAJIT_CDATA, Tcl_GetStringFromObj(objv[1], NULL)) ||
         lua_pcall(LUAJIT_CDATA, 0, 1, 0);
    lua_res = lua_tolstring(LUAJIT_CDATA, -1, &len);
    Tcl_SetObjResult(interp, Tcl_NewByteArrayObj(lua_res, len));
    lua_pop(LUAJIT_CDATA, 1);

    if (rc == 0) {
        return TCL_OK;
    } else {
        return TCL_ERROR;
    }
} -clientdata L

critcl::ccode {
    Tcl_Obj* table2tcl(ClientData cdata, Tcl_Interp *interp,
                       lua_State *L, int index) {
        Tcl_Obj* result = Tcl_NewListObj(0, NULL);
        lua_pushnil(L);
        index--;

        while (lua_next(L, index) != 0) {
            Tcl_Obj* kv[2];
            int i;
            for (i = 0; i < 2; i++) {
                int kv_index = -2 + i;
                if (lua_istable(L, kv_index)) {
                    kv[i] = table2tcl(cdata, interp, L, kv_index);
                    if (kv[i] == NULL) {
                        return NULL;
                    }
                } else {
                    /* Can't call lua_tolstring() unconditionally because it
                       changes the internal representation of the value, which
                       will throw off lua_next() if the value is a number. */
                    if (lua_isnumber(LUAJIT_CDATA, kv_index)) {
                        kv[i] = Tcl_NewWideIntObj(
                            lua_tonumber(LUAJIT_CDATA, kv_index)
                        );
                    } else {
                        size_t lua_str_len;
                        const char* lua_str = lua_tolstring(LUAJIT_CDATA,
                                                            kv_index,
                                                            &lua_str_len);
                        kv[i] = Tcl_NewByteArrayObj(lua_str, lua_str_len);            
                    }
                }
                if (Tcl_ListObjAppendElement(interp, result, kv[i]) != TCL_OK) {
                    return NULL;
                }
            }
            lua_pop(L, 1);
        }
        return result;
    }
}

critcl::ccommand luajit::call {cdata interp objc objv} {
    int err;
    int i;
    int len;
    int level = lua_gettop(LUAJIT_CDATA);
    int nresults = -1;
    int listc;
    Tcl_Obj **listv;
    Tcl_Obj *results;

    if (objc < 2) {
        Tcl_WrongNumArgs(interp, 1, objv, "funcpath ?arg ...?");
        return TCL_ERROR;
    }

    /* Traverse tables to find the function to call. */
    if (Tcl_ListObjGetElements(interp, objv[1], &listc, &listv) != TCL_OK) {
        Tcl_SetObjResult(
            interp,
            Tcl_NewStringObj("can't process function path list", -1)
        );
        return TCL_ERROR;
    }
    if (listc == 0) {
        Tcl_SetObjResult(
            interp,
            Tcl_NewStringObj("function path can't be empty", -1)
        );
        return TCL_ERROR;
    }
    lua_getglobal(LUAJIT_CDATA, Tcl_GetStringFromObj(listv[0], NULL));
    if (lua_isnil(LUAJIT_CDATA, -1)) {
        Tcl_SetObjResult(
            interp,
            Tcl_NewStringObj("global Lua variable not found", -1)
        );
        lua_pop(LUAJIT_CDATA, 1);
        return TCL_ERROR;
    }
    for (i = 1; i < listc; i++) {
        lua_getfield(LUAJIT_CDATA, -1, Tcl_GetStringFromObj(listv[i], NULL));
        lua_remove(LUAJIT_CDATA, -2);
    }
    if (lua_isnil(LUAJIT_CDATA, -1)) {
        Tcl_SetObjResult(
            interp,
            Tcl_NewStringObj("Lua function not found", -1)
        );
        lua_pop(LUAJIT_CDATA, 1);
        return TCL_ERROR;
    }

    /* Push the arguments. */
    for (i = 2; i < objc; i++) {
        char* arg = Tcl_GetByteArrayFromObj(objv[i], &len);
        lua_pushlstring(LUAJIT_CDATA, arg, len);
    }

    /* Call the function. */
    err = lua_pcall(LUAJIT_CDATA, objc - 2, LUA_MULTRET, 0);
    nresults = lua_gettop(LUAJIT_CDATA) - level;

    /* Process multiple return values. */
    results = Tcl_NewListObj(0, NULL);
    for (i = nresults - 1; i >= 0; i--) {
        Tcl_Obj *elem = NULL;
        int index = -1 - i;
        if (lua_istable(LUAJIT_CDATA, index)) {
            elem = table2tcl(cdata, interp, LUAJIT_CDATA, index);
            if (elem == NULL) {
                Tcl_SetObjResult(
                    interp,
                    Tcl_NewStringObj("can't convert Lua table to list", -1)
                );
                err = 1;
                break;
            }
        } else {
            size_t lua_str_len;
            const char* lua_str = lua_tolstring(LUAJIT_CDATA, index,
                                                &lua_str_len);
            elem = Tcl_NewByteArrayObj(lua_str, lua_str_len);
        }
        if (Tcl_ListObjAppendElement(interp, results, elem) != TCL_OK) {
            Tcl_SetObjResult(
                interp,
                Tcl_NewStringObj("can't create result list", -1)
            );
            err = 1;
            break;
        }
    }
    if (nresults == 1) {
        Tcl_Obj* result;
        Tcl_ListObjIndex(interp, results, 0, &result);
        Tcl_SetObjResult(interp, result);
    } else {
        Tcl_SetObjResult(interp, results);
    }

    lua_pop(LUAJIT_CDATA, nresults);

    if (err) {
        return TCL_ERROR;
    } else {
        return TCL_OK;
    }
} -clientdata L

proc ::luajit::safe-name {text} {
    string trim [regsub -all {[^[:alnum:]]+} $text _] _
}

proc ::luajit::ljproc {name arglist body} {
    set luaName [safe-name $name]
    set luaArgs {}
    set procBody "luajit::call $luaName"
    foreach arg $arglist {
        if {$arg eq {args}} {
            append procBody " {*}\$args"
            lappend luaArgs ...
        } else {
            append procBody " \$$arg"
            lappend luaArgs $arg
        }
    }
    ::luajit::eval "function ${luaName}([join $luaArgs ,])\n$body\nend"
    proc $name $arglist $procBody
}

proc ::luajit::assert-equal {actual args} {
    set matched 0
    foreach expected $args {
        if {$actual eq $expected} {
            set matched 1
            break
        }
    }
    if {!$matched} {
        if {[string length $actual] > 200} {
            set actual [string range $actual 0 199]...
        }
        error "expected \"[join $args {" or "}]\",\n\
               but got \"$actual\""
    }
}

proc ::luajit::dict-sort d {
    set res {}
    foreach key [lsort [dict keys $d]] {
        set value [dict get $d $key]
        if {[llength $key] % 2 == 0} {
            set key [dict-sort $key]
        }
        if {[llength $value] % 2 == 0} {
            set value [dict-sort $value]
        }
        lappend res $key $value
    }
    return $res
}

proc ::luajit::test {} {
    if [catch {luajit::eval {
        print("using " .. jit.version)
    }}] {
        luajit::eval {
            print("using " .. _VERSION)
        }
    }

    assert-equal [luajit::call {string find} abcdef cd] {3 4}

    catch {luajit::call {} abcdef cd} err
    assert-equal $err {function path can't be empty}

    catch {luajit::call {nope bogus} foo} err
    assert-equal $err {global Lua variable not found}

    catch {luajit::call {math bogus} foo} err
    assert-equal $err {Lua function not found}


    luajit::ljproc add {a b} {
        return a + b
    }
    assert-equal [luajit::call add 5 7] 12 12.0
    assert-equal [add 5 7] 12 12.0

    luajit::ljproc divmul {a b} {
        return math.floor(a / b), a % b, a, b
    }
    assert-equal [luajit::call divmul 7 3] {2 1 7 3} {2 1.0 7 3}
    assert-equal [divmul 7 3] {2 1 7 3} {2 1.0 7 3}

    luajit::ljproc varargs {a b c args} {
        return a, b, c, {...}
    }
    assert-equal [luajit::call varargs foo bar baz a b c] \
                 {foo bar baz {1 a 2 b 3 c}}
    assert-equal [varargs foo bar baz a b c] {foo bar baz {1 a 2 b 3 c}}

    catch {luajit::ljproc foo {} {blah}} err
    assert-equal $err \
                 {[string "function foo()..."]:3: '=' expected near 'end'} \
                 {[string "function foo()..."]:3: syntax error near 'end'}

    luajit::ljproc bar {} {return nope['bogus']}
    catch bar err
    assert-equal $err \
                 {[string "function bar()..."]:2: attempt to index global\
                  'nope' (a nil value)} \
                 {[string "function bar()..."]:2: attempt to index a nil value\
                  (global 'nope')}

    luajit::ljproc table-1 {} {
        return 1, {a = 1, b = 2, c = 3}, 3, 4, 5
    }
    set res [table-1]
    lset res 1 [luajit::dict-sort [lindex $res 1]]
    assert-equal $res {1 {a 1 b 2 c 3} 3 4 5}
    unset res

    luajit::ljproc table-2 {} {
        return {}
    }
    assert-equal [table-2] {}

    luajit::ljproc table-3 {} {
        return {2, 3, 4}
    }
    assert-equal [luajit::dict-sort [table-3]] {1 2 2 3 3 4}

    luajit::ljproc nested-table-1 {} {
        return {a = 1, b = 2, c = {3, 4, 5}, d = {e = 6, f = 7}}
    }
    assert-equal [luajit::dict-sort [nested-table-1]] \
                 {a 1 b 2 c {1 3 2 4 3 5} d {e 6 f 7}}

    luajit::ljproc nested-table-2 {} {
        local t1 = {a = 1, b = 2}
        local t2 = {hello = {-3, -2, -1}}
        t2[t1] = {0, 1, 2}
        return t2
    }
    assert-equal [luajit::dict-sort [nested-table-2]] \
                 {{a 1 b 2} {1 0 2 1 3 2} hello {1 -3 2 -2 3 -1}}


    # Benchmark.
    set s {}
    set refValue 0
    for {set i 0} {$i < 1024*1014} {incr i} {
        incr refValue [expr {$i % 256}]
        append s [format %c [expr {$i % 256}]]
    }
    proc char-sum-1 data {
        set sum 0
        set len [string length $data]
        for {set i 0} {$i < $len} {incr i} {
            binary scan $data "@$i cu" byte
            incr sum $byte
        }
        return $sum
    }
    proc char-sum-2 data {
        set sum 0
        foreach c [split $data {}] {
            scan $c %c byte
            incr sum $byte
        }
        return $sum
    }
    luajit::ljproc lj-sum data {
        local sum = 0
        for i = 1, #data do
            sum = sum + data:byte(i, i)
        end
        return sum
    }
    assert-equal [char-sum-1 $s] $refValue
    assert-equal [char-sum-2 $s] $refValue
    assert-equal [lj-sum $s] $refValue
    puts [time {char-sum-1 $s} 5]
    puts [time {char-sum-2 $s} 5]
    puts [time {lj-sum $s} 5]
    puts [time {::luajit::call lj_sum $s} 5]
}

# If this is the main script...
if {[info exists argv0] && ([file tail [info script]] eq [file tail $argv0])} {
    ::luajit::test
}

Creating a Tcl interpreter in LuaJIT

dbohdan 2016-09-17: LuaJIT comes stock with a really nice FFI library . The code below is a translation of PYK's example from the Ffidl page that shows how to create a Tcl interpreter in LuaJIT.

#! /usr/bin/env luajit

ffi = require("ffi")
-- The following line is for openSUSE Tumbleweed. If you run a different OS
-- you should probably replace it with something like
-- local tcl = ffi.load("tcl8.6")
local tcl = ffi.load("/usr/lib64/libtcl8.6.so")
ffi.cdef[[
typedef struct Tcl_Interp Tcl_Interp;
typedef struct Tcl_Obj Tcl_Obj;
typedef struct Tcl_DString {
    char *string;
    int length;
    int spaceAvl;
    char staticSpace[200];
} Tcl_DString;

Tcl_Obj *        Tcl_NewStringObj(const char *bytes, int length);
Tcl_Interp *     Tcl_CreateInterp(void);
char *           Tcl_GetCwd(Tcl_Interp *interp, Tcl_DString *cwdPtr);
int              Tcl_InterpDeleted(Tcl_Interp *interp);
char *           Tcl_GetString(Tcl_Obj *objPtr);
int              Tcl_EvalObjEx(Tcl_Interp *interp, Tcl_Obj *objPtr, int flags);
]]

local interp = tcl.Tcl_CreateInterp()
local script = tcl.Tcl_NewStringObj("puts [pwd]", -1)
tcl.Tcl_EvalObjEx(interp, script, 0)

local pwd = ffi.new("Tcl_DString")
tcl.Tcl_GetCwd(interp, pwd)
print(ffi.string(pwd.string))

Discussion