LuaJIT

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 a Lua proc returns 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 dramatic speed boost), pkg-config and Critcl 3.1.11 or later. Earlier versions 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 separate Tcl interpreter for each.

Code

# 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 for your OS.
# 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.1
    # 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::clibraries $::luajit::luaLib
critcl::ccode {
    #include <lua.h>
    #include <lauxlib.h>
    #include <lualib.h>
    #define LUAJIT_CDATA ((lua_State *) cdata)
}

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. It
                       will throw off lua_next() if a value that was a number
                       becomes a string. */
                    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-is-one-of {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::assert-equal {actual expected} {
    ::luajit::assert-is-one-of $actual $expected
}

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-is-one-of [luajit::call add 5 7] 12 12.0
    assert-is-one-of [add 5 7] 12 12.0

    luajit::ljproc divmul {a b} {
        return math.floor(a / b), a % b, a, b
    }
    assert-is-one-of [luajit::call divmul 7 3] {2 1 7 3} {2 1.0 7 3}
    assert-is-one-of [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-is-one-of $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-is-one-of $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
}

Benchmark results

using LuaJIT 2.0.4
798469.2 microseconds per iteration
682693.4 microseconds per iteration
2614.4 microseconds per iteration
2527.8 microseconds per iteration

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