Posts
Search
Contact
Cookies
About
RSS

Lua and Foreign Function Interface (FFI) fun!

Added 2 Dec 2015, 8:42 p.m. edited 18 Jun 2023, 1:12 a.m.
I've always had a soft spot for Lua and although I'm not actively using it in any projects at the moment, I frequently make a point of seeing what's happening with it. There are a number of design decisions with Lua that leave you with a powerful yet lightweight tool which above all because of its genius simplicity makes it very easy to use both as an end user (for example a games modder) and a developer wanting to embed a language into their game. It has to be said however that Lua isn't the fastest, while the interpreted code is compiled to a kind of halfway house byte code, its not as speedy as say for example Java. This lead me to have a look at LuaJit - which is basically in effect a reimplemented Lua but with a Just In Time (JIT) compiler. However it wasn't so much the speed of LuaJIT that interested me but also LuaJIT's foreign function interface (FFI). What's really impressed me is just how really very easy the FFI is to work with, lets take a less than trivial example OpenGLES or actually before you get there GLFW, if found in the past the GLFW just makes a whole heap of issues go away and allows you very quickly set up a context for OpenGL, which just works without drama on its supported platforms Now I should mention this isn't a full blown attempt to bind GLFW for lua but rather a quick hack to show the FFI being used in anger - if you really want to use GLFW in your Lua project I'd suggest something like luajit-glfw which is a far more thorough job! Anyhow its long past time to look at some code!!!
local ffi = require("ffi")
ffi.cdef[[
 
 static const int GL_COLOR_BUFFER_BIT = 0x00004000;
 
 void glClearColor(float r, float g, float b, float a);
 void glClear(int buffers);

 static const int GLFW_CLIENT_API = 0x00022001;
 static const int GLFW_CONTEXT_VERSION_MAJOR = 0x00022002;
 static const int GLFW_CONTEXT_VERSION_MINOR = 0x00022003;
 static const int GLFW_OPENGL_API = 0x00030001;
 static const int GLFW_OPENGL_ES_API = 0x00030002;

 typedef void(* GLFWerrorfun )(int, const char *);
 typedef struct GLFWwindow GLFWwindow;
 typedef struct GLFWmonitor GLFWmonitor;
 
 int glfwInit (void);
 void glfwTerminate (void);
 void glfwGetVersion (int *major, int *minor, int *rev);
 const char* glfwGetVersionString (void);
 GLFWerrorfun glfwSetErrorCallback (GLFWerrorfun cbfun);
 GLFWwindow* glfwCreateWindow (int width, int height, const char *title, GLFWmonitor *monitor, GLFWwindow *share);
 void glfwMakeContextCurrent (GLFWwindow *window);
 int glfwWindowShouldClose (GLFWwindow *window);
 void glfwSwapBuffers (GLFWwindow *window);
 void glfwPollEvents (void);
 void glfwDefaultWindowHints (void);
 void glfwWindowHint (int target, int hint);

]] -- TODO #define ??
Straight away you can see from my TODO comment I'm just doing the constants with the first idea that worked (really they should be part of the Lua objects meta). After getting a reference to the FFI module we can immediately start defining out C function we wish to access - this is done in almost the same manner as you'd expect to do in a C header file... Actually "linking" to the library is equally straight forward
local gles = ffi.load(ffi.os == "Windows" and "OpenGL32" or "GLESv2")
local glfw = ffi.load("glfw")

local intPtr = ffi.typeof("int[1]")
local longPtr = ffi.typeof("long[1]")
You can easily specify different library names depending on your platform (for example here I'd use a subset of OpenGL on windows and GLES2.0 everywhere else) also notice intPtr and longPtr this will allow us to create int and long pointer, which you can supply to any C function that takes a pointer (often to modify the structure or value its pointing to) take for example glfwGetVersion
local var major = intPtr()
local var minor = intPtr()
local var rev = intPtr()

glfw.glfwGetVersion(major, minor, rev)
Once the C function is called it will return with the lua variable set as you'd expect incidentally its quite possible to make for example a glfw object containing a getVersion function take a look at luajit-glfw's version and you'll see what I mean
function mod.GetVersion()
  local major = ffi.new('int[1]')
  local minor = ffi.new('int[1]')
  local rev = ffi.new('int[1]')

  bind.glfwGetVersion(major, minor, rev)

  local version = {
    major = major[0],
    minor = minor[0],
    rev = rev[0]
  }

  return version
end
The mod object later becomes part of a glfw objects meta allowing you to call glfw.getVersion() which returns a table with the three version values, much more in the Lua style but with a slight overhead, but it is a lot neater that a quick try it and see hack! The way callbacks are handled I found really much easier than many languages FFI's
    typedef void(*GLFWerrorfun )(int, const char *);

    GLFWerrorfun glfwSetErrorCallback (GLFWerrorfun cbfun);
]]

