How to Use the onCleanup Mechanism in Nelson for Reliable Resource Management
Use the onCleanup function in nelson-lang/nelson to register a callback that executes automatically when a function exits, ensuring resources like file handles are released even during errors.
The onCleanup mechanism provides deterministic resource cleanup for the Nelson numerical programming environment. Based on a handle class architecture similar to MATLAB's onCleanup, this feature guarantees that registered functions execute when the parent macro function terminates, regardless of whether the exit is normal or caused by an error.
What Is the onCleanup Mechanism?
The onCleanup mechanism implements a resource acquisition is initialization (RAII) pattern for Nelson scripts. When you create an onCleanup object inside a function, the interpreter stores a reference to your cleanup callback in the current macro function's internal task stack.
According to the nelson-lang/nelson source code, the mechanism relies on three core components:
OnCleanupObjectHandle: A C++ handle class that stores the user-provided function handle and manages execution state.MacroFunctionDef::cleanupTasks: A vector that maintains the stack of pending cleanup objects for the current function scope.onCleanupbuiltin: The gateway function that validates inputs and registers handles with the cleanup stack.
How onCleanup Works Under the Hood
The Builtin Registration Process
In modules/interpreter/builtin/cpp/onCleanupBuiltin.cpp, the builtin performs strict validation before creating the cleanup handle:
ArrayOfVector
Nelson::InterpreterGateway::onCleanupBuiltin(Evaluator* eval, int nLhs,
const ArrayOfVector& argIn)
{
nargincheck(argIn, 1, 1);
nargoutcheck(nLhs, 0, 1);
const ArrayOf& functionArray = argIn[0];
if (!functionArray.isFunctionHandle()) {
Error(ERROR_WRONG_ARGUMENT_1_TYPE_FUNCTION_HANDLE_EXPECTED);
}
auto* onCleanupObj = new OnCleanupObjectHandle(functionArray);
ArrayOf handleArray = ArrayOf::handleConstructor(onCleanupObj);
// Registration happens only inside macro functions
Context* ctx = eval->getContext();
FunctionDef* funcDef = nullptr;
const std::string& currentName = ctx->getCurrentScope()->getName();
if (ctx->lookupFunction(currentName, funcDef) &&
funcDef->type() == NLS_MACRO_FUNCTION) {
static_cast<MacroFunctionDef*>(funcDef)->addCleanupFunction(handleArray);
}
return ArrayOfVector{handleArray};
}
The builtin ensures that only function handles are accepted and that registration occurs only when inside a macro function.
The Cleanup Stack Architecture
The MacroFunctionDef class in modules/interpreter/src/include/MacroFunctionDef.hpp maintains the cleanup task stack:
void addCleanupFunction(ArrayOf& task) {
cleanupTasks.push_back(task);
}
ArrayOfVector cleanupTasks;
When a macro function terminates, the interpreter calls MacroFunctionDef::onCleanup() (defined in modules/interpreter/src/cpp/MacroFunctionDef.cpp), which iterates through the stack:
void MacroFunctionDef::onCleanup(Evaluator* eval)
{
for (auto task : cleanupTasks) {
if (task.isHandle() && task.getHandleClassName() == NLS_HANDLE_ONCLEANUP_CATEGORY_STR) {
OnCleanupObjectHandle* obj =
static_cast<OnCleanupObjectHandle*>(task.getContentAsHandleScalar());
if (obj && obj->isScoped()) {
obj->cleanup(eval);
}
}
}
cleanupTasks.clear();
}
Handle Execution and Cancellation
The OnCleanupObjectHandle class in modules/interpreter/src/cpp/OnCleanupObjectHandle.cpp executes the stored callback:
void OnCleanupObjectHandle::cleanup(Evaluator* eval)
{
if (isCanceled) {
return;
}
function_handle fh = task.getContentAsFunctionHandle();
FunctionDef* funcDef = nullptr;
if (fh.anonymousHandle != nullptr) {
funcDef = static_cast<FunctionDef*>(fh.anonymousHandle);
}
if (funcDef) {
funcDef->evaluateFunction(eval, ArrayOfVector(), 0);
}
cancel();
}
After execution, the cancel() method sets isCanceled = true, preventing double execution if the handle is somehow triggered again.
Practical Usage Examples
Basic File Resource Management
The most common use case ensures files are closed even if errors occur:
function processData(filename)
% Open resource
fid = fopen(filename, 'w');
% Register cleanup - executes when function exits
onCleanupObj = onCleanup(@() fclose(fid));
% Your processing logic here
fprintf(fid, 'Processing data...\n');
% If an error occurs here, fclose still runs
% Normal exit also triggers cleanup
end
When processData returns, the interpreter automatically calls fclose(fid), preventing resource leaks.
Forcing Early Cleanup
You can trigger cleanup immediately by clearing the handle:
function demoExplicit()
cleanup = onCleanup(@() disp('Cleanup executed now'));
disp('Doing work...');
clear cleanup; % Forces immediate execution
disp('Work finished');
end
Output:
Doing work...
Cleanup executed now
Work finished
Canceling Cleanup Tasks
To prevent a registered cleanup from running, call the cancel method:
function conditionalCleanup()
fid = fopen('temp.txt', 'w');
cleanupObj = onCleanup(@() fclose(fid));
% Some logic that might keep the file open
success = processFile(fid);
if success
% Keep file open, cancel auto-close
cleanupObj.cancel();
return; % fclose will NOT run
end
% If not successful, function exit triggers fclose
end
Key Source Files and Architecture
Understanding the implementation helps debug complex scenarios. The nelson-lang/nelson repository organizes the cleanup mechanism across these files:
| File Path | Component |
|---|---|
modules/interpreter/builtin/cpp/onCleanupBuiltin.cpp |
Builtin gateway that validates function handles and registers cleanup tasks |
modules/interpreter/src/include/OnCleanupObjectHandle.hpp |
Handle class declaration storing the callback |
modules/interpreter/src/cpp/OnCleanupObjectHandle.cpp |
Implementation of cleanup() and cancel() methods |
modules/interpreter/src/include/MacroFunctionDef.hpp |
Declaration of cleanupTasks vector and addCleanupFunction() |
modules/interpreter/src/cpp/MacroFunctionDef.cpp |
Execution loop that invokes cleanup tasks at function exit |
modules/interpreter/tests/onCleanup/demo_onCleanup.m |
Demonstration script showing usage patterns |
modules/interpreter/help/en_US/xml/onCleanup.xml |
Official documentation source |
Summary
- Create an
onCleanupobject by passing a function handle toonCleanup()inside a macro function. - The interpreter stores the handle in
MacroFunctionDef::cleanupTasks, a stack managed by the current function scope. - At function exit,
MacroFunctionDef::onCleanupiterates the stack and invokes each handle'scleanupmethod, which executes your callback and marks the task as cancelled to prevent double execution. - Use
clear objto force immediate cleanup orobj.cancel()to suppress the cleanup action entirely. - The mechanism requires execution inside a macro function; calling
onCleanupat the command line or in scripts not wrapped in functions will not register the task.
Frequently Asked Questions
What happens if I call onCleanup outside of a function?
If you call onCleanup at the command prompt or in a script that is not inside a function definition, the builtin cannot register the cleanup task. According to onCleanupBuiltin.cpp, registration only occurs when ctx->lookupFunction finds a NLS_MACRO_FUNCTION. The object is created but never added to cleanupTasks, so your callback will never execute.
Can I register multiple onCleanup objects in one function?
Yes. The cleanupTasks member in MacroFunctionDef is an ArrayOfVector that acts as a stack. Each call to onCleanup pushes a new handle onto this vector. When the function exits, MacroFunctionDef::onCleanup iterates through all registered tasks in the order they were added, ensuring every resource is properly released.
How do I prevent a cleanup task from running?
Call the cancel() method on the onCleanup object. In OnCleanupObjectHandle.cpp, this method sets the internal isCanceled flag to true. When the function scope ends, the cleanup method checks this flag and returns immediately without executing the callback. This is useful when you need to keep a resource open beyond the function scope.
Is onCleanup similar to Python's with statement or C++ RAII?
Conceptually yes, though the implementation differs. Like Python's with or C++ destructors, onCleanup guarantees execution of cleanup code when a scope exits. However, Nelson's implementation uses an explicit handle object registered with the interpreter's MacroFunctionDef cleanup stack rather than implicit destructor calls. The handle pattern gives you explicit control via cancel() and clear(), making it more flexible than strict RAII while providing the same safety guarantees.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →