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.
  • onCleanup builtin: 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 onCleanup object by passing a function handle to onCleanup() inside a macro function.
  • The interpreter stores the handle in MacroFunctionDef::cleanupTasks, a stack managed by the current function scope.
  • At function exit, MacroFunctionDef::onCleanup iterates the stack and invokes each handle's cleanup method, which executes your callback and marks the task as cancelled to prevent double execution.
  • Use clear obj to force immediate cleanup or obj.cancel() to suppress the cleanup action entirely.
  • The mechanism requires execution inside a macro function; calling onCleanup at 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →