Looking to assess feasibility (not desirability): • When code is loaded into a namespace, associate that namespace with a "code source" (a jar, usually) • Making it a hard error to alter or add vars within a namespace in code defined in a different code source • Have hidden namespaces whose vars are not directly accessible by namespaces outside of the same code source I am thinking about library authors, but also some static checks that are being done at work. I am wondering how practical it is to make these checks dynamic as well. (This definitely rhymes with the Java module system in concept, except instead of modules containing packages we have <unnamed> containing namespaces.)
Your best bet will be building your own module system, with your own ns type macro that does all that.
Note for anyone who wants to do crazy stuff like this - StackWalker is a fun class.
The current thought I have is two modes: One dynamic where when developing module errors are done on top of the classpath. This would be ideal for a REPL session. Enforcement is there, but bypassable with some effort
Another static, where we compile module.edn files to module-info.class files that we additionally mark with a @CljMod annotation.
{:name "example"
:export {:namespaces "example.core"}
:requires ["some.library"]
;; Any namespaced keys get put in the meta part of the static annotation
:custom/build-sha "..."
:custom/maintainer "Demo Team"}
@CljMod(
exportedNamespaces = {
@NS(name="example.core")
},
meta = """
{:custom/build-sha "..."
:custom/maintainer "Demo Team"}
"""
)
module example {
// This one is implicit
requires clojure;
requires some.library;
}and in this mode - with the module-info.class files in actual jars on the filesystem - we can get rid of any loopholes
{:name clojure
:requires [{:module clojure.spec}]
:export {:namespaces [clojure.core]
:packages [clojure.asm
clojure.java.api
clojure.lang
clojure]}}
private static Module getCallerModule() {
var sw = StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE);
var callerStackFrame = sw.walk(stream -> stream
// We want to skip all stack frames from clojure
.dropWhile(sf -> Modules.class.getModule().equals(
sf
.getDeclaringClass()
.getModule()
))
.findFirst()
);
return callerStackFrame.map(StackWalker.StackFrame::getDeclaringClass)
.map(Class::getModule)
.orElse(null);
}
static void validateCallerWritePermission(Namespace ns) {
if (!RT.moduleMode) { return; }
var callingModule = getCallerModule();
var calleeModule = ns.module;
if (!callingModule.equals(calleeModule)) {
throw new InaccessibleObjectException(
callingModule
+ " cannot write vars in "
+ calleeModule + "/" + ns
);
}
}(in-module demo)
(ns secure-ns)
(defn- private-inc
[x]
(inc x))
(defn example
[x]
(private-inc x))
(defn abc
[]
#'example)
Posted a code snippet and forgot to explain