...

glfw.glfwSetErrorCallback(
  function(err, desc)
    print ("error code " .. err )
    print ("error text " .. ffi.string(desc))
  end
)

Just like passing pointers, this isn't the PITA is all too often is with other languages! From here everything else is like shelling peas   here's the complete hack ....
local ffi = require("ffi")
ffi.cdef[[
    
  static const int GL_COLOR_BUFFER_BIT          =   0x00004000;
      
  void glClearColor(float r, float g, float b, float a);
  void glClear(int buffers);

  static const int GLFW_CLIENT_API              =   0x00022001;
  static const int GLFW_CONTEXT_VERSION_MAJOR   =   0x00022002;
  static const int GLFW_CONTEXT_VERSION_MINOR   =   0x00022003;
  static const int GLFW_OPENGL_API              =   0x00030001;
  static const int GLFW_OPENGL_ES_API           =   0x00030002;

  typedef void(*     GLFWerrorfun )(int, const char *);
  typedef struct GLFWwindow GLFWwindow;
  typedef struct GLFWmonitor GLFWmonitor;

  int glfwInit (void);
  void glfwTerminate (void);
  void glfwGetVersion (int *major, int *minor, int *rev);
  const char* glfwGetVersionString (void);
  GLFWerrorfun glfwSetErrorCallback (GLFWerrorfun cbfun);
  GLFWwindow* glfwCreateWindow (int width, int height, const char *title, GLFWmonitor *monitor, GLFWwindow *share);
  void glfwMakeContextCurrent (GLFWwindow *window);
  int glfwWindowShouldClose (GLFWwindow *window);
  void glfwSwapBuffers (GLFWwindow *window);
  void glfwPollEvents (void);
  void glfwDefaultWindowHints (void);
  void     glfwWindowHint (int target, int hint);

]]    -- TODO #define ??

-- use libGLESv2.so everywhere except use a subset of GL2.0 in windows
local gles = ffi.load(ffi.os == "Windows" and "OpenGL32" or "GLESv2")
local glfw = ffi.load("glfw")

local intPtr = ffi.typeof("int[1]")
local longPtr = ffi.typeof("long[1]")

glfw.glfwSetErrorCallback(
  function(err, desc)
    print ("error code " .. err )
    print ("error text " .. ffi.string(desc))
  end
)

if not glfw.glfwInit() then
  print "can't initialise glfw"
  return
end

glfw.glfwDefaultWindowHints();     

if ffi.os == "Windows" then
  glfw.glfwWindowHint (glfw.GLFW_CLIENT_API, glfw.GLFW_OPENGL_API);
else
  glfw.glfwWindowHint (glfw.GLFW_CLIENT_API, glfw.GLFW_OPENGL_ES_API);  
end
glfw.glfwWindowHint (glfw.GLFW_CONTEXT_VERSION_MAJOR, 2);
glfw.glfwWindowHint (glfw.GLFW_CONTEXT_VERSION_MINOR, 0);             
    

local var major = intPtr()
local var minor = intPtr()
local var rev = intPtr()

glfw.glfwGetVersion(major, minor, rev)
print ("initialised version " .. major[0] .. "." .. minor[0] .. "." .. rev[0] )
print ( ffi.string( glfw.glfwGetVersionString() ) )

local var window = longPtr() -- TODO is this the right way to do it? (need to be able to nil check)
window = glfw.glfwCreateWindow(640, 480, "Hello World", NULL, NULL);

if window==nil then
  print "could not create window"
  glfw.glfwTerminate();
  return
end

glfw.glfwMakeContextCurrent(window)
local var t=0
local abs=math.abs
local sin=math.sin
while glfw.glfwWindowShouldClose(window)==0 do
  t=t+0.001
  gles.glClearColor(abs(sin(t)),abs(sin(t/2.2)),abs(sin(-t/1.7)),1.0)
  gles.glClear(gles.GL_COLOR_BUFFER_BIT);
  glfw.glfwSwapBuffers(window);
  glfw.glfwPollEvents();
end

glfw.glfwTerminate();

return