Difference between revisions of "Making a Plugin"
m (Text replacement - "</source>" to "</syntaxhighlight>") |
|||
Line 649: | Line 649: | ||
'''PREV.''': [[Exporting to a Player]]<br/> | '''PREV.''': [[Exporting to a Player]]<br/> | ||
− | ''' | + | '''NEXT''': [[Lua to Luau conversion guide]]<br/> |
Revision as of 04:13, 4 November 2023
The Ultimate Guide to Gideros Studio
Lua Based Plugins
If your plugin is 100% Lua code then you can easily make it into a plugin.
Just create a folder based on the name of the plugin and add a file (also based on the plugin name) with the extension .gplugin .
The contents of the file should look like this:
<plugin name="My plugin name" description="What my plugin does!"> <target name="APK,iOS,Html5,WinRT,Windows,MacOSX,Win32"></target> </plugin>
Also add a sub-folder called 'luaplugin' to your folder, put your Lua files in there. These will be automatically added to a project that adds your plugin. You can run them by using the Lua 'require' command.
You can test them as userplugins (click add plugin, then click user plugins to find where to add them) then possibly submit them using github to include in the distribution. If you do submit them for inclusion then please try make the name of the plugin unique to avoid confusion with other similar plugins - eg don't call it UI, but rather give it a name like razorUI ...
Native Plugins
Using plugins written in the native language of the iOS or Android device, you can extend Gideros to use all the native features of the device. There is an example iOS plugin included with Gideros Studio for Game Kit. This means that your Gideros application can fully use Apple’s GameCenter.
Referring back to the architecture described in chapter 1.5, this chapter will deal with the top three layers.
Gideros Studio <--------> Lua - C++ bridge <--------> 3rd party plugins
The Lua - C++ bridge API
Lua provides a full C API interface, which allows communication between Gideros Studio and your plugin. Gideros Studio and your plugin exchange data through a stack.
Data is pushed onto the stack, and popped off the stack. In this example, an integer has just been pushed onto the stack, and is at the top of the stack.
4 | integer | -1 |
3 | table | -2 |
2 | function | -3 |
1 | “string” | -4 |
Notice the numbering - on the left is the position in the stack, 1 being the deepest. On the right is the relative numbering from the top, -1 being the top, and -4 here being the deepest.
A pseudo-code example
This all takes place in the C API plugin code.
Push “fred” onto an empty stack | 1 | “fred” | -1 |
Push a function onto the stack | 2 | function | -1 |
1 | “fred” | -2 |
Push a new table onto the stack | 3 | table (empty) | -1 |
2 | function | -2 | |
1 | “fred” | -3 |
Push an integer onto the stack | 4 | integer | -1 |
3 | table (empty) | -2 | |
2 | function | -3 | |
1 | “fred” | -4 |
Move the integer into the table | 3 | table (contains integer) | -1 |
2 | function | -2 | |
1 | “fred” | -3 |
Pop 1 off the stack | 2 | function | -1 |
1 | “fred” | -2 |
Pop 2 off the stack | <stack empty> |
A first plugin for the iPhone
Gideros Studio will send the plugin two integers, which a C function will add together and return the result to Gideros Studio.
- In Xcode, open the GiderosiPhonePlayer project and create an empty file called myFirstPlugin.mm.
- Copy in the plugin code below.
- Connect the iPhone, and build and run the GiderosiPhonePlayer app on the phone. The FPS is printed on the debug console every second.
- Create a new project in Gideros Studio
- In Gideros Studio, under Player Menu > Player Settings, untick Localhost, and enter the IP address on your iPhone (wireless connection is needed here for Gideros Studio). The IP address should look something like 192.168.x.xxx.
- Copy in the Gideros Studio code below.
- Run the program in Gideros Studio.
You should have both the debug console open in Xcode, and the Output console in Gideros Studio. Messages from the plugin will be printed in the Xcode console, and messages from Lua will be printed in the Gideros Studio console.
Plugin code
Create an empty file called myFirstPlugin.mm in the GiderosiPhonePlayer Xcode project, and copy the following code into it. This is the minimum code that is necessary for a C plugin.
#include "gideros.h"
#include "lua.h"
#include "lauxlib.h"
static int addTwoIntegers(lua_State *L)
{
//retrieve the two integers from the stack
int firstInteger = lua_tointeger(L, -1);
int secondInteger = lua_tointeger(L, -2);
int result = firstInteger + secondInteger;
//place the result on the stack
lua_pushinteger(L, result);
//one item off the top of the stack, the result, is returned
return 1;
}
static int loader(lua_State *L)
{
//This is a list of functions that can be called from Lua
const luaL_Reg functionlist[] = {
{"addTwoIntegers", addTwoIntegers}, {NULL, NULL},
};
luaL_register(L, "myFirstPlugin", functionlist);
//return the pointer to the plugin
return 1;
}
static void g_initializePlugin(lua_State* L)
{
lua_getglobal(L, "package");
lua_getfield(L, -1, "preload");
lua_pushcfunction(L, loader);
lua_setfield(L, -2, "myFirst");
lua_pop(L, 2);
}
static void g_deinitializePlugin(lua_State *L)
{ }
REGISTER_PLUGIN("myFirstPlugin", "1.0")
Gideros Studio code
Create a new Gideros Studio project, and then a main.lua file containing the following code. This is the minimum code that will interface with the C plugin.
require "myFirst"
--Send two integers to be added together and return an integer result
local result = myFirstPlugin.addTwoIntegers(14, 48)
print("\n\nSample 1 - Result of addTwoIntegers:", result, "\n\n")
Explanation of the code
require "myFirst"
This Lua code calls the g_initializePlugin() function, which tells the Gideros API to initialize and call the loader() function.
The loader() function tells the Gideros API which plugin functions are available to be called from Lua. When you create a new function to be called from Lua, add that function to the functionList[] in loader(). If you get the message "attempt to call method xxx (a nil value)", then you have probably forgotten to add the function to the list.
local result = myFirstPlugin.addTwoIntegers(14, 48) This Lua code will call the plugin function addTwoIntegers().
Plugin functions are always declared:
static int function_name(lua_State *L) { return noOfArguments;}
noOfArguments is the number of arguments held on the stack to return to Gideros Studio.
Declare the plugin function:
static int addTwoIntegers(lua_State *L) { lua_State *L holds all the information of the Lua interpreter, and must be passed as the first argument. int firstInteger = lua_tointeger(L, -1); int secondInteger = lua_tointeger(L, -2); int result = firstInteger + secondInteger;
The two integers sent from Gideros Studio - 14 and 48 - are on the top of the stack. 48 is the top position (-1) and 14 is the second position (-2).
lua_tointeger(lua_State *L, int index) is one of the Lua C API functions that retrieves data from the Lua stack.
//place the result on the stack
lua_pushinteger(L, result);
lua_pushinteger(lua_State *L, int i) places the result back onto the stack in the top position.
//one item off the top of the stack, the result, is returned
return 1;
The number of arguments to be retrieved off the top of the stack are returned to Gideros Studio. As the result is the topmost position on the stack, this is the one that Gideros Studio will retrieve.
Exchanging Data
The most common Lua C API functions will be described below. For a complete listing of all the functions in the Lua C API, visit the Lua manual at: http://www.lua.org/manual/5.1/manual.html#3.7
Send data to the plugin
In the addTwoIntegers example above, two integers are sent to the plugin, and the plugin extracts them off the stack using:
lua_tointeger(L, -1);
Other types of data can be extracted using the other lua_totypes:
lua_tonumber(L, -1); lua_toboolean(L, -1); lua_tostring(L, -1);
Return data to Gideros Studio
When the plugin function is complete, the function will return to Gideros Studio with a number of arguments:
return 2;
This will return two values off the top of the stack. To get data onto the stack, use one of the lua_pushtype functions:
lua_pushstring(L, “this is a string”);
lua_pushinteger(L, result);
lua_pushboolean(L, isCorrect);
lua_pushnumber(L, afloat);
Tables
To be used in the C plugin, Lua tables have to be converted to a C type, such as an array or an NSArray or an NSDictionary. To traverse a table on the top of the stack:
//push nil onto the stack so that the while loop will work
lua_pushnil(L);
//==2 nil -1==
//==1 tableWithKeys -2==
while (lua_next(L, 1) != 0) {
//lua_next pops the key off the stack
//then pushes the next key and value onto the stack
//==3 value - table value -1==
//==2 key - key -2==
//==1 tableWithKeys -3==
//use a function like lua_tonumber to retrieve the value
//off the top of the stack
// remove value - key stays for lua_next
lua_pop(L, 1);
//==2 key - key -1==
//==1 tableWithKeys -2==
}
It is a good idea to keep track of the stack in comments, as in the example above, so that you can easily see what is happening in your code.
Example - Sort a Gideros Studio table array
This example sends an array of strings from Gideros Studio to the plugin. The plugin reads the table, and places the array into an Objective C NSMutableArray. It is then sorted and returned to Gideros Studio.
Plugin Code
Remember to add the name of the function to the function list in loader() as described above.
static int sortTable(lua_State *L)
{
//==1 tableToBeSorted -1==
//push nil onto the stack so that the while loop will work
lua_pushnil(L);
//==2 nil -1==
//==1 tableToBeSorted -2==
NSMutableArray *array = [NSMutableArray array];
//move the table into an NSMutableArray
while (lua_next(L, 1) != 0) {
//==3 value - sort this string -1==
//==2 key - in an array, this is the index number -2==
//==1 tableToBeSorted -3==
NSString *value = [NSString stringWithUTF8String:luaL_checkstring(L, -1)];
[array addObject:value];
// remove value - key stays for lua_next
lua_pop(L, 1);
}
//pop the table off the stack, as we don't need it any more
lua_pop(L, 1);
//sort the array
[array sortUsingSelector:@selector(compare:)];
//Create a new table
lua_newtable(L);
//==1 newtable -1==
//Lua tables start from 1, whereas C arrays start from 0
int index = 1;
for (NSString *string in array) {
//push the table index
lua_pushnumber(L, index++);
//push the string on the stack, converting it to a const char*
lua_pushstring(L, [string UTF8String]);
//==3 "animal string" -1==
//==2 index number -2==
//==1 newtable -3==
//store index and string value into the table
lua_rawset(L, -3);
//==1 newtable -1==
}
//return the sorted table on the stack to Gideros Studio
return 1;
}
Gideros Studio code:
require “pluginName”
local sortedResult = pluginName.sortTable({"giraffe", "antelope", "aardvark", "zebra", "badger"})
for i = 1, #sortedResult do
print("\t\t", sortedResult[i])
end
Communication from the plugin to Gideros Studio
Communication from the plugin to Gideros Studio can be achieved by direct calling of a Gideros Studio function in the plugin, and also by dispatching events, which a Gideros Studio listener will pick up.
Call a Gideros Studio function from the plugin
This example will define two functions in Gideros Studio - printName() and eventHappened(). The C plugin will call these two functions.
Gideros Studio Code:
local function printName(name)
print("\n\t\tFunction called from plugin")
print("\t\t", "Name:", name)
end
local function eventHappened(event, x, y)
print("\n\t\tFunction called from plugin")
print("\t\t", "Event:", event, "X:", x, "Y:", y)
end
print("\nSample 7 - Call functions from plugin:")
pluginName.doCallback(printName, "Caroline", 1)
pluginName.doCallback(eventHappened, "Touched", 52, 243, 3)
Plugin code:
The function doCallback() accepts the function name to be called, then the arguments for that function, and then the number of arguments sent.
static int doCallback(lua_State *L)
{
//==4 no of arguments -1==
//==3 argument -2==
//==2 argument -3==
//==1 function -4==
int noOfArguments = lua_tointeger(L, -1);
//remove no of arguments off the stack
lua_pop(L, 1);
//call the function.
//noOfArguments tells the Lua C API how many arguments are
//on the stack.
//the function is on the stack below all the arguments
lua_call(L, noOfArguments, 0);
return 0;
}
Retain data in the Lua Registry
To be able to retain information, the Lua C API provides a registry. This behaves like the stack, but has a special index called LUA_REGISTRYINDEX. You can create tables and push them into the registry by
lua_rawset(L, LUA_REGISTRYINDEX)
Because the registry is common over all available plugins, the usual way of creating a unique key for a registry table is to use the address of a variable.
This example saves data into a KEY_OBJECTS table that has been previously created in the registry.
static const char KEY_OBJECTS = ' ';
lua_pushlightuserdata(L, (void *)&KEY_OBJECTS);
//== 1 address of KEY_OBJECTS -1==
lua_rawget(L, LUA_REGISTRYINDEX);
//== 1 table of KEY_OBJECTS -1==
lua_pushstring(L, “value”);
//== 2 “value” -1==
//== 1 table of KEY_OBJECTS -2==
//”value” goes into KEY_OBJECTS at index 1
lua_rawseti(L, -2, 1);
//== 1 table of KEY_OBJECTS -1==
Retrieving this data - "value" will be placed at the top of the stack for further use:
lua_pushlightuserdata(L, (void *)&KEY_OBJECTS);
//== 1 address of KEY_OBJECTS -1==
lua_rawget(L, LUA_REGISTRYINDEX);
//== 1 table of KEY_OBJECTS -1==
//retrieve index 1 in the KEY_OBJECTS table
lua_rawgeti(L, -2, 1);
//== 2 “value” -1==
//== 1 table of KEY_OBJECTS -2==
Dispatch events to Gideros Studio
Events and listeners are one of the core features of Gideros Studio. To set up the C API to dispatch events, you need to create a sub-class of GEventDispatcherProxy. This class will be retained in the Lua registry as described above.
The following example will build a ProgressBar class, which will accept an integer from Gideros Studio, add it to a total held in the ProgressBar class, and when the total reaches 100, will dispatch an event to Gideros Studio.
This loader function will:
- Create an Event Dispatcher class
- Define an event and save it in the Lua global Event table
- Create a table in the Lua registry to retain variables
- Instantiate the Event Dispatcher class
- Retain the Event Dispatcher class instance in the registry table
static int loader(lua_State *L)
{
//list the functions available to Gideros Studio
const luaL_Reg functionlist[] = {
{"addToProgress", addToProgress},
{NULL, NULL},
};
// 1. Create an Event Dispatcher class, defining optional
//create and destruct methods
g_createClass(L, "ProgressBar", "EventDispatcher", NULL, destruct, functionlist);
// 2. Define an event and save it in the Lua global Event table
lua_getglobal(L, "Event");
lua_pushstring(L, "progressComplete");
lua_setfield(L, -2, "progressComplete");
lua_pop(L, 1);
// 3. create a weak table in LUA_REGISTRYINDEX that can be accessed with the address of KEY_OBJECTS
lua_pushlightuserdata(L, (void *)&KEY_OBJECTS);
lua_newtable(L); // create a table
lua_pushliteral(L, "v");
lua_setfield(L, -2, "__mode"); // set as weak-value table
lua_pushvalue(L, -1); // duplicate table
lua_setmetatable(L, -2); // set itself as metatable
lua_rawset(L, LUA_REGISTRYINDEX);
// 4. Instantiate the Event Dispatcher class
ProgressBar *bar = new ProgressBar(L);
g_pushInstance(L, "ProgressBar", bar->object());
// 5. Retain the Event Dispatcher class instance in the registry table
lua_pushlightuserdata(L, (void *)&KEY_OBJECTS);
lua_rawget(L, LUA_REGISTRYINDEX);
lua_pushvalue(L, -2);
lua_rawseti(L, -2, 1);
lua_pop(L, 1);
lua_pushvalue(L, -1);
lua_setglobal(L, "progress");
//return the pointer to the plugin
return 1;
}
The addToProgress function, listed in the plugin’s function list, simply receives an integer from Gideros Studio and adds it to the ProgressBar’s totalProgress variable:
static int addToProgress(lua_State *L)
{
//how to access an internal instance - see below
GReferenced* object = static_cast<GReferenced*>(g_getInstance(L, "ProgressBar", 1));
ProgressBar* bar = static_cast<ProgressBar*>(object->proxy());
int progress = lua_tointeger(L, -1);
bar->totalProgress += progress;
//remove the received integer off the stack
lua_pop(L, 1);
//check to see if totalProgress is greater than 100
//if it is, dispatch an event to Gideros Studio.
bar->checkProgress(L);
return 0;
}
The loader function creates a ProgressBar class, which inherits from the Gideros class GEventDispatcherProxy. Proxies are the abstract classes for implementing new types of Gideros objects. Each proxy class owns an instance of an internal class. For example, when you create a GEventDispatcherProxy instance:
GEventDispatcherProxy* eventDispatcherProxy = new GEventDispatcherProxy();
you can access the internal instance by using the object() function:
GReferenced* eventDispatcher = eventDispatcherProxy->object();
Or, if you have a internal object pointer, you can access the proxy object like:
GReferenced proxy = eventDispatcher->proxy();
Functions like g_pushInstance(), g_getInstance() or destruct() work with internal object pointers. To place an object on the stack:
g_pushInstance(L, "GameKit", gamekit->object());
Or to retrieve:
GReferenced* object = static_cast(g_getInstance(L, "GameKit", index)); GameKit* gamekit = static_cast(object->proxy());
Similarly, in the destruct function, you first get the internal object pointer and then get the proxy pointer from it:
void* ptr = *(void**)lua_touserdata(L, 1); GReferenced* object = static_cast(ptr); GameKit* gamekit = static_cast(object->proxy());
The ProgressBar class holds a totalProgress variable, and has two methods:
checkProgress, to see if totalProgress is greater than 100
dispatchEvent, which dispatches the event to Gideros Studio:
class ProgressBar : public GEventDispatcherProxy
{
public:
int totalProgress;
ProgressBar(lua_State* L) : L(L)
{
totalProgress = 0;
}
ProgressBar()
{ }
void dispatchEvent(const char* type,
NSError* error,
NSString *returnMessage)
{
//== Stack contents
//==1 plugin details -1==
lua_pushlightuserdata(L, (void *)&KEY_OBJECTS);
lua_rawget(L, LUA_REGISTRYINDEX);
lua_rawgeti(L, -1, 1);
//==3 Registry table -1==
//==2 Key Objects table -2==
//==1 plugin details -3==
if (!lua_isnil(L, -1))
{
//get the function name from the Registry
//for dispatchEvent
lua_getfield(L, -1, "dispatchEvent");
//==4 function for dispatchEvent -1==
//==3 Registry table -2==
//==2 Key Objects table -3==
//==1 plugin details -4==
//copy the Registry table to the top of the stack
lua_pushvalue(L, -2);
//==5 Registry table -1==
//==4 function for dispatchEvent -2==
//==etc
//get the event strings for "Event"
//these were set up in loader()
//"Event" currently only contains “progressComplete”
lua_getglobal(L, "Event");
//==6 Event table -1==
//==5 Registry table -2==
//==4 function for dispatchEvent -3==
//==etc
//get the Event function associated with the string "new"
lua_getfield(L, -1, "new");
//==7 function -1==
//etc
//remove the Event table
lua_remove(L, -2);
//==6 function -1==
//==5 Registry table -2==
//==4 function for dispatchEvent -3==
//==3 etc
//push the event type ("progressComplete" in this example)
lua_pushstring(L, type);
//==7 "progressComplete" -1==
//==6 function -2==
//==5 Registry table -3==
//==4 etc
//call the "new" event function
//with "progressComplete" as the argument
//One return value is placed on the stack
lua_call(L, 1, 1);
//==6 table returned from "new" -1==
//==5 Registry table -2==
//==4 function for dispatchEvent -3==
//==etc
if (error)
{
lua_pushinteger(L, error.code);
lua_setfield(L, -2, "errorCode");
lua_pushstring(L, [error.localizedDescription UTF8String]);
lua_setfield(L, -2, "errorDescription");
}
if (returnMessage)
{
//any arguments are pushed into the table
//returned from "new"
//this table will be sent to the Lua Event function
lua_pushstring(L, [returnMessage UTF8String]);
lua_setfield(L, -2, "stringReturned");
}
//two tables are sent to the
//dispatch event function
if (lua_pcall(L, 2, 0, 0) != 0)
{
g_error(L, lua_tostring(L, -1));
return;
}
}
lua_pop(L, 2);
}
void checkProgress(lua_State *L) {
if (totalProgress >= 100) {
dispatchEvent("progressComplete", NULL, @"Progress Complete");
}
}
private:
lua_State* L;
};
When the ProgressBar was first declared to Gideros:
g_createClass(L, "ProgressBar", "EventDispatcher", NULL, destruct, functionlist);
destruct() was the function listed to be called to clean up the memory. (NULL in that example could have been a function to be called when creating the class):
static int destruct(lua_State* L)
{
//get the internal object pointer
void *ptr = *(void**)lua_touserdata(L, 1);
//get the proxy pointer
GReferenced *object = static_cast<GReferenced*>(ptr);
ProgressBar *bar = static_cast<ProgressBar*>(object->proxy());
bar->unref();
return 0;
}
The Gideros Studio code to use this plugin is simple:
require "progress"
--this function will be called when the
--"progressComplete" event is dispatched
function onProgressComplete(event)
print("Progress Complete")
end
progress:addEventListener("progressComplete", onProgressComplete)
--totalProgress will be 10
progress:addToProgress(10)
--totalProgress will be 30
--this is an equivalent way of calling the plugin function
ProgressBar.addToProgress(progress, 20)
--totalProgress will be greater than 100, and an event will be
--dispatched, calling onProgressComplete
progress:addToProgress(94)
This flowchart summarizes the flow of control between Gideros Studio, the C plugin and the ProgressBar C++ class for data exchange and event dispatching:
PREV.: Exporting to a Player
NEXT: Lua to Luau conversion guide