Code archives/Miscellaneous/Type Access in Lua via Reflection

This code has been declared by its author to be Public Domain code.

Download source code

Type Access in Lua via Reflection by Nilium
(Posted 18 years ago)
The point of this code was to test an idea I had about using reflection to automate the use of BMax types/objects in Lua. As luck would have it, it works. It's probably not very speedy, since I haven't optimized this much, but I'm pretty happy to know that it works, which is the important point for me.


[:: Function Reference ::]

# lua_implementtypes( state:lua_State )
This will iterate over all types and expose any with the {expose} attribute to Lua. See the other attributes below.

# lua_implementtype( state:lua_State, typeid:TTypeId, expose%=-1, static%=-1, noclass%=-1, hidefields%=-1 )
This will implement a specific type, specified by typeid. The remaining arguments, expose, static, noclass, and hidefields, are used to override any metadata settings on the type. Keep in mind that it is possible to implement a type twice, and if I know my Lua right it ought to just over-write the old type constructor/methods and Lua's GC will pick up the scraps. I do not recommend trying this, just because you should never have to implement a type twice for the same Lua state.

# lua_pushbmaxobject( state:lua_State, obj:Object, excludeMethods%=False )
This pushes a BMax object onto the stack using the same method that the rest of this system does. If you pass True to excludeMethods, this will more or less circumvent any use of the reflection and only push a new table with the object onto the stack. This can possibly make things run faster, especially if you don't need Lua to know what that object is (there are plenty of good reasons to do this), but I have not tested whether or not pushing objects method-less works well, so that's up to you.

# lua_tobmaxobject:Object( state:lua_State, index% )
This will retrieve a BlitzMax object from the stack. If the location in the stack specified by index is a table with an object field, you (should) get an object back. If it's not a table, not an object, nil, etc. then you will simply get Null. Errors may occur due to this being used in less than pristine conditions, so consider yourself warned.

# lua_pushbmaxarray( state:lua_State, arr:Object, excludeMethods%=False )
This pushes a BMax array onto the stack as a table. excludeMethods behaves the same way as with lua_pushbmaxobjects, and is only applicable to arrays of BMax objects.

# lua_tobmaxarray:Object[]( state:lua_State, index% )
This converts a table at index to a BMax array. This only concerns itself with the length of a table returned by lua_objlen. This is iffy at best, and I do not recommend using this extensively, if at all, for the time being.

# lua_pushbmaxtlist( state:lua_State, list:TList, excludeMethods%=False )
More or less the same as lua_pushbmaxarray, but this takes a TList rather than an array.

# lua_pushbmaxtmap( state:lua_State, map:TMap, excludeMethods%=False )
Again, more or less the same as lua_pushbmaxarray, the difference being that TMap is likely the closest in use to a Lua table. The table key for each field is the result of calling the map key's ToString().


[:: Attributes ::]

{expose} - When applied to a type, this will result in the type being exposed to Lua when ImplementTypes or ImplementType is called on it and any BMax objects pushed onto the stack will have their methods and field metatable attached to the object unless specified otherwise. Regular object instances can have their methods accessed via varname.methodName() or varname:methodName(). For instances, it is better to use the second unless you are calling the method as if it were a delegate.

{static} - When applied to a type, the type acts as a static class or namespace in Lua. An instance of the type is created and the type is created as a global table in Lua. These are accessed either via TypeName.methodName() or TypeName:methodName() - either works, but I recommend using the first. The fields of the instance this static object is based off of are accessible.

{noclass} - Requires {static}. When applied to a type, the behavior is the same as {static}, except that the functions are pushed as global variables/functions in Lua rather than as fields of a table. Only methodName() is required to call them. Fields are inaccessible using this attribute, even if static is not set.

{rename="newName"} - A form of aliasing. Renames a method, such that if a method is named lua_Print and it has the attribute {rename="Print"}, the function in Lua will be Print, not lua_Print. This can only be applied to methods, currently. I may change this later. This attribute does not apply to fields.

{hidden} - When applied to a method or field, will not expose the method/field to Lua. This is useful if you would like to only expose methods of a type to Lua.

{hidefields} - When applied to a type, not a field, this will hide all fields without exception. As a result, no metatable is set on instances of objects created with the type this is applied to.






A quick example to test with:
' test.bmx

Type Foo {expose}

    Method Bazzle( obj:Bar )
        If obj = Null Then
            Print "Null obj"
        Else
            Print obj.ToString()
        EndIf
    End Method

End Type


Type Bar {expose}

    Method ToString:String()
        Return "Woopertonville"
    End Method

End Type


Type FooBar Extends Foo {expose}
End Type


Type Common {expose static noclass}

    ' Some wrapper functions are necessary, since I cannot really do anything about exposing regular functions
    Method luaOpenFile:TStream( file:String, mode:String ) {rename="OpenStream"}

        Local read:Int, write:Int

        read = False
        write = False

        If mode.Contains("r") Then
            read = True
        EndIf

        If mode.Contains("w") Then
            write = True
        EndIf

        Return OpenFile( file, read, write )

    End Method

End Type


' Exec

Local vm:Byte Ptr = luaL_newstate()

' Call this to implement any types with the proper attributes/metadata
lua_implementtypes(vm)
' Example of forcing exposure of a type (handy when you don't want to or can't modify source)
lua_implementtype( vm, TTypeId.ForName("TStream"), True, False, False )

If lua_dofile( vm, "test.lua" ) <> 0 Then
    Print "[ERROR] "+lua_tostring( vm, -1 )
EndIf

lua_close(vm)
vm = Null

Input("Done.")


And a script:
-- test.lua

-- Random stuff

local foo,bar

foo = NewFooBar()
bar = NewBar()

foo:Bazzle(bar)
foo:Bazzle(nil)

bar:Delete()
foo:Delete()

