Lua Technical Note 4

A thin API for interlanguage working, or Lua in Four Easy Calls

by Reuben Thomas

Abstract

The obvious way to make Lua interwork with language L is to implement the Lua API in L, but this is daunting for the implementor, and burdens the L programmer with a verbose syntax. A simpler solution is to implement just lua_open, lua_close, lua_dobuffer and lua_register in L, expanding lua_register to do inter-language marshalling. Additional functionality can then be provided in Lua.

The problem

The Lua API allows a Lua state to be completely controlled from C. But what if you want to interwork with another language, L? The obvious approach, assuming that L can interwork with C, is to reflect the C API into L. However, this is a rather daunting prospect, as the Lua API is quite large and has some hidden subtleties. Also, while it is a good medium for writing extension libraries and tools for Lua, it does not lead naturally to a convenient syntax for L programmers; the Lua manual shows (see section 5.12 on p. 26 in the 4.0 edition) how the Lua statement

a,b = f("how", t.x, 4)

becomes ten calls to the Lua API.

The solution

There is an easier way: using the first rule of Lua ("Do it in Lua"):

lua_dostring(S, "a,b = f(\"how\", t.x, 4)");

where S is the state in which the code is to be executed. In fact, the only things you can't do with lua_dostring are get values back from Lua, and allow Lua to call L. These can both be achieved with lua_register.

Hence all that is needed for language interworking is lua_dostring and lua_register, plus lua_open and lua_close to allow Lua states to be created and destroyed. Also, it's better to use lua_dobuffer than lua_dostring, as it can handle pre-compiled code too.

But wait! In the C API, lua_register says nothing about the argument or result types of the function being registered; these have to be dealt with by inspection and manipulation of the Lua stack. A brutal but simple solution to this is to make lua_register specify the type and number of arguments and return values, and allow only such types as map naturally into L.

The final list of functions for the thin API is:

Case study: Lua to OPL

When porting Lua to EPOC, Symbian's OS for mobile devices such as PDAs, I wanted to provide hooks to OS features such as the Eikon GUI. EPOC is C++-based, which looks promising, but for space reasons its libraries contain no symbol information, so run-time dynamic linking by name is impossible. Not wanting to resort to tolua, I decided instead to bind Lua to OPL, EPOC's interpreted BASIC-like RAD language, which has both good support for EPOC, including a wide range of OPXs (OPL libraries implemented in C++), and allows procedures to be called dynamically by name.

OPL has four basic types: 16-bit and 32-bit integers, 64-bit floats, and strings. 16-bit integers are denoted %, 32-bit integers &, strings $, and floats by nothing. OPL supports C-like function prototypes, for example:

foo&:(a,b%,c$)

foo is the name of the function. The & indicates that it returns a 32-bit integer (all OPL functions return a value, which defaults to zero or the empty string if there is no explicit RETURN statement). The colon indicates that foo is a function. Next comes the optional argument list; in this case, there are three arguments: a float a, a 16-bit integer b%, and a string c$. (Strings may be at most 255 characters long; in this API, longer strings may not be exchanged with Lua directly.)

Hence, I created a small OPX which provided the following OPL functions:

Lua&: seemed a better name than LuaDoBuffer&: as it is both apt (Lua&: is the function that does some Lua) and a nice short name for what is likely to be the most widely used procedure by far out of the four. When an OPL function registered by LuaRegister: is called from Lua, the arguments are automatically translated to the OPL types, and the result type translated back. It is the programmer's responsibility to check that integer arguments are in range.

Is a thin API enough?

At first sight, this interface may seem very limited. For example, there's no simple way to evaluate a Lua expression and return its result to OPL, nor is it possible to traverse Lua tables in OPL. This is intentional: adding these facilities would complicate the API, and omitting them encourages programmers to use OPL only to provide library routines to Lua. After all, the main motivation for linking Lua to OPL was to be able to access EPOC without needing to write lots of C++ libraries for Lua first.

However, in some cases I might want to write much of the application in the other language, because of its application domain properties (for example, SQL or Prolog). Also, I seem to be promoting Lua from its intended use as application extension language to the main language in which the application is written.

Actually, there is no conflict here. Think of Lua not so much as an application extension language as a glue language, binding bits of programs written in other languages together. The core of the application's functionality will often be implemented in some other language L, perhaps C for speed, or some domain-specific language. By structuring this core as a library, the L programmer is free to concentrate on providing application primitives in L, without worrying about tying them together; L may well not be suitable for this. The application can then be implemented as a layer of Lua on top of a series of libraries; this separates the different concerns of programming the domain-specific primitives in L from configuring the particular application, which makes the application easier to write, and promotes reuse of both Lua and L code.

If it is really necessary to implement other parts of the Lua API in L, then, provided it is not for performance reasons, the requisite functionality can still be implemented in Lua with L callbacks. Indeed, it would be possible to write a Lua implementation of the full Lua API which would then work with any language to which Lua was interfaced by the thin API.

Conclusion

Lua can be connected to other languages with a very simple API, which is mostly a subset of the standard C API. It is quick to implement, provided that the target language can interwork with C, and provides all the necessary functionality for writing applications in a mixture of Lua and the target language. Some seeming restrictions in the thin API actually help to write more reusable code.


Last update: Mon Aug 12 15:49:10 EST 2002 by lhf.