Inline Cee

lua-users home
wiki

Here's a simple module that allows C code to be embedded inline in Lua code. The module invokes your C compiler (e.g. gcc) to turn the C code into a Lua function that can be executed in Lua.

Example:

local CC = require "inlinec"

local f = CC.compile [[
  int start(lua_State * L) {
    luaL_checkstring(L,1);
    lua_pushstring(L, "hello ");
    lua_pushvalue(L, 1);
    lua_concat(L, 2);
    return 1;
  }
]]

print(f("world"))  --> "hello world"

Implementation:

-- inlinec.lua
-- (c) D.Manura, 2008.
-- Licensed under the same terms as Lua itself (MIT license).
local M = {}

M.debug = false

local preamble = [[
#include <lua.h>
#include <lauxlib.h>
#include <stdio.h>
#include <stdlib.h>
]]

-- Count number of lines in string.
local function numlines(s)
  return # s:gsub("[^\n]", "")
end

-- Add lines to string so that any compile errors
-- properly indicate line numbers in this file.
local function adjustlines(src, level, extralines)
  local line = debug.getinfo(level+1,'l').currentline
  return ("\n"):rep(line - numlines(src) - extralines) .. src
end

-- Create temporary file containing text and extension.
local function make_temp_file(text, ext)
  local filename = os.tmpname() .. '.' .. ext
  local fh = assert(io.open(filename, 'w'))
  fh:write(text)
  fh:close()
  return filename
end

-- Create temporary header file with preamble.
-- The preamble is placed in a separate file so as not
-- to increase line numbers in compiler errors.
local pre_filename
local function make_preamble()
  if not pre_filename then
    pre_filename = make_temp_file(preamble, 'h')
  end
  return pre_filename
end

-- Execute command.
local function exec(cmd)
  if M.debug then print(cmd) end
  assert(os.execute(cmd) == 0, 'command failed')
end

-- Compile C source, returning corresponding Lua function.
-- Function must be named 'start' in C.
local function compile(src)
  local CC = 'gcc -O2'

  local pre_filename = make_preamble()
  src = ('#include %q\n'):format(pre_filename) .. src
  src = adjustlines(src, 2, 1)

  local modname = os.tmpname() .. '.so'

  local srcname = make_temp_file(src, "c")
  local cmd = CC .. " -shared -o '" .. modname .. "' '" .. srcname .. "' -llua"
  exec(cmd)

  local func = assert(package.loadlib(modname, 'start'))
  return func
end
M.compile = compile

return M

Possible improvements:

Possible applications:

Take #2

To avoid implementing all the platform-dependent code, we might instead reuse LuaRocks to do the building. LuaRocks already abstracts away the platform-dependencies. Example:

-- Example of compiling inline C code using LuaRocks to
-- do the compilation.
-- Tested on LuaRocks 2008-08-16 CVS version.
-- Warning: this was quickly put together for demonstration
-- purposes and might not be robust.
-- D.Manura, 2008-06. 

local build = require "luarocks.build.builtin"
local fs = require "luarocks.fs"
local type_check = require "luarocks.type_check"
local path = require "luarocks.path"
local deps = require "luarocks.deps"
local util = require "luarocks.util"

-- Create temporary file containing text and extension.
local function make_temp_file(text, ext)
  local filename = os.tmpname() .. '.' .. ext
  local fh = assert(io.open(filename, 'w'))
  fh:write(text)
  fh:close()
  return filename
end

-- C code to compile.
local srcfilename = make_temp_file([[
#include <lua.h>
#include <lauxlib.h>
  int hello(lua_State * L) {
    luaL_checkstring(L,1);
    lua_pushstring(L, "hello ");
    lua_pushvalue(L, 1);
    lua_concat(L, 2);
    return 1;
  }
  int luaopen_mymodule(lua_State * L) {
    lua_pushcfunction(L, hello);
    return 1;
  }
]], 'c')


-- LuaRock .rockspec definition
-- (provides build instructions)
local rs = {
  package = "MYPACKAGE",
  version = "1.0.0-1",
  source = { url = "unused" },
  build = {
    type = "module",
    modules = {
      mymodule = {
        srcfilename,
        libraries = {"lua"}
      }
    },
  }
}

-- This long function is based on fetch.lua:load_local_rockspec
-- but differs in that it takes a Lua table rather than a file name
-- as input.
-- FIX: It may be best if such a function were instead incorporated
-- into LuaRocks fetch.lua to avoid redundancy.
function create_rockspec(t, filename)
   assert(type(filename) == "string")

   local rockspec, err = t
   if not rockspec then
      return nil, "Could not load rockspec file "..filename.." ("..err..")"
   end

   local ok, err = type_check.type_check_rockspec(rockspec)
   if not ok then
      return nil, filename..": "..err
   end
   
   if rockspec.rockspec_format then
      if deps.compare_versions(
         rockspec.rockspec_format, type_check.rockspec_format)
      then
         return nil, "Rockspec format "..rockspec.rockspec_format..
                     " is not supported, please upgrade LuaRocks."
      end
   end

   util.platform_overrides(rockspec.build)
   util.platform_overrides(rockspec.dependencies)
   util.platform_overrides(rockspec.external_dependencies)
   util.platform_overrides(rockspec.source)

   local basename = fs.base_name(filename)
   rockspec.name = basename:match("(.*)-[^-]*-[0-9]*")
   if not rockspec.name then
      return nil, "Expected filename in format 'name-version-revision.rockspec'."
   end

   local protocol, pathname = fs.split_url(rockspec.source.url)
   if protocol == "http" or protocol == "https" or
      protocol == "ftp" or protocol == "file"
   then
      rockspec.source.file = rockspec.source.file or fs.base_name(rockspec.source.url)
   end
   rockspec.source.protocol, rockspec.source.pathname = protocol, pathname

   -- Temporary compatibility
   if not rockspec.source.module then
      rockspec.source.module = rockspec.source.cvs_module
      rockspec.source.tag = rockspec.source.cvs_tag
   end

   local name_version = rockspec.package:lower() .. "-" .. rockspec.version
   if basename ~= name_version .. ".rockspec" then
      return nil, "Inconsistency between rockspec filename ("..
          basename..") and its contents ("..name_version..".rockspec)."
   end

   rockspec.local_filename = filename
   local filebase = rockspec.source.file or rockspec.source.url
   local base = fs.base_name(filebase)
   base = base:gsub("%.[^.]*$", ""):gsub("%.tar$", "")
   rockspec.source.dir = rockspec.source.dir
                      or rockspec.source.module
                      or ((filebase:match(".lua$") or filebase:match(".c$")) and ".")
                      or base

   if rockspec.dependencies then
      for i = 1, #rockspec.dependencies do
         local parsed = deps.parse_dep(rockspec.dependencies[i])
         if not parsed then
            return nil, "Parse error processing dependency '"..rockspec.dependencies[i].."'"
         end
         rockspec.dependencies[i] = parsed
      end
   else
      rockspec.dependencies = {}
   end
   local ok, err = path.configure_paths(rockspec)
   if err then
      return nil, "Error verifying paths: "..err
   end

   return rockspec
end


-- Build module
rs = assert(create_rockspec(rs, "mypackage-1.0.0-1.rockspec"))
assert(build.run(rs))

-- Load module
local hello = require "mymodule"
print(hello("world")) --> "hello world"

--DavidManura

See Also


RecentChanges · preferences
edit · history
Last edited August 17, 2008 4:01 am GMT (diff)