using System;
using System.Linq;
using System.Reflection;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using KeraLua;
using NLua.Event;
using NLua.Method;
using NLua.Exceptions;
using NLua.Extensions;
#if __IOS__ || __TVOS__ || __WATCHOS__
using ObjCRuntime;
#endif
using LuaState = KeraLua.Lua;
using LuaNativeFunction = KeraLua.LuaFunction;
namespace NLua
{
public class Lua : IDisposable
{
#region lua debug functions
///
/// Event that is raised when an exception occures during a hook call.
///
public event EventHandler HookException;
///
/// Event when lua hook callback is called
///
///
/// Is only raised if SetDebugHook is called before.
///
public event EventHandler DebugHook;
///
/// lua hook calback delegate
///
private LuaHookFunction hookCallback = null;
#endregion
#region Globals auto-complete
private readonly List globals = new List();
private bool globalsSorted;
#endregion
private LuaState luaState;
///
/// True while a script is being executed
///
public bool IsExecuting { get { return executing; } }
public LuaState State => luaState;
private LuaNativeFunction panicCallback;
private ObjectTranslator translator;
///
/// Used to protect the (global) object translator pool during add/remove
///
private static readonly object translatorPoolLock = new object();
///
/// Used to ensure multiple .net threads all get serialized by this single lock for access to the lua stack/objects
///
//private object luaLock = new object();
private bool _StatePassed;
private bool executing;
static string initLuanet =
@"local metatable = {}
local rawget = rawget
local import_type = luanet.import_type
local load_assembly = luanet.load_assembly
luanet.error, luanet.type = error, type
-- Lookup a .NET identifier component.
function metatable:__index(key) -- key is e.g. 'Form'
-- Get the fully-qualified name, e.g. 'System.Windows.Forms.Form'
local fqn = rawget(self,'.fqn')
fqn = ((fqn and fqn .. '.') or '') .. key
-- Try to find either a luanet function or a CLR type
local obj = rawget(luanet,key) or import_type(fqn)
-- If key is neither a luanet function or a CLR type, then it is simply
-- an identifier component.
if obj == nil then
-- It might be an assembly, so we load it too.
pcall(load_assembly,fqn)
obj = { ['.fqn'] = fqn }
setmetatable(obj, metatable)
end
-- Cache this lookup
rawset(self, key, obj)
return obj
end
-- A non-type has been called; e.g. foo = System.Foo()
function metatable:__call(...)
error('No such type: ' .. rawget(self,'.fqn'), 2)
end
-- This is the root of the .NET namespace
luanet['.fqn'] = false
setmetatable(luanet, metatable)
-- Preload the mscorlib assembly
luanet.load_assembly('mscorlib')";
static string clr_package = @"---
--- This lua module provides auto importing of .net classes into a named package.
--- Makes for super easy use of LuaInterface glue
---
--- example:
--- Threading = CLRPackage(""System"", ""System.Threading"")
--- Threading.Thread.Sleep(100)
---
--- Extensions:
--- import() is a version of CLRPackage() which puts the package into a list which is used by a global __index lookup,
--- and thus works rather like C#'s using statement. It also recognizes the case where one is importing a local
--- assembly, which must end with an explicit .dll extension.
--- Alternatively, luanet.namespace can be used for convenience without polluting the global namespace:
--- local sys,sysi = luanet.namespace {'System','System.IO'}
-- sys.Console.WriteLine(""we are at {0}"",sysi.Directory.GetCurrentDirectory())
-- LuaInterface hosted with stock Lua interpreter will need to explicitly require this...
if not luanet then require 'luanet' end
local import_type, load_assembly = luanet.import_type, luanet.load_assembly
local mt = {
--- Lookup a previously unfound class and add it to our table
__index = function(package, classname)
local class = rawget(package, classname)
if class == nil then
class = import_type(package.packageName .. ""."" .. classname)
if class == nil then class = import_type(classname) end
package[classname] = class -- keep what we found around, so it will be shared
end
return class
end
}
function luanet.namespace(ns)
if type(ns) == 'table' then
local res = {}
for i = 1,#ns do
res[i] = luanet.namespace(ns[i])
end
return unpack(res)
end
-- FIXME - table.packageName could instead be a private index (see Lua 13.4.4)
local t = { packageName = ns }
setmetatable(t,mt)
return t
end
local globalMT, packages
local function set_global_mt()
packages = {}
globalMT = {
__index = function(T,classname)
for i,package in ipairs(packages) do
local class = package[classname]
if class then
_G[classname] = class
return class
end
end
end
}
setmetatable(_G, globalMT)
end
--- Create a new Package class
function CLRPackage(assemblyName, packageName)
-- a sensible default...
packageName = packageName or assemblyName
local ok = pcall(load_assembly,assemblyName) -- Make sure our assembly is loaded
return luanet.namespace(packageName)
end
function import (assemblyName, packageName)
if not globalMT then
set_global_mt()
end
if not packageName then
local i = assemblyName:find('%.dll$')
if i then packageName = assemblyName:sub(1,i-1)
else packageName = assemblyName end
end
local t = CLRPackage(assemblyName,packageName)
table.insert(packages,t)
return t
end
function luanet.make_array (tp,tbl)
local arr = tp[#tbl]
for i,v in ipairs(tbl) do
arr:SetValue(v,i-1)
end
return arr
end
function luanet.each(o)
local e = o:GetEnumerator()
return function()
if e:MoveNext() then
return e.Current
end
end
end
";
public bool UseTraceback { get; set; } = false;
#region Globals auto-complete
///
/// An alphabetically sorted list of all globals (objects, methods, etc.) externally added to this Lua instance
///
/// Members of globals are also listed. The formatting is optimized for text input auto-completion.
public IEnumerable Globals {
get
{
// Only sort list when necessary
if (!globalsSorted)
{
globals.Sort();
globalsSorted = true;
}
return globals;
}
}
#endregion
public Lua()
{
luaState = new LuaState();
Init();
// We need to keep this in a managed reference so the delegate doesn't get garbage collected
panicCallback = PanicCallback;
luaState.AtPanic(panicCallback);
}
/*
* CAUTION: NLua.Lua instances can't share the same lua state!
*/
public Lua(LuaState luaState)
{
luaState.PushString("NLua_Loaded");
luaState.GetTable((int)LuaRegistry.Index);
if (luaState.ToBoolean(-1))
{
luaState.SetTop(-2);
throw new LuaException("There is already a NLua.Lua instance associated with this Lua state");
}
else
{
this.luaState = luaState;
_StatePassed = true;
luaState.SetTop(-2);
Init();
}
}
void Init()
{
luaState.PushString("NLua_Loaded");
luaState.PushBoolean(true);
luaState.SetTable((int)LuaRegistry.Index);
if (_StatePassed == false)
{
luaState.NewTable();
luaState.SetGlobal("luanet");
}
luaState.PushGlobalTable();
luaState.GetGlobal("luanet");
luaState.PushString("getmetatable");
luaState.GetGlobal("getmetatable");
luaState.SetTable(-3);
luaState.PopGlobalTable();
translator = new ObjectTranslator(this, luaState);
lock (translatorPoolLock)
{
ObjectTranslatorPool.Instance.Add(luaState, translator);
}
luaState.PopGlobalTable();
luaState.DoString(initLuanet);
}
public void Close()
{
if (_StatePassed)
return;
if (luaState != null)
{
lock (translatorPoolLock)
{
luaState.Close();
ObjectTranslatorPool.Instance.Remove(luaState);
luaState = null;
}
}
}
#if __IOS__ || __TVOS__ || __WATCHOS__
[MonoPInvokeCallback(typeof(LuaNativeFunction))]
#endif
static int PanicCallback(IntPtr state)
{
var luaState = LuaState.FromIntPtr(state);
string reason = string.Format("Unprotected error in call to Lua API ({0})", luaState.ToString(-1));
throw new LuaException(reason);
}
///
/// Assuming we have a Lua error string sitting on the stack, throw a C# exception out to the user's app
///
/// Thrown if the script caused an exception
private void ThrowExceptionFromError(int oldTop)
{
object err = translator.GetObject(luaState, -1);
luaState.SetTop(oldTop);
// A pre-wrapped exception - just rethrow it (stack trace of InnerException will be preserved)
var luaEx = err as LuaScriptException;
if (luaEx != null)
throw luaEx;
// A non-wrapped Lua error (best interpreted as a string) - wrap it and throw it
if (err == null)
err = "Unknown Lua Error";
throw new LuaScriptException(err.ToString(), string.Empty);
}
///
/// Push a debug.traceback reference onto the stack, for a pcall function to use as error handler. (Remember to increment any top-of-stack markers!)
///
private static int PushDebugTraceback(LuaState luaState, int argCount)
{
luaState.GetGlobal("debug");
luaState.GetField(-1, "traceback");
luaState.Remove(-2);
int errindex = -argCount - 2;
luaState.Insert(errindex);
return errindex;
}
///
/// Return a debug.traceback() call result (a multi-line string, containing a full stack trace, including C calls.
/// Note: it won't return anything unless the interpreter is in the middle of execution - that is, it only makes sense to call it from a method called from Lua, or during a coroutine yield.
///
public string GetDebugTraceback()
{
int oldTop = luaState.GetTop();
luaState.GetGlobal("debug"); // stack: debug
luaState.GetField(-1, "traceback"); // stack: debug,traceback
luaState.Remove(-2); // stack: traceback
luaState.PCall(0, -1, 0);
return translator.PopValues(luaState, oldTop)[0] as string;
}
///
/// Convert C# exceptions into Lua errors
///
/// num of things on stack
/// null for no pending exception
internal int SetPendingException(Exception e)
{
var caughtExcept = e;
if (caughtExcept != null)
{
translator.ThrowError(luaState, caughtExcept);
luaState.PushNil();
return 1;
}
else
return 0;
}
///
///
///
///
///
///
public LuaFunction LoadString(string chunk, string name)
{
int oldTop = luaState.GetTop();
executing = true;
try
{
if (luaState.LoadString(chunk, name) != LuaStatus.OK)
ThrowExceptionFromError(oldTop);
}
finally
{
executing = false;
}
var result = translator.GetFunction(luaState, -1);
translator.PopValues(luaState, oldTop);
return result;
}
///
///
///
///
///
///
public LuaFunction LoadString(byte[] chunk, string name)
{
int oldTop = luaState.GetTop();
executing = true;
try
{
if (luaState.LoadBuffer(chunk, name) != LuaStatus.OK)
ThrowExceptionFromError(oldTop);
}
finally
{
executing = false;
}
var result = translator.GetFunction(luaState, -1);
translator.PopValues(luaState, oldTop);
return result;
}
///
/// Load a File on, and return a LuaFunction to execute the file loaded (useful to see if the syntax of a file is ok)
///
///
///
public LuaFunction LoadFile(string fileName)
{
int oldTop = luaState.GetTop();
if (luaState.LoadFile(fileName) != LuaStatus.OK)
ThrowExceptionFromError(oldTop);
var result = translator.GetFunction(luaState, -1);
translator.PopValues(luaState, oldTop);
return result;
}
///
/// Executes a Lua chunk and returns all the chunk's return values in an array.
///
/// Chunk to execute
/// Name to associate with the chunk. Defaults to "chunk".
///
public object[] DoString(byte[] chunk, string chunkName = "chunk")
{
int oldTop = luaState.GetTop();
executing = true;
if (luaState.LoadBuffer(chunk, chunkName) == LuaStatus.OK)
{
int errfunction = 0;
if (UseTraceback)
{
errfunction = PushDebugTraceback(luaState, 0);
oldTop++;
}
try
{
if (luaState.PCall(0, -1, errfunction) == LuaStatus.OK)
return translator.PopValues(luaState, oldTop);
else
ThrowExceptionFromError(oldTop);
}
finally
{
executing = false;
}
}
else
ThrowExceptionFromError(oldTop);
return null;
}
///
/// Executes a Lua chunk and returns all the chunk's return values in an array.
///
/// Chunk to execute
/// Name to associate with the chunk. Defaults to "chunk".
///
public object[] DoString(string chunk, string chunkName = "chunk")
{
int oldTop = luaState.GetTop();
executing = true;
if (luaState.LoadString(chunk, chunkName) == LuaStatus.OK)
{
int errfunction = 0;
if (UseTraceback)
{
errfunction = PushDebugTraceback(luaState, 0);
oldTop++;
}
try
{
if (luaState.PCall(0, -1, errfunction) == LuaStatus.OK)
return translator.PopValues(luaState, oldTop);
else
ThrowExceptionFromError(oldTop);
}
finally
{
executing = false;
}
}
else
ThrowExceptionFromError(oldTop);
return null;
}
/*
* Excutes a Lua file and returns all the chunk's return
* values in an array
*/
public object[] DoFile(string fileName)
{
int oldTop = luaState.GetTop();
if (luaState.LoadFile(fileName) == LuaStatus.OK)
{
executing = true;
int errfunction = 0;
if (UseTraceback)
{
errfunction = PushDebugTraceback(luaState, 0);
oldTop++;
}
try
{
if (luaState.PCall(0, -1, errfunction) == LuaStatus.OK)
return translator.PopValues(luaState, oldTop);
else
ThrowExceptionFromError(oldTop);
}
finally
{
executing = false;
}
}
else
ThrowExceptionFromError(oldTop);
return null;
}
/*
* Indexer for global variables from the LuaInterpreter
* Supports navigation of tables by using . operator
*/
public object this[string fullPath] {
get
{
object returnValue = null;
int oldTop = luaState.GetTop();
string[] path = FullPathToArray(fullPath);
luaState.GetGlobal(path[0]);
returnValue = translator.GetObject(luaState, -1);
if (path.Length > 1)
{
var dispose = returnValue as LuaBase;
string[] remainingPath = new string[path.Length - 1];
Array.Copy(path, 1, remainingPath, 0, path.Length - 1);
returnValue = GetObject(remainingPath);
if (dispose != null)
dispose.Dispose();
}
luaState.SetTop(oldTop);
return returnValue;
}
set
{
int oldTop = luaState.GetTop();
string[] path = FullPathToArray(fullPath);
if (path.Length == 1)
{
translator.Push(luaState, value);
luaState.SetGlobal(fullPath);
}
else
{
luaState.GetGlobal(path[0]);
string[] remainingPath = new string[path.Length - 1];
Array.Copy(path, 1, remainingPath, 0, path.Length - 1);
SetObject(remainingPath, value);
}
luaState.SetTop(oldTop);
// Globals auto-complete
if (value == null)
{
// Remove now obsolete entries
globals.Remove(fullPath);
}
else
{
// Add new entries
if (!globals.Contains(fullPath))
RegisterGlobal(fullPath, value.GetType(), 0);
}
}
}
#region Globals auto-complete
///
/// Adds an entry to (recursivley handles 2 levels of members)
///
/// The index accessor path ot the entry
/// The type of the entry
/// How deep have we gone with recursion?
private void RegisterGlobal(string path, Type type, int recursionCounter)
{
// If the type is a global method, list it directly
if (type == typeof(LuaFunction))
{
// Format for easy method invocation
globals.Add(path + "(");
}
// If the type is a class or an interface and recursion hasn't been running too long, list the members
else if ((type.IsClass || type.IsInterface) && type != typeof(string) && recursionCounter < 2)
{
#region Methods
foreach (var method in type.GetMethods(BindingFlags.Public | BindingFlags.Instance))
{
string name = method.Name;
if (
// Check that the LuaHideAttribute and LuaGlobalAttribute were not applied
(!method.GetCustomAttributes(typeof(LuaHideAttribute), false).Any()) &&
(!method.GetCustomAttributes(typeof(LuaGlobalAttribute), false).Any()) &&
// Exclude some generic .NET methods that wouldn't be very usefull in Lua
name != "GetType" && name != "GetHashCode" && name != "Equals" &&
name != "ToString" && name != "Clone" && name != "Dispose" &&
name != "GetEnumerator" && name != "CopyTo" &&
!name.StartsWith("get_", StringComparison.Ordinal) &&
!name.StartsWith("set_", StringComparison.Ordinal) &&
!name.StartsWith("add_", StringComparison.Ordinal) &&
!name.StartsWith("remove_", StringComparison.Ordinal))
{
// Format for easy method invocation
string command = path + ":" + name + "(";
if (method.GetParameters().Length == 0)
command += ")";
globals.Add(command);
}
}
#endregion
#region Fields
foreach (var field in type.GetFields(BindingFlags.Public | BindingFlags.Instance))
{
if (
// Check that the LuaHideAttribute and LuaGlobalAttribute were not applied
(!field.GetCustomAttributes(typeof(LuaHideAttribute), false).Any()) &&
(!field.GetCustomAttributes(typeof(LuaGlobalAttribute), false).Any()))
{
// Go into recursion for members
RegisterGlobal(path + "." + field.Name, field.FieldType, recursionCounter + 1);
}
}
#endregion
#region Properties
foreach (var property in type.GetProperties(BindingFlags.Public | BindingFlags.Instance))
{
if (
// Check that the LuaHideAttribute and LuaGlobalAttribute were not applied
(!property.GetCustomAttributes(typeof(LuaHideAttribute), false).Any()) &&
(!property.GetCustomAttributes(typeof(LuaGlobalAttribute), false).Any())
// Exclude some generic .NET properties that wouldn't be very useful in Lua
&& property.Name != "Item")
{
// Go into recursion for members
RegisterGlobal(path + "." + property.Name, property.PropertyType, recursionCounter + 1);
}
}
#endregion
}
else
globals.Add(path); // Otherwise simply add the element to the list
// List will need to be sorted on next access
globalsSorted = false;
}
#endregion
/*
* Navigates a table in the top of the stack, returning
* the value of the specified field
*/
object GetObject(string[] remainingPath)
{
object returnValue = null;
for (int i = 0; i < remainingPath.Length; i++)
{
luaState.PushString(remainingPath[i]);
luaState.GetTable(-2);
returnValue = translator.GetObject(luaState, -1);
if (returnValue == null)
break;
}
return returnValue;
}
/*
* Gets a numeric global variable
*/
public double GetNumber(string fullPath)
{
return (double)this[fullPath];
}
/*
* Gets a string global variable
*/
public string GetString(string fullPath)
{
return this[fullPath].ToString();
}
/*
* Gets a table global variable
*/
public LuaTable GetTable(string fullPath)
{
return (LuaTable)this[fullPath];
}
/*
* Gets a table global variable as an object implementing
* the interfaceType interface
*/
public object GetTable(Type interfaceType, string fullPath)
{
return CodeGeneration.Instance.GetClassInstance(interfaceType, GetTable(fullPath));
}
/*
* Gets a function global variable
*/
public LuaFunction GetFunction(string fullPath)
{
object obj = this[fullPath];
LuaFunction luaFunction = obj as LuaFunction;
if (luaFunction != null)
return luaFunction;
luaFunction = new LuaFunction((LuaNativeFunction) obj, this);
return luaFunction;
}
/*
* Register a delegate type to be used to convert Lua functions to C# delegates (useful for iOS where there is no dynamic code generation)
* type delegateType
*/
public void RegisterLuaDelegateType(Type delegateType, Type luaDelegateType)
{
CodeGeneration.Instance.RegisterLuaDelegateType(delegateType, luaDelegateType);
}
public void RegisterLuaClassType(Type klass, Type luaClass)
{
CodeGeneration.Instance.RegisterLuaClassType(klass, luaClass);
}
public void LoadCLRPackage()
{
luaState.DoString(Lua.clr_package);
}
/*
* Gets a function global variable as a delegate of
* type delegateType
*/
public Delegate GetFunction(Type delegateType, string fullPath)
{
return CodeGeneration.Instance.GetDelegate(delegateType, GetFunction(fullPath));
}
/*
* Calls the object as a function with the provided arguments,
* returning the function's returned values inside an array
*/
internal object[] CallFunction(object function, object[] args)
{
return CallFunction(function, args, null);
}
/*
* Calls the object as a function with the provided arguments and
* casting returned values to the types in returnTypes before returning
* them in an array
*/
internal object[] CallFunction(object function, object[] args, Type[] returnTypes)
{
int nArgs = 0;
int oldTop = luaState.GetTop();
if (!luaState.CheckStack(args.Length + 6))
throw new LuaException("Lua stack overflow");
translator.Push(luaState, function);
if (args.Length > 0)
{
nArgs = args.Length;
for (int i = 0; i < args.Length; i++)
translator.Push(luaState, args[i]);
}
executing = true;
try
{
int errfunction = 0;
if (UseTraceback)
{
errfunction = PushDebugTraceback(luaState, nArgs);
oldTop++;
}
LuaStatus error = luaState.PCall(nArgs, -1, errfunction);
if (error != LuaStatus.OK)
ThrowExceptionFromError(oldTop);
}
finally
{
executing = false;
}
if (returnTypes != null)
return translator.PopValues(luaState, oldTop, returnTypes);
return translator.PopValues(luaState, oldTop);
}
/*
* Navigates a table to set the value of one of its fields
*/
void SetObject(string[] remainingPath, object val)
{
for (int i = 0; i < remainingPath.Length - 1; i++)
{
luaState.PushString(remainingPath[i]);
luaState.GetTable(-2);
}
luaState.PushString(remainingPath[remainingPath.Length - 1]);
translator.Push(luaState, val);
luaState.SetTable(-3);
}
string[] FullPathToArray(string fullPath)
{
return fullPath.SplitWithEscape('.', '\\').ToArray();
}
/*
* Creates a new table as a global variable or as a field
* inside an existing table
*/
public void NewTable(string fullPath)
{
string[] path = FullPathToArray(fullPath);
int oldTop = luaState.GetTop();
if (path.Length == 1)
{
luaState.NewTable();
luaState.SetGlobal(fullPath);
}
else
{
luaState.GetGlobal(path[0]);
for (int i = 1; i < path.Length - 1; i++)
{
luaState.PushString(path[i]);
luaState.GetTable(-2);
}
luaState.PushString(path[path.Length - 1]);
luaState.NewTable();
luaState.SetTable(-3);
}
luaState.SetTop( oldTop);
}
public Dictionary