Embedding and Inter-Op with Lua 5.3

Introduction

The swog-lua bridge supports embedding a Lua 5.3 interpreter into a SN application, reading and writing Lua objects, and of course exposing SN objects to the Lua interpreter.

Example

  1. Create a Lua instance, load Lua standard libraries and SN extensions:

import lua._

val lua = Lua()
lua.init()
  1. Execute a script string that defines a global Lua table config:

lua.execString(
  """config = {
    |foo = 42,
    |  bar = {
    |    string = "Hello world!"
    |  }
    |}""".stripMargin)
  1. Access the values stored in config from SN:

lua.getGlobalValue("config") match {
  case Some(obj: LuaTable) =>
    println( obj.toMap() )  // convert the Lua table into a Scala map
}
  1. Define and register a Scala class that is accessible from Lua:

package bar
import scala.scalanative.scriptbridge._

@ScriptObj
class Foo(var num: Int) {
  def incr(): Int = num += 1
}

object Foo extends LuaModule {
}

lua.registerModule(Foo)
  1. Use SN class from Lua:

lua.execString(
    """-- load Scala class
      |Foo = scala.load("Foo")
      |-- create new instance
      |foo = Foo.new(42)
      |-- print current value of num
      |print(foo:num())
      |-- call Scala method incr()
      |foo:incr()
      |print(foo:num())
      |-- set num = -1
      |foo:setNum(-1)
      |print(foo:num())
      |""".stripMargin)
  1. Close Lua instance:

lua.free()

Calling Lua from Scala

TODO

Calling Scala from Lua

Three steps are required to expose a Scala class or object to a Lua interpreter:

  1. Create a class / object annotated with @ScriptObj,

  2. Register it to the Lua instance from which you want to call it,

  3. Load the Scala class into the Lua interpreter.

Basic Rules for Exposing Objects to Lua

Here is the basic template for SN classes exposed to Lua:

import scala.scalanative.scriptbridge._
import lua._

@ScriptObj
// the primary constructor will be exposed to Lua as function 'Foo.new(i)'
class Foo(i: Int) {

  // exposed to Lua as:
  //   foo:incr()     (get)
  //   foo:setIncr(i) (set)
  var incr: Int = 1

  // exposed to Lua as foo:add(i)
  def add(a: Int): Unit = // ...

  // not accessible from Lua
  private def bar(): Unit = // ...

}

// it's not required to extend LuaModule,
// but it will prevent IDEs from complaining when we call lua.registerModule(Foo)
object Foo extends LuaModule {
  // exposed to Lua as Foo.default()
  val default: Foo = new Foo(42)

  // exposed to Lua as Foo.bar(i)
  def bar(i: Int): Int = // ...
}

Keep the following rules in mind when you design your Lua bridge object:

  • You can export only a Scala object without a companion class, only a Scala class, or both. However, if you define both, only one can be annotated with @ScriptObj.

  • If you define a script bridge class Foo, its primary constructor is automatically exposed to Lua as Foo.new(). Secondary constructors are not exposed to Lua.

  • By default, every public method, val and var is exposed to Lua. However, if a member is annotated with @nolua it will not be exposed to Lua.

  • By default, the Lua name of a member is identical to the Scala name, with the prefix set for Scala setters. However, you can override this name by annotating the member with @luaname()`

  • All members exposed to Lua need an explicit type, i.e. the following will result in an error:

    @ScriptObj
    class Foo {
      var i = 42  // error: exposed member needs excplicit type annotation 'Int'
    }
    
  • Overloading of exposed methods is not supported, i.e. the following will result in a compile-time error:

    @ScriptObj
    class Foo {
      def bar(): Unit = // ...
      def bar(i: Int): Int = // ...
    }
    

Type Mapping

When a Scala method is called from Lua, the following mapping rules are applied to arguments and return values:

Boolean, Numbers, and Strings

Boolean, integer, and floating point numbers and strings will be translated into the corresponding Lua/Scala type.

However, keep in mind that Lua numbers will be represented as Long or Double in Scala by default. If you want to have an Int or Float you need to cast explicitly. This is particularly important if you access a Lua value from Scala with Lua.getValue(): Any, Lua.getGloablValue(): Option[Any], or LuaTable.get(): Option[Any], since a pattern match on Int or Float will fail.

Lua Tables (Objects)

If you want to pass a Lua table to a Scala method, you must define the corresponding Scala argument to be of type LuaTable or immutable.Map.

However, you should prefer LuaTable unless you will convert it to a Map anyway, since this will recursively convert all nested tables as well.

A Scala return value of type Map[String,Any] will ne converted into a Lua table.

Scala:

@ScriptObj
object Foo {
  def callWithTable(obj: LuaTable): Unit = {
    // get value of property 'foo'
    println( obj.getOrElse("foo",0) )
  }

  def callWithMap(obj: Map[String,Any]): Map[String,Any] = {
    // access Lua value obj.bar.string
    obj("bar") match {
      case m: Map[_,_] =>
        println( m("string") )
    }
    obj.updated("foo",43)
  }
}

Lua:

obj = {
  foo = 42
  bar = {
    string = "hello"
  }
}

Foo.callWithTable(obj)
upd = Foo.callWithMap(obj)
print( upd.foo ) -- prints 43

Lua nil and Option

A Scala Option will be converted to the corresponding Lua value (if it is Some()), or to Lua nil if it is None.

The inverse rule is applied for arguments of type Option[Any].

Scala:

@ScriptObj
object Foo {
  def withOption(in: Option[Any]): Option[Long] = in match {
    case Some(l: Long) => Some( l+1 )
    case _ => None
  }
}

Lua:

Foo.withOption(41)    -- returns 42
Foo.withOption(nil)   -- returns nil
Foo.withOption("foo") -- also returns nil

Advanced Topics

TODO