Classes Via Modules

lua-users home
wiki

This system allows you to (ab)use the require / module system to get inherited classes with no superfluous functions to be called or special class creators. You need to put the following code in a seperate file which will be called by require() once. After said file has been included, the require() function will no longer perform the original "include this file" functionality, but it overwritten with the system below (alternatively, you can change it so that it doesn't reset the package.loaders table, and instead simple adds the new function).

See the bottom of this page for an example on how to use. Feel free to credit James Rhodes (jrhodes@roket-enterprises.com) if you want, but there's no requirement to do so.

Code

-- Overwrite our loading functionality.

package.loaders = {}

package.loaders[1] = function(module, env)

	-- Declare our variables local so they don't

	-- interfere with the main program.

	if (env == nil) then

		env = _G

	end

	

	local path, ext, found_path, e, fhandle

	local chunk, lmodule, _T, lfmodulepath

	local indent, l, n, ldotpos, classname

	local namespace, class_funcs, class, mt

	local __init, f, v, k, tdotpos, __base, i

	local class_inheritance = {}

	local inherit_classname, inherit_namespaces

	local inheritance_sandbox, i2

	local function func_inherit(class)

		if (lmodule._T == nil) then

			lmodule._T = {}

		end

		

		table.insert(class_inheritance, class)

	end

	local function func_name(name)

		local ldotpos, tdotpos, classname, namespaces

		local z, x, c

		ldotpos = string.len(name)

		tdotpos = string.len(name)

		classname = nil

		namespaces = {}

		while (ldotpos > 0) do

			if (string.sub(name,ldotpos,ldotpos) == ".") then

				if (classname == nil) then

					classname = string.sub(name, ldotpos + 1, tdotpos)

				else

					table.insert(namespaces,1,string.sub(name, ldotpos + 1, tdotpos))

				end

				tdotpos = ldotpos - 1

			end

			ldotpos = ldotpos - 1

		end

		if (tdotpos > 0) then

			table.insert(namespaces,1,string.sub(name, ldotpos + 1, tdotpos))

		end

		z = {}

		for c, x in ipairs(namespaces) do

			table.insert(z,1,x)

		end

		namespaces = z

		

		if (lmodule._T == nil) then

			lmodule._T = {}

		end

		

		lmodule._T["NAME"] = classname

		if (#namespaces > 0) then

			lmodule._T["NAMESPACE"] = namespaces

		else

			lmodule._T["NAMESPACE"] = nil

		end

	end

	local function func_description(desc)

		if (lmodule._T == nil) then

			lmodule._T = {}

		end

		

		lmodule._T["DESCRIPTION"] = desc

	end

	local function func_author(author)

		if (lmodule._T == nil) then

			lmodule._T = {}

		end

		

		lmodule._T["AUTHOR"] = author

	end

	local function func_lastmodified(date)

		if (lmodule._T == nil) then

			lmodule._T = {}

		end

		

		lmodule._T["LASTMODIFIED"] = date

	end

	

	-- Replace the "." in the requested module with

	-- backslashes.

	path = string.gsub(module, "%.", "/")

	path = "./" .. path

	ext = {"rcs", "rs", "rks", "lua"}

	found_path = nil

	

	for k, e in pairs(ext) do

		fhandle = io.open(path .. "." .. e, "r")

		if (fhandle ~= nil) then

			fhandle:close()

			found_path = path .. "." .. e

			break

		end

	end

	

	if (found_path == nil) then

		print("ERR : Unable to locate module at " .. path .. ".{rcs,rs,rks,lua}.")

		return nil, "Unable to locate module at " .. path .. ".{rcs,rs,rks,lua}."

	end

	

	-- Since the file exists, we're now going to load it.

	chunk = loadfile(found_path)

	if (chunk == nil) then

		print("ERR : Module " .. module .. " contains syntax errors and cannot be included in the program.")

		return nil, "Module " .. module .. " contains syntax errors and cannot be included in the program."

	end

	

	-- Isolate the class name from the namespace component

	ldotpos = string.len(module)

	while (ldotpos > 0) do

		if (string.sub(module,ldotpos,ldotpos) == ".") then

			break

		end

		ldotpos = ldotpos - 1

	end

	classname = string.sub(module, ldotpos + 1)

	if (ldotpos ~= 0) then

		namespace = string.sub(module, 1, ldotpos - 1)

	else

		namespace = ""

	end

	

	-- Run the chunk() function inside a sandbox, so we can inspect the module.

	lmodule = {}

	lmodule[classname] = {}

	lmodule["inherits"] = func_inherit

	lmodule["name"] = func_name

	lmodule["description"] = func_description

	lmodule["author"] = func_author

	lmodule["lastmodified"] = func_lastmodified

	setfenv(chunk, lmodule)

	chunk()



	-- Check to see if _T exists, if it doesn't, show that the module can't be loaded.

	if (lmodule["_T"] == nil) then

		print("ERR : Module " .. module .. " does not specify module information.")

		return nil, "Module " .. module .. " does not specify module information."

	end

	

	-- Move the _T table from the code block, into a local variable.

	_T = lmodule["_T"]

	lmodule["_T"] = nil

	

	-- Sanitize the _T variable we will use.

	_T["NAME"]			= tostring(_T["NAME"])

	if (_T["DESCRIPTION"] ~= nil) then

		_T["DESCRIPTION"]	= tostring(_T["DESCRIPTION"])

	end

	if (_T["AUTHOR"] ~= nil) then

		_T["AUTHOR"]		= tostring(_T["AUTHOR"])

	end

	if (_T["LASTMODIFIED"] ~= nil) then

		_T["LASTMODIFIED"]	= tostring(_T["LASTMODIFIED"])

	end

	

	-- Verify that the module is located at the correct location (NAMESPACE "." NAME == module)

	if (_T["NAMESPACE"] ~= nil) then

		lfmodulepath = table.concat(_T["NAMESPACE"], ".") .. "." .. _T["NAME"]

		if (lfmodulepath ~= module) then

			print("ERR : Module name mismatch.  Loaded from " .. module .. ", but code specifies " .. lfmodulepath .. ".")

			return nil, "Module name mismatch.  Loaded from " .. module .. ", but code specifies " .. lfmodulepath .. "."

		end

	else

		lfmodulepath = _T["NAME"]

	end

	

	-- Now create the namespace tables if required.

	l = env

	if (_T["NAMESPACE"] ~= nil) then

		for k, n in pairs(_T["NAMESPACE"]) do

			l[n] = {}

			l = l[n]

		end

	end

	

	-- Get the class functions.

	class_funcs = lmodule[classname]

	lmodule[classname] = nil

	__init = nil

	__base = nil

	

	-- Build up the class.

	class = {}

	if (#class_inheritance > 0) then

		-- We need to evaluate all of the inherited class

		-- in order, to build up.

		for k, v in pairs(class_inheritance) do

			-- Isolate the class name from the namespace component

			print("INFO: Loading " .. v .. " in required module.  Namespaces / classes should not leak.")

			ldotpos = string.len(v)

			tdotpos = string.len(v)

			inherit_classname = nil

			inherit_namespaces = {}

			while (ldotpos > 0) do

				if (string.sub(v,ldotpos,ldotpos) == ".") then

					if (inherit_classname == nil) then

						inherit_classname = string.sub(v, ldotpos + 1, tdotpos)

					else

						table.insert(inherit_namespaces,1,string.sub(v, ldotpos + 1, tdotpos))

					end

					tdotpos = ldotpos - 1

				end

				ldotpos = ldotpos - 1

			end

			if (tdotpos > 0) then

				table.insert(inherit_namespaces,1,string.sub(v, ldotpos + 1, tdotpos))

			end

			

			inheritance_sandbox = {}

			package.loaders[1](v, inheritance_sandbox)

			

			-- Load the class into i.

			i = inheritance_sandbox

			for i2, n in pairs(inherit_namespaces) do

				i = i[n]

			end

			i = i[inherit_classname]

			

			-- Now copy all of the inherited classes functions.

			for n, f in pairs(i) do

				if (n == "__init") then

					__base = f

					setfenv(__base, env)

				elseif (type(f) == "function") then

					class[n] = f

					setfenv(class[n], env)

				elseif (type(f) ~= "function") then

					class[n] = f

				end

			end

		end

	end

	for n, f in pairs(class_funcs) do

		if (n == "__init") then

			__init = f

			setfenv(__init, env)

		else

			class[n] = f

			setfenv(class[n], env)

		end

	end

	for n, v in pairs(lmodule) do

		if (n ~= "inherits"

			and n ~= "name"

			and n ~= "description"

			and n ~= "author"

			and n ~= "lastmodified") then

			if (type(v) == "function") then

				print("WARN: " .. lfmodulepath .. ":0: Function " .. n .. " defined without class context.  It is not included in the class definition.")

			else

				-- Make the specified variable a static variable.

				class[n] = v

			end

		end

	end

	mt = {}

	mt.__call = function(class_tbl, ...)

		local obj = {}

		setmetatable(obj, class)

		if (__init ~= nil) then

			__init(obj, ...)

		elseif (__base ~= nil) then

			__base(obj, ...)

		end

		return obj

	end

	class.__index = class

	class.__init = __init

	class.__base = __base

	setmetatable(class, mt)

	

	-- Now put the newly generated class in the namespace.

	l[classname] = class

	

	-- Show module loaded message

	print("INFO: " .. lfmodulepath .. ":0: Loaded module " .. _T["NAME"] .. ".")

	indent = string.rep(" ", string.len("INFO: " .. lfmodulepath .. ":0: "))

	if (_T["NAMESPACE"] ~= nil) then

		print(indent .. "    in namespace " .. namespace)

	end

	if (_T["DESCRIPTION"] ~= nil) then

		print(indent .. "    Description: " .. _T["DESCRIPTION"])

	end

	if (_T["AUTHOR"] ~= nil) then

		print(indent .. "    Author: " .. _T["AUTHOR"])

	end

	if (_T["LASTMODIFIED"] ~= nil) then

		print(indent .. "    Last Modified: " .. _T["LASTMODIFIED"])

	end

	

	return function()

	end

end

Usage

Each class has it's own file, so for the file displayed below, you would place it in a directory called AnotherNamespace?, and name the file ClassB.lua. Notice it inherits from MainNamespace?.ClassA, so you would also need to create that file as well (you can comment out the inherits if you want to just test the code).

It's important to note that to inherit multiple classes, you simply call inherits() multiple times. The last call will have the most effect, so for example, if two classes referenced by inherits() defined an A() function, the last call would be the definition for A(), except if ClassB below override it itself. Even though you can inherit multiple classes, self.__base() always points to the constructor of the last inherited class.

name "AnotherNamespace.ClassB"

inherits "MainNamespace.ClassA"

description [[Provides advanced class functionality.]]

author "James Rhodes"

lastmodified "20th May, 2010"



myStaticVariable = 5



function ClassB:__init()

	self.__base()

	print "ClassB constructor!"

end



function ClassB:Function()

	print "Called from MyClassFunction (ClassB)"

end


RecentChanges · preferences
edit · history
Last edited May 20, 2010 3:24 pm GMT (diff)