-- Once :Delete() is called, the object is no longer usable, as the table is
-- cleared out.  If you have any data being stored inside the object's table,
-- make damn sure you've got it stored elsewhere.  I don't know why you'd
-- attach anything to the object, it's a bad idea, but whatever.


-- TStream

local mystream = OpenStream("woop.txt","w")

mystream:WriteString("Wooperton, yo.")
mystream:Close()

-- You may still call mystream:Delete(), but it is not required
mystream = nil



Finally, the code:
SuperStrict


Import brl.Reflection
Import axe.Lua


Rem	'=======================================================================

Changes

July 4, 2008 - 11:30 PM
- The method call closure no longer provides basic default arguments.  I
decided that, ultimately, this does not benefit any sort of system in the
long run and is better off removed.

- I have not tested the array code for arguments, fields, etc., much at
all, so that's still highly experimented.  I do not recommend trying to use
it, since for all I know every condition other than my own will break it.

- Prepended the object field string with 'LREF_'.

- Added some support for garbage collection of BMax objects in Lua.  This
is done through the finalizer metamethod of userdata.  Objects that are
'forcefully' deleted via the Delete method now rely upon this.  The handle
for an object, as this is how Lua accesses objects in case you didn't find
that out just by looking, is not released until the userdata is collected
by Lua's GC.  More on this below.

- There is a new metatable for garbage collection, as mentioned above.

- Metatables are created on-demand and stored in the Lua registry.  The
metatables are no longer created over and over for each and every object
pushed onto the stack.

- Due to Lua's design, meta tables cannot be assigned to light userdata.
Because of this, all use of light user data for object handles (not
methods and type ids) has been moved to full userdata.  In order to prevent
the chance that an object will be garbage-collected by Lua while still in
use (because each push creates a new userdata), generation of object
handles is now simply an ever-increasing (and eventually looping around)
Long.  Handles are stored in the LREF_objectHandles TMap.

