I've been thinking that the best way to handle "modules" (loadable and unloadable bits of code that may have inter-module depencies) is to use the property tree. What this really allows for is requesting loading/unloading using the functionality of listeners. Loading is simply loading file(s) into a namespace, unloading potentially triggers listeners set during loading, and reloading - well, I have to decide on that one. There's a tree structure setup in property-tree://nasal/; /nasal/base-dir[0..n] are standard locations to search for Nasal files (currently $FG_ROOT/Nasal and $FG_HOME/Nasal, probably $FG_AIRCRAFT/Nasal, and maybe also $FG_NASAL and such). /nasal/base-dir has "file" and "dir" nodes which have "loaded", "load-requested", "unload-requested" nodes (bool), "path" (relative to parent), "absolute-path", and "namespace" nodes (string). dir can also contain dir and file nodes, recursively. Loading a dir involves loading all of its children; first files then directories (outside-in loading order, because I determined that makes most sense).
One thing this does is allow for circular dependencies that resolve really well. For instance, one can specify a list of symbols for the require() directive that must be present; if they aren't, the loading fails.
It also should allow for runlevels, early Nasal initialization (parsing command line arguments!! ), and C++ code can simply set a property to request "all of the rest of Nasal be loaded".
I have an initial sketch of a "nasal_bootstrap.nas" file (untested, I don't have much access to FG these days):
- Code: Select all
# Runs the caller in the desired namespace; returns 1 if it had
# to be called again, 0 if it was already in the namespace.
#
# Defining the namespace:
# Method 1: names
# run_in_namespace can accept multiple scalars specifying
# a path from the global namespace, e.g. "io" (globals.io)
# or "canvas", "svg" (globals.canvas.svg). If the namespace
# does not exist, it is created.
# Method 2: hash
# It can also accept a direct hash for the namespace.
#
# Common usage:
# run_this_in_namespace(...) and return;
#
# if (run_this_in_namespace) return;
var run_this_in_namespace = func(ns...) {
if (size(ns) == 1 and typeof(ns[0]) == 'hash') {
ns = ns[0];
} else {
var namespace = globals;
foreach (var n; ns) {
if (typeof(namespace) == 'hash' and !contains(namespace, n))
namespace = (namespace[n] = {});
else namespace = namespace[n];
}
ns = namespace;
}
var c = caller(1);
if (c[0] == ns) return 0;
else {
call(c[1], c[0]["arg"], c[0]["me"], ns);
return 1;
}
}
# Load a sub-module or file if not already loaded.
# @param module_path A sufficiently resolvable path pointing
# to a *.nas file or a directory of *.nas
# files and/or subdirectories.
# @param symbols A list of symbols, as per _require_symbols(),
# to require the module to have once loaded or,
# in the case of a circular dependency, once
# recursively required again. An empty list just
# requires that the whole file is loaded before
# continuing. Nil says "I don't care".
var require = func(module, symbols=nil) {
resource = find_resource(module_path);
if (resource.getValue("loaded")) {
if (symbols == nil) return;
var namespace = globals[var ns = resource.getValue("namespace")];
var sym = _require_symbols(namespace, symbol);
if (sym != nil)
die(sprintf("symbolic dependency failed: symbol '%s' not"
" found in module '%s' once loaded", sym, ns));
return;
}
if (resource.getValue("load-requested")) {
if (symbols == nil) return;
var namespace = globals[var ns = resource.getValue("namespace")];
if (typeof(symbols) == 'scalar')
symbols = [symbols];
elsif (!size(symbols))
die("circular dependency failed: resource wasn't loaded");
var sym = _require_symbols(namespace, symbol);
if (sym != nil)
die(sprintf("circular dependency failed: symbol '%s' not"
" found in module '%s'", sym, ns));
return;
}
resource.setValue("load-requested", 1);
}
# Private; for each symbol in symbols, return it if it doesn't
# exist. Each symbol can be a scalar (first-level symbol) or
# a list of scalars (multi-level symbol, e.g. props.Node.getValues).
var _require_symbols = func(namespace, symbols) {
if (typeof(symbols) == 'scalar')
symbols = [symbols];
foreach (var sym; symbols) {
if (typeof(sym == 'scalar')
if (!contains(namespace, sym))
return sym;
else {
var current = namespace;
foreach (var subsym; sym)
if (!contains(current, subsym))
return sym~"."~subsym;
else current = current[subsym];
)
}
return nil;
}
var nasal_dirs = nil;
var make_resources = func() {
nasal_dirs = [ getprop("/sim/fg-root") ~ "/Nasal",
getprop("/sim/fg-home") ~ "/Nasal"
] ~ props.globals.getNode("sim").getChildren("fg-nasal");
var NasalNode = props.globals.getNode("nasal");
#NasalNode.removeChildren();
var load_listener = func(n) {
if (!n.getValue()) return;
var parent = n.getParent();
if (parent.getValue("loaded"))
parent.setValue("unload-requested", 1);
var name = parent.getName();
if (name == "file")
io.load_nasal(parent.getValue("absolute-path"),
parent.getValue("namespace"));
else {
if (name == "nasal")
# Load "standard" directories before extra files:
var resources = parent.getChildren("base-dir")
~parent.getChildren("file");
else
# Outside-in loading order otherwise:
var resources = parent.getChildren("file")
~parent.getChildren("dir");
foreach (var resource; resources)
resource.setValue("load-requested", 1);
}
parent.setValue("loaded", 1);
n.setValue(0);
}
var unload_listener = func(n) {
if (!n.getValue()) return;
var parent = n.getParent();
if (!parent.getValue("loaded"))
{ n.setValue(0); return }
parent.setValue("loaded", 0);
settimer(func n.setValue(0), 0); #next frame, after all listeners have run, FIXME
}
var get_abs_path = func(node) {
node.getParent().getValue("absolute-path")
~node.getValue("path");
}
var get_module_name = func(node) {
var current = node.getParent();
while (current.getParent().getName() != "base-dir")
current = current.getParent();
return current.getValue("path");
)
var init_module_part = func(node, path) {
node.setStringValue("path", path);
node.initNode("loaded", 0, "BOOL");
setlistener(
node.initNode("load-requested", 0, "BOOL"),
load_listener);
setlistener(
node.initNode("unload-requested", 0, "BOOL"),
unload_listener);
if (node.getName() != "base-dir" and node.getName() != "nasal")
node.setStringValue("absolute-path", get_abs_path(node));
if (node.getName() != "nasal")
node.setStringValue("namespace", get_module_name(node));
return node;
}
var make_basedir = func(path) {
if (path[0] != `/`)
die("paths must be absolute");
path = string.normpath(path);
var dir = init_module_path(NasalNode.addChild("base-dir"), path);
var subpaths = directory(path);
foreach (var subp; subpaths) {
make_child(dir, subp);
}
}
var make_child = func(node, path) {
var subpaths = directory(node.getValue("path") ~ path);
if (subpaths == nil) {
if (split(".", path)[-1] == "nas")
init_module_part(node.addChild("file"), path);
} elsif (size(subpaths)) {
var dir = init_module_part(node.addChild("dir"), path);
foreach (var subp; subpaths) {
make_child(dir, subp);
}
}
}
init_module_part(NasalNode);
foreach (var abspath; nasal_dirs)
make_base_dir(abspath);
}
# A few rules on top of ordinary resolving (see http://wiki.flightgear.org/
# Resolving_Paths). Tries prepending Nasal/ and appending .nas.
#
# It is an error in the filesystem to have both ${resolvable_path}
# and ${resolvable_path}.nas.
var resolvenasal = func(path) {
var p = path;
var try = resolvepath(path);
if (try != "") return try;
if (split("/", path)[0] != "Nasal") {
path = "Nasal/"~path;
var try = resolvepath(path);
if (try != "") return try;
}
if (split(".", path)[-1] != "nas") {
path = path~".nas";
var try = resolvepath(path);
return try;
}
}
var find_resource = func(path) {
var p = path;
path = resolvenasal(path);
if (path == "") die("nasal path could not be resolved: "~p);
var NasalNode = props.globals.getNode("nasal");
var startswith = func(a,b) substr(a,0,size(b)) == b;
var _find_resource = func(node) {
foreach (var resource; node.getChildren("dir")~node.getChildren("file")) {
if ((var abs = resource.getValue("absolute-path")) == path) return resource;
elsif (startswith(abs, path)) {
var res = _find_resource(resource);
if (res != nil) return res;
}
}
nil;
}
foreach (var basedir; NasalNode.getChildren("base-dir"))
if (startswith(basedir.getValue("path"), path)) {
var res = _find_resource(basedir);
if (res != nil) return res;
}
var res = _find_resource(NasalNode);
if (res != nil) return res;
printlog("info", "Creating new nasal resource for path "~p);
var resource = NasalNode.addChild("file");
resource.setStringValue("path", p);
resource.setStringValue("absolute-path", path);
resource.initNode("loaded", 0, "BOOL");
setlistener(
node.initNode("load-requested", 0, "BOOL"),
func(n) {
if (!n.getValue()) return;
var parent = n.getParent();
if (parent.getValue("loaded"))
parent.setValue("unload-requested", 1);
io.load_nasal(parent.getValue("absolute-path"),
parent.getValue("namespace"));
parent.setValue("loaded", 1);
n.setValue(0);
});
node.initNode("unload-requested", 0, "BOOL");
node.setStringValue("namespace", split(split(path, "/")[0], ".")[0]);
return resource;
}