| name | boxlang-core-dev-runtime-architecture |
| description | Use this skill when understanding BoxLang internals: BoxRuntime services, IBoxContext hierarchy, scope chain resolution, DynamicObject, type system (IBoxType), parsing pipeline (source to bytecode), class loader isolation, virtual threads, or using --bx-printast for AST debugging. |
BoxLang Runtime Architecture
Overview
BoxLang is a dynamic JVM language (JRE 21+) that compiles source to Java bytecode
at runtime. The central singleton BoxRuntime acts as a service locator providing
access to all core services. Understanding the architecture is essential for building
modules, BIFs, interceptors, and custom language extensions.
BoxRuntime — The Central Singleton
import ortus.boxlang.runtime.BoxRuntime;
BoxRuntime runtime = BoxRuntime.getInstance();
BoxRuntime runtime = BoxRuntime.getInstance( true );
runtime.getInterceptorService()
runtime.getModuleService()
runtime.getCacheService()
runtime.getAsyncService()
runtime.getSchedulerService()
runtime.getDataSourceService()
runtime.getFunctionService()
runtime.getComponentService()
runtime.getClassLocator()
runtime.getConfiguration()
runtime.getTypeService()
Key Services
InterceptorService
Manages the global event bus. See interceptors skill for full details.
InterceptorService svc = runtime.getInterceptorService();
svc.announce( Key.of("myEvent"), dataStruct );
svc.register( myInterceptor );
svc.unregister( myInterceptor );
ModuleService
ModuleService moduleSvc = runtime.getModuleService();
boolean loaded = moduleSvc.hasModule( Key.of("bx-redis") );
ModuleRecord record = moduleSvc.getModuleRecord( Key.of("bx-redis") );
String version = record.version;
String physPath = record.physicalPath;
IStruct settings = record.settings;
Set<Key> modules = moduleSvc.getLoadedModules();
FunctionService (BIF Registry)
FunctionService fnSvc = runtime.getFunctionService();
fnSvc.registerGlobalFunction( Key.of("myBif"), new MyBIF() );
boolean exists = fnSvc.hasGlobalFunction( Key.of("len") );
BIF len = fnSvc.getGlobalFunction( Key.of("len") );
AsyncService
AsyncService asyncSvc = runtime.getAsyncService();
IBoxExecutor executor = asyncSvc.newExecutor( "myPool", ExecutorType.FIXED, 4 );
IBoxExecutor exec = asyncSvc.getExecutor( "boxlang-tasks" );
BoxFuture<Object> future = asyncSvc.newFuture( () -> expensiveWork() );
IBoxContext — Execution Context Hierarchy
Every BoxLang execution has a context. Contexts form a linked chain for scope resolution.
BoxRuntime
└── ScriptingRequestBoxContext (CLI / script execution)
└── FunctionBoxContext (inside a function call)
└── ClosureBoxContext (inside a closure)
└── LambdaBoxContext (inside a lambda)
└── WebRequestBoxContext (HTTP request)
└── ApplicationBoxContext (Application.bx scope)
└── SessionBoxContext (Session scope)
└── FunctionBoxContext
import ortus.boxlang.runtime.context.IBoxContext;
import ortus.boxlang.runtime.context.FunctionBoxContext;
import ortus.boxlang.runtime.context.ScriptingRequestBoxContext;
IBoxContext current = context;
while ( current != null ) {
System.out.println( current.getClass().getSimpleName() );
current = current.getParent();
}
IScope localScope = context.getScopeNearby( LocalScope.name );
IScope sessionScope = context.getScopeNearby( SessionScope.name );
ScopeSearchResult result = context.scopeFindNearby( Key.of("myVar"), null );
if ( result != null ) {
Object value = result.value();
IScope foundIn = result.scope();
}
Object result = context.invokeFunction( Key.of("len"), new Object[]{ "hello" } );
Scope Chain Resolution Order
When BoxLang resolves an unqualified variable name, it searches these scopes in order:
local — declared with var in current function
arguments — function parameters
variables — class/component private scope
- Named scopes:
url, form, cgi, cookie
request — per-request shared scope
session — user session
application — application-wide
server — server-wide
DynamicObject — BoxLang's Java Object Wrapper
DynamicObject wraps Java objects and provides BoxLang-style method invocation:
import ortus.boxlang.runtime.dynamic.DynamicObject;
DynamicObject wrapped = DynamicObject.of( new java.util.ArrayList() );
wrapped.invoke( context, "add", new Object[]{ "item" } );
Object size = wrapped.invoke( context, "size", new Object[]{} );
Object field = wrapped.getField( Key.of("fieldName") );
wrapped.setField( Key.of("fieldName"), value );
IBoxType — BoxLang Native Types
All BoxLang values implement IBoxType:
import ortus.boxlang.runtime.types.*;
import ortus.boxlang.runtime.types.IType;
BoxArray bxArray = new BoxArray();
BoxStruct bxStruct = BoxStruct.of("k","v");
BoxString bxStr = BoxString.of("hello");
BoxInteger bxInt = BoxInteger.of(42);
BoxDouble bxDbl = BoxDouble.of(3.14);
BoxBoolean bxBool = BoxBoolean.of(true);
BoxNull bxNull = BoxNull.INSTANCE;
if ( value instanceof BoxArray arr ) {
}
if ( value instanceof IStruct struct ) {
}
Key nameKey = Key.of("name");
Key idKey = Key.of("id");
Parsing Pipeline
Source code → AST → Java Bytecode:
Source (.bx / .bxs / .bxm / .cfm / .cfc)
│
▼
Lexer (ANTLR4) → Token stream
│
▼
Parser (ANTLR4) → Parse tree
│
▼
AST Builder → BoxLang AST (BoxNode tree)
│
▼
Transpiler/Transformer → Java AST (JavaParser)
│
▼
Java Compiler → Java bytecode (.class)
│
▼
ClassLoader → Loaded & executed by JVM
Debug the AST
boxlang --bx-printast myScript.bxs
echo 'var x = 1 + 2' | boxlang --bx-printast -
boxlang --bx-printast /path/to/file.bxs
Compiled Class Caching
Once compiled, the generated .class files are cached by the ClassLoader.
Re-compilation only happens when the source timestamp changes or when
reloadOnChange=true is configured.
ClassLoader Isolation
Each module gets its own isolated ClassLoader for the libs/ directory:
BoxLang Runtime ClassLoader
├── Core BIFs & Runtime Classes
├── Module A ClassLoader
│ └── libs/moduleA-dependency.jar
├── Module B ClassLoader
│ └── libs/moduleB-dependency.jar (no conflict with Module A)
└── Application ClassLoader
└── Application-specific JARs (this.javaSettings)
This prevents JAR version conflicts between modules — each module's libs/
directory is visible only to that module.
Key Java Packages
| Package | Contents |
|---|
ortus.boxlang.runtime | BoxRuntime, configuration |
ortus.boxlang.runtime.bifs | BIF base classes, annotations |
ortus.boxlang.runtime.context | Context classes, scope resolution |
ortus.boxlang.runtime.scopes | Scope implementations, Key |
ortus.boxlang.runtime.types | BoxArray, BoxStruct, IStruct, etc. |
ortus.boxlang.runtime.events | BaseInterceptor, InterceptorService |
ortus.boxlang.runtime.services | All service classes |
ortus.boxlang.runtime.modules | ModuleRecord, ModuleService |
ortus.boxlang.runtime.dynamic | DynamicObject, Java interop |
ortus.boxlang.runtime.loader | Class loading, bytecode generation |
ortus.boxlang.compiler.ast | AST node classes |
Virtual Threads and Project Loom
BoxLang uses JRE 21 virtual threads (Project Loom) for its default executor:
ExecutorService virtualExec = Executors.newVirtualThreadPerTaskExecutor();
Java CDS (Class Data Sharing)
BoxLang supports Java CDS archives for faster startup:
java -Xshare:dump -XX:SharedArchiveFile=boxlang.jsa -jar boxlang.jar
java -Xshare:on -XX:SharedArchiveFile=boxlang.jsa -jar boxlang.jar
Runtime Configuration Access
BoxLangConfiguration config = runtime.getConfiguration();
boolean debugMode = config.debugMode;
String timezone = config.timezone;
IStruct modules = config.modules;
IStruct caches = config.caches;
IStruct datasources = config.datasources;
IStruct moduleSettings = config.modules
.getAsStruct( Key.of("bx-redis") )
.getAsStruct( Key.of("settings") );
Embedding BoxLang in a Java Application
import ortus.boxlang.runtime.BoxRuntime;
import ortus.boxlang.runtime.context.ScriptingRequestBoxContext;
import ortus.boxlang.runtime.scopes.Key;
BoxRuntime runtime = BoxRuntime.getInstance( true );
ScriptingRequestBoxContext context = new ScriptingRequestBoxContext( runtime.getRuntimeContext() );
context.getScope( VariablesScope.name ).put( Key.of("greeting"), "Hello!" );
runtime.executeStatement( "println( greeting )", context );
runtime.executeTemplate( "/path/to/script.bxs", context );
runtime.shutdown();
Attempt — Null-Safe Value Wrapper
Attempt<T> is a fluent, immutable wrapper for values that may be null or invalid.
It is analogous to Java's Optional but with built-in BoxLang validation support.
Used extensively in BIFs and services, especially for optional argument handling.
import ortus.boxlang.runtime.dynamic.Attempt;
Attempt<String> attempt = Attempt.of( arguments.get( Key.of("name") ) );
Attempt<Integer> empty = Attempt.empty();
if ( attempt.isPresent() ) { ... }
if ( attempt.isEmpty() ) { ... }
String value = attempt.get();
String value = attempt.orElse( "default" );
String value = attempt.orElseGet( () -> computeDefault() );
Attempt.of( null ).orThrow( "mymodule.NotFound", "Value not found" );
Attempt<Integer> len = attempt.map( s -> s.length() );
Attempt<String> filtered = attempt.filter( s -> s.length() > 3 );
attempt.stream().forEach( v -> process(v) );
Attempt with Validation
Attempt.of( value ).toBeType( "email" ).isValid();
Attempt.of( 42 ).toBeBetween( 1.0, 100.0 ).isValid();
Attempt.of( "hello" ).toMatchRegex( "^[a-z]+$" ).isValid();
Attempt.of( "HELLO" ).toMatchRegex( "^[a-z]+$", false ).isValid();
Attempt.of( user ).toSatisfy( u -> u.isActive() ).isValid();
Using Attempt in BIF Argument Handling
@Override
public Object invoke( IBoxContext context, ArgumentsScope arguments ) {
String name = arguments.getAsAttempt( Key.of("name"), String.class ).orElse( "World" );
Integer timeout = arguments.getAsAttempt( Key.of("timeout"), Integer.class ).orElse( 30 );
return "Hello, " + name + "!";
}
Type Casters
BoxLang provides a rich set of type casters in ortus.boxlang.runtime.dynamic.casters.
Each returns a CastAttempt<T> (similar to Attempt) with .wasSuccessful() and .get().
import ortus.boxlang.runtime.dynamic.casters.*;
CastAttempt<String> strAttempt = StringCaster.attempt( value );
CastAttempt<Integer> intAttempt = IntegerCaster.attempt( value );
CastAttempt<Long> longAttempt = LongCaster.attempt( value );
CastAttempt<Double> dblAttempt = DoubleCaster.attempt( value );
CastAttempt<Boolean> boolAttempt = BooleanCaster.attempt( value );
if ( intAttempt.wasSuccessful() ) {
int n = intAttempt.get();
}
String str = StringCaster.cast( value );
Integer num = IntegerCaster.cast( value );
Boolean bool = BooleanCaster.cast( value );
Array arr = ArrayCaster.cast( value );
IStruct strc = StructCaster.cast( value );
IService — Creating Custom Runtime Services
Implement IService to create a service that's lifecycle-managed by the BoxLang runtime.
The runtime discovers and calls lifecycle methods automatically.
import ortus.boxlang.runtime.services.IService;
import ortus.boxlang.runtime.scopes.Key;
public class MyCustomService implements IService {
private static final Key SERVICE_NAME = Key.of( "MyCustomService" );
private BoxLangLogger logger;
@Override
public Key getName() {
return SERVICE_NAME;
}
@Override
public void onConfigurationLoad() {
this.logger = BoxRuntime.getInstance().getLoggingService().getLogger( "mycustomservice" );
logger.debug( "Configuration loaded" );
}
@Override
public void onStartup() {
logger.info( "MyCustomService started" );
}
@Override
public void onShutdown( Boolean force ) {
logger.info( "MyCustomService shutting down (force={})", force );
}
public String doSomething( String input ) {
return input.toUpperCase();
}
}
Registering a Service in a Module
BoxRuntime.getInstance().registerService( new MyCustomService() );
// ModuleConfig.bx
class {
function onLoad() {
var svc = createObject( "java", "com.example.services.MyCustomService" ).init()
boxRuntime.registerService( svc )
log.info( "MyCustomService registered" )
}
}
Accessing a Registered Service
MyCustomService svc = (MyCustomService) BoxRuntime.getInstance()
.getService( Key.of("MyCustomService") );
svc.doSomething( "hello" );
References