Bounces Native API

Using Lua Contexts in Your App

This document covers the basics for integrating a Bounces Lua Execution Context in your app. You will learn how to create and terminate a Lua Context, how to run Lua code in a Lua Context and how simple it is to exchange data between you app native code and a Lua Context.

Using Lua Contexts in Your App

Adding the Bounces framework to your application

The CIMLua framework adds the features of Bounces Lua Execution Contexts to your application.

Adding the CIMLua framework to your Xcode application project is generally managed from a Bounces project: when you set the associated Xcode project of a Bounces project, the CIMLua framework is automatically added to this associated Xcode project. See Bounces project creation and configuration for more details on this.

You can also simply add the CIMLua framework to an Xcode project by dragging the CIMLua framework into the target project (for example from an other Xcode project already including this framework).

When the CIMLua framework is part of your Xcode project, you classically have to import it before using any of the provided APIs:

import CIMLua

Lua Execution Contexts

When you integrate Bounces in an application, your native code will mainly use the LuaContext class. A LuaContext object encapsulate a complete Lua execution context, i.e. an autonomous Lua runtime, plus all Bounces extensions: object framework, native bridge, asynchronous features…

In your target app, a Lua Context is used to load and execute Lua code integrated with the native part of the application.

If needed, multiple LuaContext instances can be used in your app, each of these Lua contexts being fully independent of the others in term of memory, global state, or threading resources.

Obtaining a Lua Context

In the common case, when your app uses of a single Lua context, the easiest way to get one is to use the shared default Lua Context:

let myLuaContext = LuaContext.default

If no active Lua Context exists, CIMLuaContext.default creates one and returns it, otherwise it returns the first Lua context created by the application.

Controlling Lua Context creation

If your app needs more than one Lua context or if you want more control on Lua Context creation, you will rather use the LuaContext constructor:

let myLuaContext = LuaContext(name: contextName)

where the contextName parameter contains a unique name identifying the created Lua context.

Once a Lua context is created, you can retrieve it by name from anywhere in the app using:

let myLuaContext = LuaContext.named("MyContextName")

Terminating a Lua Context

When a Lua Context is not needed anymore, you should terminate it to prevent any Lua code from this Lua context to be executed in the future. Additionally, termination is required before a Lua Context can be deallocated.

myLuaContext.terminate()
myLuaContext = nil

Note that terminating the default Lua context can be used as a way to operate a clean restart, as a new default Lua Context will be created the next time LuaContext.default is called:

let myLuaContext = LuaContext.default

// ...

myLuaContext.terminate()
myLuaContext = LuaContext.default // Creates a new default Lua Context

Monitoring a Lua Context

By default, for security reason, a LuaContext created in your application does not communicate with the Bounces application, and features like dynamic code update or debugging are not enabled.

To enable the control of a Lua Context by the Bounces application, you call the enableMonitoring method on this Lua Context:

let luaContext = LuaContext.default
luaContext.enableMonitoring()

When a Lua Context is monitored, Lua debug becomes enabled and all Lua modules / resources loading requests are processed by the Bounces application.

Therefore, when enableMonitoring is called, the Lua Context asynchronously tries to connect to Bounces and suspends Lua modules loading until the connection is established, with a timeout of a few seconds, so that initially-loaded Lua modules actually come from your Bounces project and not from the target application's file system.

If the default value of this connection timeout is not sufficient for your specific network conditions, you can pass a timeout parameter to the enableMonitoring method:

luaContext.enableMonitoring(connectionTimeout: 15.0)

And because it is very common to use a default Lua Context for app development, a shortcut property is provided that returns the default Lua Context with monitoring enabled:

let luaContext = LuaContext.defaultMonitored

When you don't need Lua Context monitoring anymore, you can disable monitoring a Lua Context by calling:

luaContext.disableMonitoring()

Important! When you publish your application, you should check that no embedded Lua Context has monitoring enabled in the released version. Publishing an app with a monitored Lua Context in it makes this Lua Context potentially controllable by anyone on the same local network with the Bounces application!

Running Lua code

Once you have created a Lua Context, you need to run some Lua code in it. As we have seen in the Lua language overview, there are basically two ways of running Lua code: executing a Lua code string or loading a Lua module. Since Lua modules are by far the most commonly used technique in Bounces, because they enable dynamic code update, let's start with Lua modules.

Loading a Lua module

In a LuaContext, you load a Lua module by calling one of the loadModule methods. Loading a module from the app native code is equivalent to calling the require function in Lua.

The preferred form of loadModule is asynchronous:

luaContext.loadModule("Main", completion: { (result) in
     print("Lua module loaded", result ?? "no result")
})

This loads a Lua module named "Main" without blocking the current thread, and calls the completion handler in the app main thread when the module loading is complete, passing it the first module's result.

Often, the completion handler is not needed, so a nil completion is passed to loadModule to indicate that module loading is asynchronous. For example:

LuaContext.default.loadModule("StartModule", completion: nil)

In addition, although generally not recommended, you can load a Lua module synchronously, blocking the current thread until the module is loaded, by calling:

let moduleResult = luaContext.loadModule("SomeModule")

Controlling modules search locations

When a Lua module is loaded, either in Lua by calling the require function or in the app native code by calling the LuaContext method loadModule, a Lua file matching the provided module name is searched in the source locations defined for the current Lua context.

The document Bounces Lua modules provides a good overview of Bounces Lua modules search strategy, focussed on a typical development configuration when the current Lua context is monitored (as defined above in this document).

However, if the current Lua Context is not monitored or if no module matching the provided module name is found in the connected Bounces project, a module search is done locally in the target application, in the following order:

  1. If a Lua Package, exported from a Bounces project is embedded in the application and declared as default source location (typically by having set its location-identifier equal to the current Lua Context name), this Lua Package is searched first.

  2. Source directories can be defined for a given Lua Context, by calling method addSourceDirectory. For example, to add the app's documents directory to the source locations, you could write:

    if let documentsDirUrl = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first {
       luaContext.addSourceDirectory(documentsDirUrl)
    }
    

    In a source directory, Lua module paths are considered as Lua file paths relative to the source directory URL (with '.' in the module path replaced by '/').

  3. The application bundle's resource directory is always considered as a source directory, so you can add Lua files to your Xcode project and load them as Lua modules.

Executing a Lua code string

Beside modules, LuaContext provide methods to execute Lua code available as a string or file URL: the execute methods. These methods follow the same pattern as the loadModule methods: methods with a completion parameter are asynchronous, whereas methods without completion block the current thread until the Lua code execution returns.

For example, this code synchronously executes a Lua source string and prints the result:

let luaSimpleAdd = """
    local a, b = 2, 3
    return a + b
"""

let sum = luaContext.execute(sourceString: luaSimpleAdd) as! Float
print(sum) // --> 5.0

Asynchronous execute methods include:

func execute(sourceString: String, completion: ((Any?) -> Void)?)
func execute(sourceFile: URL, completion: ((Any?) -> Void)?)

Accessing Lua Context variables

Getting and setting Lua globals

Lua global variables in a Lua Context can be read and written using the native API.

To get the value of a Lua global variable, you call the Lua Context method luaContext.valueOfLuaGlobal("variableName") or you use the equivalent subscript syntax luaContext["variableName"].

To set the value of a Lua global variable, you call the Lua Context method luaContext.setLuaGlobal("variableName", withValue: someValue) or you use the equivalent subscript syntax luaContext["variableName"] = someValue.

For example:

let luaContext = LuaContext.default

// Setting Lua globals
luaContext.setLuaGlobal("a", withValue: "hello")
luaContext["b"] = "world"
luaContext["c"] = 1
luaContext["d"] = false

// Getting Lua global's values
let a = luaContext["a"] as! String
let c = luaContext.valueOfLuaGlobal("c") as! Int
print(a, c)

As a convenience, You can set several Lua global variables with a single cal to method setLuaGlobalValues(_:). This example is equivalent to the above "Setting Lua globals" section:

// Setting Lua globals
luaContext.setLuaGlobalValues(["a": "hello", "b": "world", "c": 1, "d": false])

When you set a Lua global variable, it is automatically converted into the closest matching Lua type, as described in the native bridge documentation.

When you get the value of a Lua global variable, the same conversion rules are applied and the value is typed as Any?. Therefore before using a Lua global variable value, you generally have to cast it to the expected Swift type (e.g. luaContext["a"] as! String).

Manipulating Lua-specific types

Some Lua values —like Lua tables and functions— do not correspond to any standard Swift type. Such values are read as objects of class LuaTable (for Lua tables), LuaFunction (for Lua functions) or LuaValue (for other specific Lua types).

The LuaTable class provides methods to get and set the entries of the corresponding table in its original Lua Context (i.e. a LuaTable instance is a reference to the actual Lua table in the Lua Context).

For example, here is how you can create and use a Lua table from your app's native code:

// Use Lua code execution to create a Lua table stored in global variable `aTable`
luaContext.execute(sourceString: "aTable = { x = 10, y = 100 }")

// Get, update and print the content of this Lua table
let aTable = luaContext.valueOfLuaGlobal("aTable") as! LuaTable
let x = aTable["x"] as! Int
aTable["x"] = x + 2
print("aTable= ", luaContext.valueOfLuaGlobal("aTable") ?? "not set or nil")
// --> aTable=  { x = 12; y = 100; }

Values of types LuaFunction and LuaValue are opaque objects, whose main utility is to be passed back to their original Lua Context at a later time.

Calling Lua methods from your app

As explained on the Lua code side in this Lua native bridge document section, a Lua method needs to have a known interface in order to be callable by the application native code, and the preferred way of defining known method interfaces is to define a protocol declaring these method interfaces.

To illustrate this technique, we will reuse this Lua sample code getting a Wikipedia article text. Instead of loading a hard-coded Wikipedia article, we can define a Lua class with a extract-getter method that takes the article name as a parameter.

Let's define a protocol declaring the interface of this method:

@objc protocol ExtractGetter {
    func getExtract(articleTitle: String, completion:@escaping (String?) -> Void)
}

The getExtract method takes the article title as parameter and calls a completion closure when the article extract is obtained. The ExtractGetter protocol has to be defined with the @objc attribute in order to be visible by Bounces Lua native bridge.

Because the Swift compiler slightly renames the method for exposing it to the objc runtime, with a name such as getExtractWithArticleTitle:completion:, we have to name the getExtract Lua method getExtractWithArticleTitle_completion. We also have to declare that the Lua class implements the protocol by calling the declareProtocol method.

This results in the following Lua module (stored in file LuaWikiGetter.lua):

local JsonUrlLoader = require "LuaLib.JsonUrlLoader" -- Load the JsonLoader class

local WikipediaGetter = class.create("WikipediaGetter")

WikipediaGetter:declareProtocol "ExtractGetter"

function WikipediaGetter:init ()
    self.jsonLoader = JsonUrlLoader:new()
end

function WikipediaGetter:getExtractWithArticleTitle_completion(articleTitle, completion)

    local articleUrl = objc.NSURL:URLWithString ("https://en.wikipedia.org/w/api.php?format=json&action=query&prop=extracts&explaintext=&titles=" .. articleTitle)
    if articleUrl ~= nil then
        self.jsonLoader:getJsonAtURL_WithCompletionHandler (articleUrl, 
                                                            function(jsonObject, error)
                                                                if jsonObject then
                                                                    local firstPageInReply = jsonObject.query.pages.allValues.firstObject
                                                                    if firstPageInReply then
                                                                        completion(firstPageInReply.extract)
                                                                    end
                                                                elseif error then
                                                                    completion ("Error:" .. error.localizedDescription)
                                                                end
                                                            end)
    end

end

-- create a global wikipedia getter
wikiGetter = WikipediaGetter:new()

Now, the getExtract method can be called from native code on any object of the Lua class WikipediaGetter, created by loading the LuaWikiGetter module:

luaContext.loadModule("LuaWikiGetter") { (_) in
     // Get the global variable wikiGetter and cast it to the expected protocol
     let wikiGetter = luaContext["wikiGetter"] as! ExtractGetter

     wikiGetter.getExtract(articleTitle: "Hedgehog") { (extract) in
         print (extract ?? "Hedgehog article not found")
     }
}

Note that, in this example, the WikipediaGetter Lua class is not known by the Swift compiler, meaning that we cannot create a WikipediaGetter instance from the native code. This is the reason why a shared global wikiGetter instance is created in the Lua module and used in the native code.

Using module loaded notifications

When a Lua module gets loaded or reloaded, a Lua Context posts a notification named LuaContext.didLoadModule. Your app can observe this notification to detect changes in the Lua context's code and modules reload events.

This notification is sent when any module has completed loading, and it includes the module's name and module function results in the notification's userInfo dictionary.

For example, you can print information about all loaded Lua modules in your app by writting:

NotificationCenter.default.addObserver(forName: LuaContext.didLoadModule, object: luaContext, queue: OperationQueue.main) { (notification) in
    let userInfo = notification.userInfo!
    let loadedModuleName = userInfo[LuaContext.didLoadModuleKeyModuleName] as! String 
    let loadedModuleResults = userInfo[LuaContext.didLoadModuleKeyResults] as! [Any]

    print("module loaded : ", loadedModuleName)
    print("   with results: ", loadedModuleResults)
}

Post a Comment