Whenever a BMax object is pushed, a new handle is 'allocated,' if you will.
This means that at any time, an object may have as many handles as a Long
can represent (that's quite a lot).  If a handle is taken,
LREF_CreateHandle will continually increase the handle counter until it
hits a handle that is not currently in use.  If it loops around and reaches
the handle it started at, an exception is thrown.  Should you ever get this
exception, you probably have more things to worry about than running out of
handles.

- The delete method no longer has the object as an upvalue.  This means you
/must/ use the table:method() syntax to call it.  As a side-effect, you can
call the destructor as table.Delete(otherTable).  Calling :Delete does not
mean the object's handle is freed immediately, as this is left up to Lua's
garbage collector.  What this means is that an object is not freed until
you have removed all traces of it, including any method pointers from the
object's table.  Because of this, however, I'm pondering removing the
Delete method entirely, since it no longer really serves a purpose.

You do not have to call the Delete method to release an object as well,
but Delete may decrease the amount of time before the object is garbage
collected.  The most important reason to use Delete is to wipe out the
object's table, thereby removing the chance that you might accidentally use
the object again.

- I have just realized that a good chunk of this source code is whitespace
and documentation.

July 4, 2008 - 12:48 PM
- Introduced the LREF_USE_EXCEPTIONS constant, which, as the name suggests,
allows one to toggle whether or not LREF uses exceptions.  This does not
mean that errors go unhandled.  See the next note for why.  Turning this
off will only disable exceptions in functions where lua_error can be used.

- lua_error calls added after all exceptions for when LREF_USE_EXCEPTIONS
is set to 0.  Either way, the code is producing an error, letting it go
without notice is not permissible, so you have the choice of handling an
exception or letting Lua handle the error (which you can, in turn, handle
through Lua if you've read the docs).

- All error messages are now string constants at the head of functions for
the sake of organization.

June 26, 2008 - 6:49 PM
- Added lua_pushbmaxarray, lua_tobmaxarray, lua_pushbmaxtmap, and
lua_pushbmaxtlist as utility functions, since it's something you would
likely want to do at some point.

June 26, 2008 - 6:16 PM
- Changed back from pushing method names to pushing the method object.
Was mainly for debugging, and I think I've got most of the issues there
ironed out now.

June 26, 2008 - 6:00 PM
- Removed debugging code since I no longer intend to use it.

- Added ILREFException class for exception throwing.  Currently only used
for constructor call errors.

- Removed LREF_DumpStackMsg, replaced by LREF_DumpStack.  No longer takes a
message argument, instead opting to choose whether or not the information
is printed on the spot or not.  The stack string is always returned.

- Fixed a bug in LREF_TypeCall where if you used Type:Method calling, the
closure would attempt to get one more argument than it should have.  This
was because I'd forgotten to apply the use of argOffset properly to the
loop where arguments are retrieved from the stack.

- Renamed ImplementType/s to lua_implementtype/s to match Lua's naming
scheme as well as the lua_pushbmaxobject/tobmaxobject functions.

June 23, 2008 - 12:12 AM
- Switched set/gettable back to rawset/get where the object field is being
modified after deciding I did not want metatables affecting that field.
Other fields remain metatable'd.

- Changed object handle type in Lua from number to lightuserdata, since
it's just better suited for this task.

- Increased the amount of debugging code for LREF_DEBUG_HEAVY dramatically.
Setting debug info to this mode is no longer recommended unless you intend
to fix bugs in the code or want to see in detail what occurs.

- Other bug-fixes, focusing mainly on pushing/popping objects where some
data was not being popped from the stack, resulting in incorrect data
being passed between functions.

June 22, 2008 - 8:16 PM
- Fixed a brainfart where hidefields did the opposite of what it was 
supposed to do.

June 22, 2008 - 7:46 PM
- Support for get/setting type fields has been added.  The {hidden}
attribute applies to these as well, since there are obviously fields that
you do not want to be modifiable in Lua.

Because you may not want any fields exposed, I've included {hidefields} as
an attribute that will cause all fields to be hidden (it's not that they're
really hidden, it's just that the object does not get a metatable).

June 22, 2008 - 6:16 PM
- Changed all use of lua_rawset/rawget to lua_settable/gettable.  The
original intention for using rawset/get over set/gettable was that I wished
to avoid the effects of metatables on BMax objects.  However, I feel this
is not in the interest of preserving the features and flow that Lua is
known for.

- Fixed a bug in lua_tobmaxobject, where relative indices were not used
properly.  This has since been fixed, and should no longer be an issue.

June 20, 2008 - 11:56 PM
- Renamed BMX_OBJECT_FIELD to LREF_OBJECT_FIELD to be consistent with other
variable names.

June 20, 2008 - 10:10 PM
- Replaced use of lua_tointeger with lua_tonumber.  Lua uses ptrdiff_t when
you push an integer, but it does not store the number as an actual integer.
I would rather cast it to an integer myself than have it cast the double to
ptrdiff_t and then to int.  Changing LUA_INTEGER to int fixes conversion
problems, but requiring a modification to the library seems unreasonable to
me.

- New metadata attributes that apply to types are {static} and {noclass}.
Static means an instance of the object is created upon being added to Lua,
however the type itself acts similar to a static class in C#.  The type
does not have a delete method, and is accessed via <TypeName> as a global
variable.

This allows something similar to Lua's standard libraries to be implemented
in the program without writing glue functions.

A noclass type causes the methods to be global without requiring you to
access them via <TypeName>.<MethodName>. This is more convenient for widely
used functions where you don't want to specify its class each and every
time.

- Methods no longer use {expose}- instead, they are exposed unless they
have the {hidden} attribute.  Methods may be aliased via the {rename}
attribute, such as [Method Foo() {rename="Bar"}].  I'd have used {alias},
but that's reserved in BlitzMax for something.  Nobody seems to know what
for though.

- Metadata booleans are now stored in type constructors for when their
specs are overridden by the expose, static, and noclass arguments for
ImplementType.

- Some debug information is available by setting the LREF_DEBUG_MODE global
to one of the LREF_DEBUG_ constants.

- Other bug fixes, of course, but I can't remember what.

June 19, 2008
- Initial version

EndRem '=======================================================================


Rem '==========================================================================

To-do

6/20/08 - Optimize the way objects are pushed onto the stack.  Given that
methods are pushed with an object as they come, some changes may be
required to improve speed at the cost of losing method pointers of sorts.
If this had a preprocessor I could just shoehorn it in with an ifdef,
but...

	EndRem '=======================================================================


Rem '==========================================================================

Notes

This has not been extensively tested and I have no doubts that there are
still a significant amount of bugs, so I cannot guarantee that there will
be no errors.  All the work presented in this code should be treated as an
experiment and nothing more.  Of course, I expect all those using this to
understand this and, if possible, provide useful reports of errors.  If you
cannot provide that much, I am not interested in your problems.

EndRem '=======================================================================


Private

Const LREF_OBJECT_FIELD:String = "LREF_bmxObject" ' The field of BMax object tables in Lua: table = { <LREF_OBJECT_FIELD> = HANDLE }
Const LREF_METATABLE_FIELDS:String = "LREF_metatable_fields"	' Field access metatable
Const LREF_METATABLE_OBJECTS:String = "LREF_metatable_objects"  ' Userdata collection metatable
Const LREF_USE_EXCEPTIONS:Int = True

' Object handles are used for full userdata objects (not methods/type ids and such, since those do not need to have their IDs freed)
Global LREF_objectHandles:TMap = New TMap

Type LREF_ObjRef

	Field m_obj:Object
	Field m_handle:Long

	Global s_nextHandle:Long = 0

	Method Set:LREF_ObjRef( obj:Object )

		m_handle = s_nextHandle
		s_nextHandle :+ 1
		m_obj = obj
		Return Self

	End Method

	Method Get:LREF_ObjRef( handle:Long )

		m_handle = handle
		Return Self

	End Method

	Method Dispose()

		m_obj = Null
		m_handle = 0

	End Method

	Method Compare:Int( o:Object )

		Local oh:Long = LREF_ObjRef(o).m_handle

		If oh < m_handle Then
			Return -1
		ElseIf oh > m_handle Then
			Return 1
		Else
			Return 0
		EndIf

	End Method

End Type


Function LREF_CreateHandle:Long(obj:Object)
	Local ref:LREF_ObjRef = New LREF_ObjRef.Set(obj)
	Local initHandle:Long = ref.m_handle
	While LREF_objectHandles.Contains( ref )
		ref.Set(obj)

		If initHandle = ref.m_handle Then
			' For reference, this should never, ever happen - you shouldn't be able to use up all the references, you're
			' more likely to run out of memory before that I'd guess
			Throw LREF_Exception( "No object handles available", Null )
		EndIf
	Wend
	LREF_objectHandles.Insert( ref, ref )
	Return ref.m_handle
End Function


Function LREF_HandleToObject:Object( h:Long )
	Local ref:LREF_ObjRef = LREF_ObjRef(LREF_objectHandles.ValueForKey(New LREF_ObjRef.Get(h)))
	If ref Then
		Return ref.m_obj
	Else
		Return Null
	EndIf
End Function


Function LREF_ReleaseHandle( h:Long )
	Local ref:LREF_ObjRef = LREF_ObjRef(LREF_objectHandles.ValueForKey(New LREF_ObjRef.Get(h)))
	If ref Then
		ref.Dispose()
	EndIf
End Function


Public

' Exception when an error occurs
Type ILREFException Final

	Field m_stack:String
	Field m_msg:String

	Method Stack:String()
		Return m_stack
	End Method

	Method Message:String()
		Return m_msg
	End Method

	Method ToString:String()
		Return m_msg
	End Method

End Type


Function LREF_Exception:ILREFException( msg$="", state:Byte Ptr )

	Local ex:ILREFException = New ILREFException

	If state <> Null Then
		ex.m_stack = LREF_DumpStack( state, False )
	Else
		ex.m_stack = ""
	EndIf

	ex.m_msg = msg

	Return ex

End Function


Private

' Currently not implemented in axe.lua
Function LREF_lua_upvalueindex:Int(idx:Int) NoDebug
	Return LUA_GLOBALSINDEX-idx
End Function


Function LREF_ToObjectHandle:Long( state:Byte Ptr, idx:Int )

	Local p:Long Ptr
	Local handle:Long

	p = Long Ptr lua_touserdata( state, idx )
	handle = p[0]

	Return handle

End Function


Function LREF_AttachMetatable( state:Byte Ptr, idx:Int, metatable$ )

	If idx < 1 And idx > LUA_REGISTRYINDEX Then
		idx = lua_gettop(state) - (idx+1)
	EndIf

	lua_pushstring( state, metatable )
	lua_gettable( state, LUA_REGISTRYINDEX )

	If lua_type( state, -1 ) = LUA_TNIL Then

		lua_pop( state, 1 )
		LREF_CreateMetaTables(state)

		lua_pushstring( state, metatable )
		lua_gettable( state, LUA_REGISTRYINDEX )

	EndIf

	lua_setmetatable( state, idx )

End Function


' TypeNew([object])
Function LREF_TypeNew:Int(state:Byte Ptr)

	Const ERROR_NO_TYPEID$ = "Unable to construct object: no TTypeId attached to class constructor"
	Const ERROR_OBJ_NULL$ = "Unable to construct object: object passed to constructor is Null"
	Const ERROR_CANNOT_ALLOCATE_OBJ$ = "Unable to construct object: object could not be allocated"

	Local typeid:TTypeId
	Local obj:Object
	Local expose:Int, noclass:Int, static:Int, hidefields:Int
	Local objIdx:Int = -1

	typeid = TTypeId(HandleToObject(Int lua_touserdata( state, LREF_lua_upvalueindex(1) )))

	If typeid = Null Then

		If LREF_USE_EXCEPTIONS Then
			Throw LREF_Exception( ERROR_NO_TYPEID, state )
		EndIf

		lua_pushstring( state, ERROR_NO_TYPEID )
		lua_error(state)

		Return 0

	Else

		If lua_gettop(state) = 1 And lua_type( state, 1 ) = LUA_TUSERDATA Then

			objIdx = 1
			obj = LREF_HandleToObject(LREF_ToObjectHandle( state, objIdx ))

			If obj = Null Then

				If LREF_USE_EXCEPTIONS Then
					Throw LREF_Exception( ERROR_OBJ_NULL, state )
				EndIf

				lua_pushstring( state, ERROR_OBJ_NULL )
				lua_error(state)

				Return 0

			EndIf

		Else
			obj = typeid.NewObject()
		EndIf

		If obj = Null Then

			If LREF_USE_EXCEPTIONS Then
				Throw LREF_Exception( ERROR_CANNOT_ALLOCATE_OBJ, state )
			EndIf

			lua_pushstring( state, ERROR_CANNOT_ALLOCATE_OBJ )
			lua_error(state)

			Return 0

		Else

			expose = lua_toboolean( state, LREF_lua_upvalueindex(2) )
			static = lua_toboolean( state, LREF_lua_upvalueindex(3) )
			noclass = lua_toboolean( state, LREF_lua_upvalueindex(4) )
			hidefields = lua_toboolean( state, LREF_lua_upvalueindex(5) )

			LREF_PushBMaxObject( state, obj, typeid, expose, static, noclass, hidefields, objIdx )

			Return 1

		EndIf

	EndIf

End Function


' TypeFieldSet( table, key, value ) [obj.field = newvalue]
Function LREF_TypeFieldSet:Int(state:Byte Ptr)

	Local obj:Object
	Local rfield:TField
	Local name:String

	If lua_type( state, 2 ) <> LUA_TSTRING Then

		lua_rawset( state, 1 )
		Return 0

	EndIf

	name = lua_tostring( state, 2 )
	obj = LREF_GetValue( state, 1 )

	If obj Then
		rfield = TTypeId.ForObject(obj).FindField(name)
	EndIf

	If rfield <> Null And (Not rfield.MetaData("hidden")) Then
		rfield.Set( obj, LREF_GetValue( state, 3 ) )					' Is a type field
	Else
		lua_rawset( state, 1 )										  ' Not a type field
	EndIf

	Return 0

End Function


' TypeFieldGet( table, key ) [someVar = obj.field]
Function LREF_TypeFieldGet:Int(state:Byte Ptr)

	Local obj:Object
	Local typeid:TTypeId = Null
	Local rfield:TField = Null
	Local name:String

	If lua_type( state, 2 ) <> LUA_TSTRING Then

		lua_rawget( state, 1 )  ' Not a string (name), so not a field
		Return 1

	EndIf

	name = lua_tostring( state, 2 )
	obj = LREF_GetValue( state, 1 )

	If obj Then

		typeid = TTypeId.ForObject(obj)

		If typeid Then
			rfield = typeid.FindField(lua_tostring( state, -1 ))
		EndIf

	EndIf

	If rfield <> Null And (Not rfield.MetaData("hidden")) Then

		Select rfield.TypeId()

			Case FloatTypeId
			lua_pushnumber( state, rfield.GetFloat(obj) )

			Case DoubleTypeId
			lua_pushnumber( state, rfield.GetDouble(obj) )

			Case ByteTypeId, ShortTypeId, IntTypeId
			lua_pushnumber( state, Long rfield.GetInt(obj) )

			Case LongTypeId
			lua_pushnumber( state, Long rfield.GetLong(obj) )

			Case StringTypeId
			lua_pushnumber( state, Long rfield.GetString(obj) )

			Case ArrayTypeId
			lua_pushbmaxarray( state, rfield.Get(obj), False )

			Default
			LREF_ConstructBMaxObject( state, rfield.Get(obj), typeid )

		End Select

	Else
		lua_rawget( state, 1 )
	EndIf

	Return 1

End Function


' TypeDelete()
Function LREF_TypeDelete:Int(state:Byte Ptr)

	Const ERROR_NOT_AN_OBJECT$ = "Error calling type destructor: destructor called from non-object"
	Const ERROR_NO_OBJECT$ = "Error calling type destructor: invalid arguments to destructor"

	Local objIdx:Int
	Local typeid:TTypeId

	If lua_gettop(state) <> 1 Then

		If LREF_USE_EXCEPTIONS Then
			Throw LREF_Exception( ERROR_NO_OBJECT, state )
		EndIf

		lua_pushstring( state, ERROR_NO_OBJECT )
		lua_error(state)

		Return 0

	EndIf

	If lua_type( state, 1 ) <> LUA_TTABLE Then

		If LREF_USE_EXCEPTIONS Then
			Throw LREF_Exception( ERROR_NOT_AN_OBJECT, state )
		EndIf

		lua_pushstring( state, ERROR_NOT_AN_OBJECT )
		lua_error(state)

		Return 0

	EndIf

	lua_pushstring( state, LREF_OBJECT_FIELD )
	lua_rawget( state, 1 )

	If lua_type( state, 2 ) <> LUA_TUSERDATA Then

		If LREF_USE_EXCEPTIONS Then
			Throw LREF_Exception( ERROR_NOT_AN_OBJECT, state )
		EndIf

		lua_pushstring( state, ERROR_NOT_AN_OBJECT )
		lua_error(state)

		Return 0

	EndIf

	lua_pop( state, 1 )

	LREF_ClearTable( state, 1 )

	Return 0

End Function


' This is only called if the type's field
Function LREF_TypeCollection:Int(state:Byte Ptr)

	Local udata:Long = LREF_ToObjectHandle( state, 1 )
	LREF_ReleaseHandle(udata)

	Return 0

End Function


Function LREF_TypeCall:Int(state:Byte Ptr)

	Const ERROR_NULL_METHOD$ = "Unable to call method: Null TMethod object attached to closure"
	Const ERROR_NUM_ARGUMENTS$ = "Error calling $1::$2(): Expected $3 arguments, only received $4"

	Local obj:Object
	Local meth:TMethod
	Local typeid:TTypeId
	Local result:Object
	Local argOffset:Int

	obj = LREF_HandleToObject(LREF_ToObjectHandle( state, LREF_lua_upvalueindex(2) ))

	typeid = TTypeId.ForObject(obj)
	meth = TMethod(HandleToObject(Int lua_touserdata( state, LREF_lua_upvalueindex(1) )))

	If meth = Null Then

		If LREF_USE_EXCEPTIONS Then
			Throw LREF_Exception( ERROR_NULL_METHOD, state )
		EndIf

		lua_pushstring( state, ERROR_NULL_METHOD )
		lua_error(state)

		Return 0

	EndIf

	Local argTypes:TTypeId[] = meth.ArgTypes()
	Local args:Object[meth.ArgTypes().Length]
	Local argIdx:Int
	Local numArgs:Int = args.Length

	If numArgs > 0 And lua_type( state, 1 ) = LUA_TTABLE And LREF_GetValue( state, 1 ) = obj Then
		argOffset = 2
	Else
		argOffset = 1
	EndIf

	If numArgs > lua_gettop(state)-(argOffset-1) Then

		If LREF_USE_EXCEPTIONS Then
			Throw LREF_Exception( "Error calling "+typeid.Name()+"::"+meth.Name()+"(): Expected "+numArgs+" arguments, only received "+(lua_gettop(state)-(argOffset-1)), state )
			' this happens to be the only exception to storing the error as a constant in the function header
		EndIf

		lua_pushstring( state, "Error calling "+typeid.Name()+"::"+meth.Name()+"(): Expected "+numArgs+" arguments, only received "+(lua_gettop(state)-(argOffset-1)) )
		lua_error(state)
		' numArgs = lua_gettop(state)-(argOffset-1) ' No more default arguments

	EndIf

	For argIdx = 0 To numArgs-1
		args[argIdx] = LREF_GetValue( state, argOffset+argIdx )
	Next

	result = meth.Invoke( obj, args )

	If result <> Null Then

		Select meth.TypeId()

			Case LongTypeId,IntTypeId,ByteTypeId,ShortTypeId
			lua_pushinteger( state, result.ToString().ToLong() )
			Return 1

			Case StringTypeId
			lua_pushstring( state, result.ToString() )
			Return 1

			Case FloatTypeId,DoubleTypeId
			lua_pushnumber( state, result.ToString().ToDouble() )
			Return 1

			Case ArrayTypeId
			lua_pushbmaxarray( state, result, False )
			Return 1

			Default
			LREF_ConstructBMaxObject( state, result, Null )
			Return 1

		End Select

	Else

		lua_pushnil(state)
		Return 1

	EndIf

End Function


Function LREF_ConstructBMaxObject( state:Byte Ptr, obj:Object, typeId:TTypeId )

	If typeId = Null Then
		typeId = TTypeId.ForObject(obj)
	EndIf

	lua_pushstring( state, "New"+typeId.Name() )
	lua_gettable( state, LUA_GLOBALSINDEX )

	If lua_type( state, -1 ) = LUA_TFUNCTION Then

		' In the event that a constructor for the object's type already exists, use that
		Print "Using existing constructor"

		Local p:Long Ptr = Long Ptr lua_newuserdata( state, 8 )
		p[0] = LREF_CreateHandle(obj)

		LREF_AttachMetaTable( state, -1, LREF_METATABLE_OBJECTS )

		If lua_pcall( state, 1, 1, 0 ) <> 0 Then

			If LREF_USE_EXCEPTIONS Then
				Throw LREF_Exception( "Error calling constructor for "+typeId.Name()+"~nLua error: "+lua_tostring( state, -1 ), state )
			EndIf

			' I understand there is some pointlessness to handling an error here only to produce another error.
			lua_pushstring( state, "Error calling constructor for "+typeId.Name()+"~nLua error: "+lua_tostring( state, -1 ) )
			lua_error(state)

			Return

		EndIf

	Else

		lua_pop( state, 1 )

		If typeId.SuperType() Then

			' If the object extends another class, check to see if its base class has been implemented
			LREF_ConstructBMaxObject( state, obj, typeId.SuperType() )

		Else

			' If no constructor for the class exists, push it as just an object without methods
			lua_pop( state, 1 )
			LREF_PushBMaxObject( state, obj, Null, False, False, False, True, -1 )

		EndIf

	EndIf

End Function


Function LREF_ClearTable( state:Byte Ptr, idx:Int )

	Local top:Int = lua_gettop(state)

	lua_pushnil(state)

	While lua_next( state, idx ) <> 0

		lua_pop( state, 1 )
		lua_pushvalue( state, -1 )
		lua_pushnil(state)
		lua_rawset( state, idx )		' Do not want metatables here.

	Wend

	top = lua_gettop(state)-top

	If top > 0 Then
		lua_pop( state, top )
	EndIf

End Function


Function LREF_GetValue:Object( state:Byte Ptr, idx:Int )

	Local obj:Object

	Select lua_type( state, idx )

		Case LUA_TNIL
		Return Null

		Case LUA_TSTRING
		Return lua_tostring( state, idx )

		Case LUA_TNUMBER
		Return String(lua_tonumber( state, idx ))

		Case LUA_TFUNCTION
		Return Null

		Case LUA_TTABLE
		' check if the table is an object
		lua_pushstring( state, LREF_OBJECT_FIELD )
		lua_rawget( state, idx )

		If lua_type( state, -1 ) <> LUA_TUSERDATA Then	   ' Treat value as an array
			Local arr:Object[] = lua_tobmaxarray( state, idx )
			lua_pop( state, 1 )
			Return arr
		ElseIf lua_type(state, -1) = LUA_TUSERDATA Then

			' Otherwise, it's an object
			obj = LREF_HandleToObject(LREF_ToObjectHandle( state, -1 ))
			lua_pop( state, 1 )

			Return obj

		Else

			lua_pop(state, 1)

			Return Null

		EndIf

		Case LUA_TBOOLEAN
		Return String(lua_toboolean( state, idx ))

		Case LUA_TUSERDATA,LUA_TLIGHTUSERDATA
		Return String(Int(lua_topointer( state, idx )))

		Default
		Return Null

	End Select

End Function


' NOTE: consider rewriting this such that the method table is an upvalue to the
' New*() function that is then copied and modified for the object.  (May pose
' issues for method-pointer style functions.)
Function LREF_PushBMaxObject( state:Byte Ptr, obj:Object, from:TTypeId, expose:Int=-1, static:Int=-1, noclass:Int=-1, hidefields:Int=-1, objidx:Int=-1 )

	Local methIter:TMethod
	Local tableIdx:Int
	Local rename:String = Null
	Local name:String = Null
	Local ownObjIdx:Int = False

	If from = Null Then
		from = TTypeId.ForObject(obj)
	EndIf

	If expose = -1 And from.MetaData("expose") Then
		expose = True
	ElseIf expose = -1 Then
		expose = False
	EndIf

	If static = -1 And from.MetaData("static") Then
		static = True
	ElseIf static = -1 Then
		static = False
	EndIf

	If noclass = -1 And from.MetaData("noclass") Then
		noclass = True
	ElseIf noclass = -1 Then
		noclass = False
	EndIf

	If hidefields = -1 And from.MetaData("hidefields") Then
		hidefields = True
	ElseIf hidefields = -1 Then
		hidefields = False
	EndIf

	If objIdx = -1 Then

		Local p:Long Ptr = Long Ptr lua_newuserdata( state, 8 )
		p[0] = LREF_CreateHandle(obj)
		objIdx = lua_gettop(state)
		ownObjIdx = True

		LREF_AttachMetaTable( state, objIdx, LREF_METATABLE_OBJECTS )

	EndIf

	If noclass And static Then

		tableIdx = LUA_GLOBALSINDEX
		hidefields = True ' No exceptions

	Else

		' Create the new object table if it's an instance or regular static class
		tableIdx = -3

		lua_createtable( state, 0, 1 )

		lua_pushstring( state, LREF_OBJECT_FIELD )
		lua_pushvalue( state, objIdx )

		lua_settable(state, tableIdx)

	EndIf

	If from <> Null And expose Then

		' This is not something I'm particularly fond of, and I would like to
		' see if I can do this better.  Currently, the methods are iterated
		' over and pushed onto the stack each time you push an object.  This
		' doesn't apply to unexposed objects, but this may be a problem if you
		' rely heavily on Lua.
		For methIter = EachIn from.EnumMethods()

			name = methIter.Name()

			If from.MetaData("hidden") Or name.ToLower() = "delete" Or name.ToLower() = "new" Then
				Continue
			EndIf

			rename = methIter.MetaData("rename")
			If rename Then
				name = rename
			EndIf
			rename = Null

			' {Type}::{Name}
			lua_pushstring( state, name )

			'lua_pushstring( state, methIter.Name() )
			lua_pushlightuserdata( state, Byte Ptr HandleFromObject(methIter) )
			lua_pushvalue( state, objIdx )

			lua_pushcclosure( state, LREF_TypeCall, 2 )

			lua_settable( state, tableIdx )

			' NOTE: An unintended side-effect of the object being passed as an
			' upvalue is that you sort of have delegates in Lua...

		Next

	EndIf

	If Not static Then

		' {Type}::Delete method
		lua_pushstring( state, "Delete" )
		lua_pushcclosure( state, LREF_TypeDelete, 0 )
		lua_settable( state, tableIdx )

	EndIf

	If expose = True And hidefields = False And noclass = False Then  ' If the type is not exposed or if it's regular functions, fields will be inaccessible
		LREF_AttachMetaTable( state, -2, LREF_METATABLE_FIELDS )
	EndIf

	If ownObjIdx Then
		lua_remove( state, objIdx )
	EndIf

End Function


Function LREF_CreateMetaTables(state:Byte Ptr)

	' Fields
	lua_pushstring( state, LREF_METATABLE_FIELDS )
	lua_createtable( state, 0, 2 )

	lua_pushstring( state, "__newindex" )
	lua_pushcclosure( state, LREF_TypeFieldSet, 0 )
	lua_rawset( state, -3 )

	lua_pushstring( state, "__index" )
	lua_pushcclosure( state, LREF_TypeFieldGet, 0 )
	lua_rawset( state, -3 )

	lua_settable( state, LUA_REGISTRYINDEX )

	' Userdata collection
	lua_pushstring( state, LREF_METATABLE_OBJECTS )
	lua_createtable( state, 0, 1 )

	lua_pushstring( state, "__gc" )
	lua_pushcclosure( state, LREF_TypeCollection, 0 )
	lua_rawset( state, -3 )

	lua_settable( state, LUA_REGISTRYINDEX )

End Function


' Debugging code
Function LREF_DumpStack$( lua:Byte Ptr, output%=True ) NoDebug

	Local sout:String = ""
	Local lout:String
	Local idx:Int

	For idx = 1 To lua_gettop(lua)

		sout :+ "  "+("["+idx+"]")[..5]+" "

		Select lua_type( lua, idx )
			Case LUA_TBOOLEAN
			sout :+ "boolean   "+lua_toboolean( lua, idx )
			Case LUA_TNIL
			sout :+ "nil"
			Case LUA_TNUMBER
			sout :+ "number	"+lua_tonumber( lua, idx )
			Case LUA_TFUNCTION
			sout :+ "function"
			Case LUA_TUSERDATA,LUA_TLIGHTUSERDATA
			sout :+ "userdata  0x"+Hex(Int(lua_topointer( lua, idx )))
			Case LUA_TSTRING
			sout :+ "string	"+lua_tostring( lua, idx )
			Case LUA_TTABLE
			sout :+ "table	 "+LREF_TableToString( lua, idx, 6  )
			Default
			sout :+"object/unknown/null"
		End Select

		sout :+ "~n"

	Next

	If output Then
		Print sout
	EndIf

	Return sout

End Function


Function LREF_TableToString$( lua:Byte Ptr, idx:Int, indent%=0 ) NoDebug

	Local out$ = "{ "
	Local idn$ = " "[..indent]
	Local top% = lua_gettop(lua)
	Local ran% = 0
	Local key$
	Local value$

	lua_pushnil(lua)

	While lua_next( lua, idx ) <> 0
		ran = True

		key = lua_tostring( lua, -2 )

		If lua_type( lua, -1 ) = LUA_TFUNCTION Then
			value = "function"
		ElseIf lua_type( lua, -1 ) = LUA_TNIL Then
			value = "nil"
		ElseIf lua_type( lua, -1 ) = LUA_TNUMBER Then
			value = lua_tonumber(lua, -1)
		ElseIf lua_type( lua, -1 ) = LUA_TBOOLEAN Then

			If lua_toboolean(lua, -1) Then
				value="true"
			Else
				value="false"
			EndIf

		ElseIf lua_type( lua, -1 ) = LUA_TUSERDATA Then
			value = "userdata"
		ElseIf lua_type( lua, -1 ) = LUA_TLIGHTUSERDATA Then
			value = "0x"+Hex(Int(lua_touserdata(lua,-1)))
		Else
			lua_tostring( lua, -1 )
		EndIf

		out :+ idn+key+"="+value+", ~n"
		lua_pop( lua, 1 )

	Wend

	If ran Then
		out = out[..out.Length-3]
	EndIf

	If lua_gettop(lua)-top > 0 Then
		lua_pop(lua, lua_gettop(lua)-top)
	EndIf

	out :+ " }"

	Return out

End Function


Public

Function lua_implementtype( state:Byte Ptr, from:TTypeID, expose:Int=-1, static:Int=-1, noclass:Int=-1, hidefields%=-1 )

	If expose = -1 And from.MetaData("expose") Then
		expose = True
	ElseIf expose = -1 Then
		expose = False
	EndIf

	If static = -1 And from.MetaData("static") Then
		static = True
	ElseIf static = -1 Then
		static = False
	EndIf

	If noclass = -1 And from.MetaData("noclass") Then
		noclass = True
	ElseIf noclass = -1 Then
		noclass = False
	EndIf

	If hidefields = -1 And from.MetaData("hidefields") Then
		hidefields = True
	ElseIf hidefields = -1 Then
		hidefields = False
	EndIf

	If expose Then

		If static Then

			If noclass Then
				LREF_PushBMaxObject( state, from.NewObject(), from )
			Else

				lua_pushstring( state, from.Name() )
				LREF_PushBMaxObject( state, from.NewObject(), from, expose, static, noclass )
				lua_settable( state, LUA_GLOBALSINDEX )

			EndIf

		Else

			' function NewName()
			lua_pushstring( state, "New"+from.Name() )
			' upvalues - a lot of them
			lua_pushlightuserdata( state, Byte Ptr HandleFromObject(from) )	  ' uv 1
			lua_pushboolean( state, expose )
			lua_pushboolean( state, static )
			lua_pushboolean( state, noclass )
			lua_pushboolean( state, hidefields )
			' closure
			lua_pushcclosure( state, LREF_TypeNew, 5 )

			lua_settable( state, LUA_GLOBALSINDEX )

		EndIf

	EndIf

End Function


Function lua_implementtypes(state:Byte Ptr)

	Local typeIter:TTypeId

	For typeIter = EachIn TTypeId.EnumTypes()
		lua_implementtype( state, typeIter )
	Next

End Function


' Convenience functions

' Passing true to excludeMethods may be faster, at the expense of, well, not having methods
Function lua_pushbmaxobject( state:Byte Ptr, obj:Object, excludeMethods:Int=False )

	If excludeMethods Then
		LREF_PushBMaxObject( state, obj, Null, False, False, False, True )
	Else
		LREF_ConstructBMaxObject( state, obj, Null )
	EndIf

End Function


Function lua_tobmaxobject:Object( state:Byte Ptr, idx:Int )

	Local obj:Object = Null

	If idx < 1 And idx > LUA_REGISTRYINDEX Then
		idx = lua_gettop(state) - (idx+1)
	EndIf

	If lua_type( state, idx ) <> LUA_TTABLE Then
		Return Null
	Else

		lua_pushstring( state, LREF_OBJECT_FIELD )
		lua_rawget( state, idx )

		If lua_type( state, -1 ) = LUA_TUSERDATA Then

			obj = LREF_HandleToObject(LREF_ToObjectHandle( state, -1 ))
			lua_pop( state, 1 )

			Return obj

		Else

			lua_pop( state, 1 )
			Return Null

		EndIf

	EndIf

End Function


' Currently undecided on how I'll get arrays /from/ Lua, but this works on giving them to Lua
Function lua_pushbmaxarray( state:Byte Ptr, obj:Object, excludeMethods:Int = False ) ' Can be an array of objects, so excludeMethods is still an argument here

	Local typeid:TTypeId = TTypeId.ForObject(obj)
	Local idx:Int
	Local arrLen:Int

	If typeid <> ArrayTypeId Then

		If LREF_USE_EXCEPTIONS Then
			Throw LREF_Exception( "lua_pushbmaxarray: obj is not an array", Null )
		EndIf

		lua_pushstring( state, "Error calling lua_pushbmaxarray: obj is not an array" )
		lua_error(state)

		Return

	EndIf

	arrLen = typeid.ArrayLength(obj)

	lua_createtable( state, arrLen, 0 )

	Select typeid.ElementType()

		Case FloatTypeId, DoubleTypeId, LongTypeId, IntTypeId, ShortTypeId, ByteTypeId ' These are all numbers!  ('-'\)  .('-')/  (.'-')/
		For idx = 0 To arrLen-1

			lua_pushnumber( state, idx )
			lua_pushnumber( state, String(typeid.GetArrayElement( obj, idx )).ToDouble() )
			lua_settable( state, -3 )

		Next

		Case StringTypeId	   ' And this is a ball of yarn.
		For idx = 0 To arrLen-1

			lua_pushnumber( state, idx )
			lua_pushstring( state, String(typeid.GetArrayElement( obj, idx )) )
			lua_settable( state, -3 )

		Next

		Case ArrayTypeId
		For idx = 0 To arrLen-1

			lua_pushnumber( state, idx )
			lua_pushbmaxarray( state, typeid.GetArrayElement( obj, idx ) )
			lua_settable( state, -3 )

		Next

		Default				 ' This is el presidente.
		For idx = 0 To arrLen-1

			lua_pushnumber( state, idx )
			lua_pushbmaxobject( state, typeid.GetArrayElement( obj, idx ), excludeMethods )
			lua_settable( state, -3 )

		Next

	End Select

End Function


Function lua_tobmaxarray:Object[]( state:Byte Ptr, idx:Int )

	Local tableLen:Int
	Local tableInner:Int
	Local arr:Object[]

	If idx < 1 And idx > LUA_REGISTRYINDEX Then
		idx = lua_gettop(state) - (idx+1)
	EndIf

	If lua_type( state, idx ) <> LUA_TTABLE Then
		Return Null
	EndIf

	tableLen = lua_objlen( state, idx )

	If tableLen = 0 Then
		Return New Object[0]
	EndIf

	arr = New Object[tableLen]

	For tableInner = 1 To tableLen

		lua_pushnumber( state, tableInner )
		lua_gettable( state, idx )

		arr[tableInner - 1] = LREF_GetValue( state, idx )

	Next

	Return arr

End Function


' Your key must either BE a string or override ToString to have any meaningful key
Function lua_pushbmaxtmap( state:Byte Ptr, map:TMap, excludeMethods:Int = False )

	Local keyval:TNode

	lua_newtable(state)

	For keyval = EachIn map

		lua_pushstring( state, keyval.Key().ToString() )

		If keyval.Value() = Null Then
			lua_pushnil(state)
		ElseIf TTypeId.ForObject(keyval.Value()) = StringTypeId Then
			lua_pushstring( state, String(keyval.Value()) )
		ElseIf TTypeId.ForObject(keyval.Value()) = ArrayTypeId Then
			lua_pushbmaxarray( state, keyval.Value(), excludeMethods )
		Else
			lua_pushbmaxobject( state, keyval.Value(), excludeMethods )
		EndIf

		lua_settable( state, -3 )

	Next

End Function


Function lua_pushbmaxtlist( state:Byte Ptr, list:TList, excludeMethods:Int = False )

	Local item:Object

	lua_createtable( state, list.Count(), 0 )

	Local idx:Int = 1
	For item = EachIn list

		lua_pushnumber( state, idx )

		If item = Null Then
			lua_pushnil(state)
		ElseIf TTypeId.ForObject(item) = StringTypeId Then
			lua_pushstring( state, String(item) )
		ElseIf TTypeId.ForObject(item) = ArrayTypeId Then
			lua_pushbmaxarray( state, item, excludeMethods )
		Else
			lua_pushbmaxobject( state, item, excludeMethods )
		EndIf

		lua_settable( state, -3 )

		idx :+ 1

	Next

End Function