Skip to content

Instantly share code, notes, and snippets.

@apendley
Last active December 16, 2015 09:18
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save apendley/5411561 to your computer and use it in GitHub Desktop.
Save apendley/5411561 to your computer and use it in GitHub Desktop.
Work in progress on a module system designed for Codea
--# cmodule
-- cmodule
-- v0.1.1
-- todo:
-- * document
-- * look for optimizations
-- * investigate better ways to handle dependency cycles
-- * support exported projects
-- - detect whether program is running in Codea or exported project
-- - reverse engineer exported project file layout
-- - load from alternate location when running an exported project
-- import global functions
local loadstring, fopen = loadstring, io.open
local insert = table.insert
local format, rep = string.format, string.rep
local setfenv, setmetatable = setfenv, setmetatable
local pairs, ipairs = pairs, ipairs
-- loaded modules
local _modules = {}
-- search path projects
local _path = nil
-- unique module environment key to specify caching options
local function opt_cache() return opt_cache end
-- path to Codea's document's directory
local _docs = os.getenv("HOME") .. "/Documents/"
-- name of currently running project
local _this = nil -- set by cmodule()
-- boilerplate function headers for modules
local _importHeader = "return function(_M, __proj, __file, __pathto, cinclude)"
local _loadHeader = "return function(__proj, __file, __pathto)"
-- convert codea path to filesystem path
local function _fspath(p, f)
return _docs .. p .. ".codea/" .. f
end
local function _appendExt(f)
return f:find('%.') and f or (f..".lua")
end
local function _removeExt(f)
local dot = f:find('.', 1, true)
return dot and f:sub(1, dot-1) or f
end
-- parse for module name when compiler exceptions occur
local function _shortsource(s, maxlength)
local max = 60
s, maxlength = s:sub(1, max), maxlength or 30
max = 1 + s:len()
local n, c
for i = 1, max do
n, c = i, s:sub(i, i)
if not(c == ' ' or c == '\n') then
break
end
end
if n == max then return '' end
local start = n
for i = start, max do
c = s:sub(i, i)
if not(c == '\n' or i >= start + maxlength) then
n = i
else break end
end
return s:sub(start, n)
end
-- TODO: when Codea allows optional tab execution,
-- we'll need to take into account added boilerplate line
-- at the top of the module when reporting errors
local function _traceback(minlevel)
minlevel = minlevel or 2
local t = {}
for level = minlevel, math.huge do
local info = debug.getinfo(level, "Sln")
if not info then break end
if info.what == "C" then
insert(t, 'C function');
else
local name, desc = info.name, _shortsource(info.source)
if desc == '' then desc = 'unknown' end
desc = '[' .. desc .. ']'
if name then desc = desc .. ":" .. name end
local currentline = info.currentline
if info.currentline == -1 then currentline = '?' end
s = desc .. ":" .. currentline .. "\n"
insert(t, s)
end
end
return "\ntrace:\n" .. table.concat(t)
end
-- remove the block comment wrapper, and
-- wrap the contents of a file with our boilerplate code
local function _wrap(s, p, t, header)
-- TODO: present error on malformed long comment wrapper
-- find the first occurrence of a long comment, so
-- we can see how many = are in it to refine our search
local b, e = s:find('--%[[%=]*%[')
local ne = rep('[%=]', (e-b)-3)
-- do end first
b, e = s:find('--%]' .. ne .. '%]')
e = s:find('\n', e, true)
local cs = s:sub(b, e):gsub('%]', '%%%]')
s = s:gsub(cs, 'end')
-- now do function header
b, e = s:find('--%[' .. ne .. '%[')
e = s:find('\n', e, true)
-- wrap the source and return it
local oc = '--[[ ' .. p .. ':' .. t .. ' --]] '
cs = s:sub(b, e-1):gsub('%[', '%%%[')
return s:gsub(cs, oc..header)
end
-- check to see if a module has already been loaded;
-- if so, return it's data
local function _findLoaded(cpath, fallback)
local spos = cpath:find(":", 1, true)
if spos ~= nil then
cpath = _appendExt(cpath)
return _modules[cpath], true, cpath, cpath:sub(1, spos-1), cpath:sub(spos+1)
else
-- check running project first
local file, cpath = cpath, _appendExt(_this .. ":" .. cpath)
local mod = _modules[cpath]
if mod ~= nil then
return mod, false, cpath, _this, cpath:sub(#_this+2)
end
-- now check all of the projects in the search path
if _path then
local project
for i = 1, #_path do
project = _path[i]
cpath = _appendExt(project .. ":" .. file)
mod = _modules[cpath]
if mod ~= nil then
return mod, false, cpath, project, cpath:sub(#project+2)
end
end
end
-- check fallback
if fallback then
cpath = _appendExt(fallback .. ":" .. file)
mod = _modules[cpath]
if mod ~= nil then
return mod, false, cpath, fallback, cpath:sub(#fallback+2)
end
end
-- module is not loaded
return nil, false, nil, nil, _appendExt(file)
end
end
-- search for a module's file in the search chain
local function _search(f, fallback)
-- check running project first
local file = fopen(_fspath(_this, f), "r")
if file ~= nil then
return _this, file
end
-- check search path
if _path then
local p = _this
for i = 1, #_path do
p = _path[i]
file = fopen(_fspath(p, f), "r")
if file ~= nil then
return p, file
end
end
end
-- check fallback
if fallback then
file = fopen(_fspath(fallback, f), "r")
if file ~= nil then
return fallback, file
end
end
end
-- load a module given a file, project name, file name, and module header
local function _readmodule(file, p, f, header)
local s = file:read("*a")
file:close()
local tab = _removeExt(f)
local chunk, e = loadstring(_wrap(s, p, tab, header), tab)
if e then
error(format("\n\n%s\n%s", e, _traceback(4)))
end
return chunk
end
-- forward declare for _override
local import
-- memoize overridden cinclude closures
-- since we only need one per owning project.
local _overrides = {}
-- provide syntactic sugar for cimport when overriding
-- search path to load from containing project. e.g.:
-- cinclude "SomeModule"
-- is the same as
-- cimport(__proj "SomeModule")
local function _override(prefix)
local override = _overrides[prefix]
if not override then
override = function(modulename)
return _import(prefix .. modulename)
end
_overrides[prefix] = override
end
return override
end
-- default module metatable/index
local _defaultMT = {__index = _G}
-- memoize __pathto closures, since we only need
-- one per owning project
local _pathto = {}
-- prepare sandboxed environment for loaded modules
local function _makeEnv(p, f, e)
e = setmetatable({}, e and {__index = e} or _defaultMT)
local prefix = p .. ":"
local pathto = _pathto[prefix]
if not pathto then
pathto = function(f) return prefix .. f end
_pathto[prefix] = pathto
end
return e, pathto, prefix
end
-- return a string listing the current project path
local function _pathString()
local spath
if _path and #_path > 0 then
spath = "{"
for i, v in ipairs(_path) do
spath = spath .. v
if i < #_path then
spath = spath .. ", "
end
end
return spath .. "}"
end
return "{none}"
end
-- keep track of modules that are currently loading,
-- so that we can detect dependency cycles
local _loading = {}
-- import a module. imported module is memoized so
-- successive imports do not have to re-load the module
-- each time. If nothing is returned, the module environment
-- itself will be imported (i.e. the module's _M table)
function _import(codeapath, fallback)
local mod, absolute, cpath, p, f = _findLoaded(codeapath)
if mod == nil then
local file
if absolute then
if p then
file = fopen(_fspath(p, f), "r")
end
else
p, file = _search(f, fallback)
if p then cpath = p .. ":" .. f end
end
if not file then
local spath = _pathString()
error(format("Module not found: %s\n\nin search path:%s\n\nfallback:%s\n\n%s", codeapath,
spath, fallback or "none", _traceback(3)))
end
if _loading[cpath] then
error(format("circular dependency detected loading %s\n%s", cpath, _traceback(3)))
end
_loading[cpath] = true
local loaded = _readmodule(file, p, f, _importHeader)()
local env, pathto, prefix = _makeEnv(p, f)
mod = setfenv(loaded, env)(env, p, f, pathto, _override(prefix)) or env
_loading[cpath] = nil
local cache = env[opt_cache]
if cache ~= nil then
env[opt_cache] = nil
if cache == false then
return mod
end
end
_modules[cpath] = mod
end
return mod
end
-- load a data module. if an environment is provided, it will
-- be the sandboxed module environment. if no environment
-- is provided, the global table will be exposed to the
-- module environment.
local function _load(codeapath, environment, fallback)
if not fallback and type(environment) == "string" then
fallback, environment = environment
end
local spos, p, f, cpath, file = codeapath:find(":", 1, true)
if spos ~= nil then
cpath = _appendExt(codeapath)
p, f = cpath:sub(1, spos-1), cpath:sub(spos+1)
file = fopen(_fspath(p, f), "r")
else
f = _appendExt(codeapath)
p, file = _search(f, fallback)
if p then cpath = p .. ":" .. f end
end
if not file then
local spath = _pathString()
error(format("Module not found: %s\n\nin search path:%s\n\nfallback:%s\n\n%s", codeapath,
spath or "{none}", fallback or "none", _traceback(3)))
end
if _loading[cpath] then
error(format("circular dependency detected loading %s\n%s", cpath, _traceback(3)))
end
_loading[cpath] = true
local loaded = _readmodule(file, p, f, _loadHeader)()
local env, pathto = _makeEnv(p, f, environment)
mod = setfenv(loaded, env)(p, f, pathto) or env
_loading[cpath] = nil
return mod
end
---------------------------
-- exports
---------------------------
-- import a source module; identical to cmodule.import
_G.cimport = _import
-- load a data module; identical to cmodule.load
_G.cload = _load
-- cmodule utitlities
_G.cmodule = setmetatable({
-- set/get the project path. the path may only be set once
path = function(...)
local narg = select("#", ...)
-- return the current path if no params are specified
if narg == 0 then
-- return a copy of the path so it can't be modified
return _path and {unpack(_path)} or nil
end
if _path then
error("cmodule.path may only be set once")
end
_path = {...}
end,
-- return the name of the running project
project = function() return _this end,
-- unload a loaded module
-- params: module name or absolute module path
unload = function(codeapath, fallback)
local mod, _, cpath = _findLoaded(codeapath, fallback)
if mod then
_modules[cpath] = nil
end
end,
-- params: module name or absolute module path
-- returns: true if a module can be located and loaded; else false
exists = function(codeapath, fallback)
local spos = codeapath:find(":", 1, true)
local absolute = (spos ~= nil)
local p, f, cpath, file
if absolute then
cpath = _appendExt(codeapath)
p, f = cpath:sub(1, spos-1), cpath:sub(spos+1)
file = fopen(_fspath(p, f), "r")
else
f = _appendExt(codeapath)
p, file = _search(f, fallback)
if p then cpath = p .. ":" .. f end
end
if file then
file:close()
return true, cpath
end
return false
end,
-- params: none, module name, or absolute module path
-- returns: if no params are specified, array of loaded modules;
-- otherwise, module path if module is found, or nil if not found
loaded = function(codeapath, fallback)
if codeapath then
local _, _, cpath = _findLoaded(codeapath, fallback)
return cpath
else
local loaded = {}
for k in pairs(_modules) do insert(loaded, k) end
return loaded
end
end,
-- split an absolute path into separate project and file components;
-- if relative path is used, currently running project name is appended
-- params: absolute module path or module name in current project
-- returns: project name, tab name
resolve = function(cpath)
cpath = _appendExt(cpath)
local i = cpath:find(":", 1, true)
if i then
return cpath:sub(1, i-1), cpath:sub(i+1), cpath
end
return _this, cpath, _this .. ":" .. cpath
end,
-- utility to export multiple variables to the global namespace
-- params: table containing key/value pairs to be exported to _G
gexport = function(exports)
for k, v in pairs(exports) do
_G[k] = v
end
end,
-- by default, all modules loaded by cimport are cached.
-- to disable caching for a module, add this to your module file:
-- _M[cmodule.cache] = false
cache = opt_cache,
}, {
-- params: project name.
-- returns: cmodule.import, as a convenience.
-- notes: this should be executed before any
-- other code in your program.
__call = function (_, thisProject)
-- set currently running project name
_this = thisProject
-- return import to allow for a chained initialization/import,
-- for example: cmodule "myProject" "setup"
return _import
end,
})
--# Main
-- Main
-- see cmodule_tests at: https://gist.github.com/apendley/5594141
-- for very basic examples of cmodule usage;
-- better tests are on the way.
--# CHANGELOG
--[[
v0.1.1: (5/21/2013)
* fixed bug where _M[cmodule.cache] setting was not being removed from the
module enviroment after loading.
v0.1.0: (5/21/2013)
* added search path for locating relative module paths; search path can be
set/inspected using cmodule.path(); pass no args for a list of projects in the path,
or pass a variable list of projects, i.e. cmodule.path("project1", "project2", "project3").
search path may only be set once. module search is executed in the order the projects
are passed to cmodule.path.
* added optional fallback project param to cimport/cload and cmodule.loaded/exists/unload;
if a module is not found in the running project or in the search path, an attempt will
be made to load from the fallback project if specified.
* __pathto no longer returns fully qualified path to current module when no
module name is specified; use __pathto(__file) instead.
* no longer generating module closures for cload; use cload(__proj "MyModule") instead.
* renamed overridden module cimport to cinclude; cinclude only takes a module
name, not an absolute path; it is intended for loading from the containing project
only; it is syntactic sugar for cimport(__pathto "MyModule")
* now memoizing module cinclude/__pathto closures, which can reduce memory
footprint considerably, and speed up loading/importing as well by not having
to create 2 closures per model per owning project (now it's 2 closures per owning project)
* removed redundant cmodule.import and cmodule.load. use cimport/cload instead.
v0.0.9: (5/17/2013):
* removed cmodule.null. it is no longer necessary since module
module returns are no longer weak referenced, and all modules
loaded with cimport are kept alive unless explicitly unloaded.
* replaced cmodule.nocache with cmodule.cache; to achieve the same effect,
put this line of code in your module file: _M[cmodule.cache] = false
v0.0.8 (5/17/2013):
* cmodule now keeps strong references to module return values
* added API cmodule.unload() to unload a loaded module.
v0.0.7 (5/17/2013):
* cimport/cload now only return 1 value, the value returned from the loaded module
* added cmodule.resolve, which returns the project and file names for the given path
if it is valid, nil otherwise.
v0.0.6 (5/17/2013):
* cmodule.loaded now auto-applies the .lua extension if no extension is specified.
* cmodule.loaded auto pre-pends the running project name if no project is specified
v0.0.5 (5/16/2013):
* fixed bug from 0.0.4 where cmodule would not return nil for newly loaded
modules that return cmodule.null
* added function cmodule.gexport, that accepts a table of key value pairs
that are batch exported to the global environment
* add value cmodule.nocache that can be returned from a module to indicate
* that cmodule should not cache the module (useful for running once-off scripts)
v0.0.4 (5/15/2013):
* added unique null type, accessible via cmodule.null
* cimport will now return nil for modules that return cmodule.null
--]]
--# README
--[====[
What is cmodule?
================
cmodule is a package-based source loader designed for Codea.
Why use cmodule?
================
Codea's built in source execution environment is fantastic for getting simple projects up and running quickly. However, once a project becomes larger in scope, it can get difficult to manage the global namespace, and to reason about the dependency graph of your project. This situation can be a hotbed for difficult to diagnose bugs. Additionally, all tabs in a Codea project are loaded and executed by default. This can cause memory to fill up unnecessarily when some of the tabs may be used infrequently (such as level data in a game). These problems can be further exascerbated when collaborating with others. cmodule attempts to address these issues, by providing a modular system for loading and executing source code. Every module declares it's dependencies explicitly by calling cmodule's cimport() and cload() functions, which eliminates the visibility of code that is not used by a module, thus helping to reduce bugs considerably. cmodule also allows for sandboxed loading and execution of tabs that are intended to be used as data files, allowing you to fully customize the format of your application data, as well as only having the data resident in memory when it is in use.
One side effect of the addressing the issues stated above is that (given wide enough adoption) code using a module system tends to be easier to share. Packaging libraries and middleware for use by others becomes much simpler.
How does cmodule work?
======================
cmodule allows you to load and execute any tab from any project currently resident in Codea on your device, without using Codea's project dependency feature. In the current pre-release form, it exploits a feature of Lua's block commenting and a bug in Codea's syntax highlighting to convince Codea not to execute the code in every tab in a project. It works by wrapping the contents of every tab that cmodule loads in a Lua block comment. Fortunately, Lua's block commenting feature is very flexible, and allows for nested block comments, so that you can still use block comments freely in your code. The form of block comment you are probably most familiar with looks like this:
--[[ this is a block comment --]]
Incidentally, that form can be expanded like so:
--[=[ this is a block comment --]=]
You can actually use as many = as you like:
--[=====[ this is a block comment --]=====]
By combining these forms, you can nest block comments:
--[=[
This is a block comment
--[[
This is a nested block comment
--]]
--]=]
cmodule is agnostic to which form you use to wrap your module's contents, so you may use whichever you like. I tend to prefer using two ==, like so:
--[==[
module contents here
--]==]
If you are reading this in Codea's text editor, you've probably already noticed the second exploit: Codea does not properly highlight block comments, so you still get the benefits of syntax highlighting when authoring your modules in Codea.
** Please note that this workaround will not be necessary for the final release. On Codea's feature tracker is a feature that allows for optional tab execution, which directly addresses the need for this workaround. In a not-entirely-coincidental time frame, cmodule should reach 1.0 **
When the end programmer calls cimport, cmodule attempts to load a module matching either a) an absolute path to a module, such as "MyProjectName:MyModuleName", or a relative path to a module, such as "MyModuleName".
If an absolute path is specified, the module at that path will be loaded, executed, and it's results (usually whatever the module returns) are cached for future reference. As such, subsequent calls to cimport using the same module path/name will not need to re-load and execute the module again, since the cached result exists. If a module is not found at an absolute path, a "module not found" error will be raised.
If a relative path is specified, cmodule will first attempt to load the module from the currently running project. If the module is not found, cmodule then attempts to find the module in any projects contained in the search path. If a module is still not found, a "module not found" error is raised (there is one exception to this, discussed later in the "fallbacks" section).
cload is a variant of cimport that is intended primarily for loading data modules in an optionally sandboxed environment. It follows the same module search procedure as cimport, though modules loaded by cload are not cached; every call to cload() with the same module path/name will result in the module being loaded and executed each time.
Getting started
===============
You can begin using cmodule in a few simple steps:
1. Download the cmodule source into it's own Codea project. Preferably, for compatibility with others, name the project "cmodule".
2. In your own project, include the cmodule project as a project dependency.
3. Either at the top of your Main tab, or at the beginning of setup, include:
cmodule "<project name>"
where <project name> is the name of your project as Codea knows it.
4. (optional) Specify the module search path for your project:
cmodule.path("SomeProject", "SomeOtherProject", <etc>)
5. Start loading modules! Use cimport() to load source modules, and cload() to load data modules. Note that modules loaded by cmodule must follow the format outlined in the "How does cmodule work?" section.
Note that it is possible to load cmodule modules even if all of the modules in your project do not adhere to the cmodule specification. You may still take advantage of Codea's auto loading/execution to export code to the global namespace (that is, you may use a mix of cmodule and non-cmodule code together, with care).
fallbacks
=========
Fallbacks provide a mechanism for specifying that you would like a project added to the end of the search path at the time of calling cimport/cload. When a fallback is specified, if a module is not found in the running project, or in the project's search path, cmodule will attempt one last time to load it from the fallback project. Commonly, this is useful for specifying that you would like to load a module from the calling module's containing project, but only if that module is not found first in the running project or a project in the search path.
cmodule API
===========
cmodule
-------
description:
Initialize cmodule
usage:
cmodule(projectName)
params:
projectName: string containing the name of the currently running project, as Codea knows it.
returns:
cimport, for chaining cmodule initialization with an immediate module import.
path
----
description:
Specify a project's module search path, or get the current project's search path.
usage:
cmodule.path([first [,second] [,third] ... ])
params:
none, or a variable list of strings containing the names of projects to include in the search path.
returns:
if zero parameters specified, a table of strings containing project names in the search path;
othersize, nil.
project
-------
description:
returns the name of the currently running project.
usage:
cmodule.project()
cimport
-------
description:
loads and executes, and caches a source module
usage:
cimport("<module path or name>" [,fallback])
params: the first param is either an absolute module path ("MyProjectName:MyModuleName") or
a relative module path ("MyModuleName").
fallback: if specified, cimport will attempt to load a module from a relative path using
the fallback project, if the module is not first found either in a) the running project,
or b) a project in the search path.
returns: upon success, returns result of module execution.
upon failure, throws a "module not found" error
cload
-----
description:
loads and executes an optionally sandboxed data file
usage:
cload("<module path or name>" [,environment] [,fallback])
params: the first param is either an absolute module path ("MyProjectName:MyModuleName") or
a relative module path ("MyModuleName").
environment: a table; if specified, cimport will use execute the module using the table
as it's running environment. Use this to enable sandboxed module execution.
if no environment is specified, the global environment (_G) will be exposed.
fallback: if specified, cload will attempt to load a module from a relative path using
the fallback project, if the module is not first found either in a) the running project,
or b) a project in the search path.
returns: upon success, returns result of module execution.
upon failure, throws a "module not found" error
loaded
------
description: returns a list of currently loaded modules, or returns the path of a module
if it is loaded.
usage:
cmodule.loaded("<module path or name>" [,fallback])
params:
zero, or:
first and fallback params follow the same rules as cimport/cload for locating modules.
returns:
if zero arguments are specified, a table containing a list of strings
containing the absolute path of each loaded module.
otherwise, the module's absolute path if module is loaded, or nil if not.
exists
------
description: identify whether a module exists given a module path or name.
usage:
cmodule.exists("<module path or name>" [,fallback])
params:
first and fallback params follow the same rules as cimport/cload for locating modules.
returns:
if module exists, true and the absolute path to the module.
otherwise, false
unload
------
description: unload a loaded module
usage:
cmodule.unload("<module path or name>" [,fallback])
params:
first and fallback params follow the same rules as cimport/cload for locating modules.
resolve
-------
description: get the separated components of an absolute path and the fully qualified path
or the components of a path to the currently running project
usage:
cmodule.resolve("<module path or name>")
params: absolute path to module, or name of module in currently running project.
returns: full path to specified module
gexport
-------
description: export multiple objects to the global namespace
usage:
cmodule.gexport(map)
params:
map: a table containing key value pairs to map to the global namespace.
The module environment
======================
Every module it's loaded with it's own environment. The module environment provides additional API to modules loaded with cimport and cload.
__proj, __file
--------------
Every module contains 2 variables to access the owning project's name, and the module's name. For example, inside of a module located at "Foo:Bar", __proj will contain "Foo", and __file will contain "Bar".
__pathto
--------
As a convenience, each module is also provided with a helper function, __pathto. __pathto accepts a module name as it's only parameter, and returns a cmodule path. For example, in module "Foo:Bar", consider:
local path = __pathto(__file)
Upon execution, path will contain "Foo:Bar". You can also use __pathto to obtain the path to another module contained in the same project. Again, in module "Foo:Bar", consider:
local path = __pathto("MyModule")
Upon execution, path will contain "Foo:MyModule". Combined with cimport, you may specify that you explicitly want to load a module from the same project, without having to literally use the project's name. Again, in module "Foo:Bar", consider:
local MyModule = cimport(__pathto "MyModule")
Upon execution, MyModule will contain the result of importing "Foo:MyModule". This usefulness of this becomes especially apparent when you duplicate/rename a project: if you had to use a string literal to specify the owning project's name, you'd have to update every single cimport() call that did so! Using this facility, this is not a problem.
cinclude
--------
Because loading a module from the same project is common, cmodule provides each module with a bit of syntactic sugar for doing so, in the form of an alternative to cimport: cinclude.
cinclude simply takes the name of a module in the same project:
local MyModule = cinclude "MyModule"
is the same as
local MyModule = cimport(__pathto "MyModule")
_M: the environment table
-----------------------------------
Every module also has an implicit global environment. That is to say, within a module, _G is not the global enviroment. Instead, there is _M. Any implicit assignments to the global environment will go to the _M table, rather than _G. Consider:
myVariable = 10
Upon execution, _M.myVariable will contain the value 10, *not* _G.myVariable.
Since every module still has access to the global table, it is still possible to access it with _G. For example, to write to the global table:
_G.myVariable = 10
You don't however need to prefix your variable if you are *reading* from the global table.
print(myVariable) -- prints 10
A variable in _G with the same name as a variable in _M will be overshadowed:
_G.myVariable = 10
print(myVariable) -- prints 10
_M.myVariable = 20
print(myVariable) -- prints 20
print(_G.myVariable) -- prints 10
A nice benefit of this is that you cannot accidentally import variables to the global environment if you forget to declare a variable as local. They will be placed in the module's environment, or _M table, instead. In the next section we'll see a technique for writing module code that allows the _M table to be used as the module's result.
exporting from a module
-----------------------
Modules have 2 means to "export" their result. That is, when cimport/cload is called with a module path, a module typically exports a value that is then returned from cimport/cload.
Method 1: explicit export via return
------------------------------------
Since a module is essentially just a Lua function, it can return values like any other Lua function.
cmodule supports returning a single value from a module. That value may be any non-nil Lua type, such
as a tables, functions, coroutines, numbers, etc. If nil as returned, the effect is the same as
Method 2 below.
Method 2: implicit export
-------------------------
If a module chooses to return nil, or returns nothing at all (which is the same as returning nil),
cimport/cload will return the module's _M table. Therefore, anything assigned to the _M table will
be accessible via the table returned from cimport/cload. IMPORT SECURITY NOTE: Due to the way cmodule
sandboxes modules, the global environment can be accessed via an exported module environment:
-- the "Module" module uses method 2 to export it's values
local Module = cimport("Module")
Module._G.whatever = 10
Be aware of this when creating your sandboxes for loading data using cload, since it allows for the
sandbox to be broken. When in doubt, explicitly return your exports from any modules that you are exposing
in your sandbox (using method 1).
module caching
--------------
By default, cimport caches all modules that are loaded successfully, so that subsequent calls to cimport with the same module path/name will simply return the previously cached version. Sometimes this is not desireable; sometimes you want to ensure that a module is compiled and executed each time it is imported. A module may specify that it does not wish to be cached by including this line of code somewhere in the module's contents:
_M[cimport.cache] = false
tips and tricks
===============
explicitly loading from common project vs. implicit search
-------------------------------------------------------------
Loading a module from the same containing project is a common occurrence. Typically, there are 2 ways to do this, each with it own's strengths:
1) use cinclude("Module") or cimport(__proj "Module") to load explicitly from containing project. Since this method generates an absolute path that is passed to cimport, *only* this path is considered as a candidate from which we should load the module. If module is not found in containing project, a "module not found" error will be thrown.
2) use cimport("Module", __proj) to search for module in running project and then search path *before* checking the containing project. Using this form allows you to write library code with overrideable behavior by client code.
Generally speaking, use 1) when a) you are sure you don't need to allow overrides for a module, and b) when a piece of code that allowed overriding before as a debugging mechanism has become mature enough to use absolute paths instead.
Use 2) when to do things like a) read library-specific config files from the running project, b) debug library modules by using the search path to override them, and c) I'm sure there's other reasons, you get the idea.
--]====]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment