5.1.45. Debug Agents
This tutorial covers debug agents — persistent objects that live in their own separate context and can intercept runtime events, collect state, and host shared data accessible from any context.
Prerequisites: Compiling and Running Programs at Runtime (Tutorial 44) for
invoke_in_context basics.
options gen2
require debugapi
require rtti
5.1.45.1. Creating and installing a debug agent
A debug agent is a class that extends DapiDebugAgent. The
fundamental pattern is:
Define a class that extends
DapiDebugAgentWrite a setup function that creates the agent and installs it under a name with
install_new_debug_agentCall
fork_debug_agent_context(@@setup)to clone the current context and run the setup function in it
The agent lives in its own “agent context” — a separate copy of the program that stays resident:
class CounterAgent : DapiDebugAgent {
count : int = 0
}
def install_counter(ctx : Context) {
install_new_debug_agent(new CounterAgent(), "counter")
}
def demo_create_agent() {
print(" has 'counter' = {has_debug_agent_context("counter")}\n")
fork_debug_agent_context(@@install_counter)
print(" has 'counter' = {has_debug_agent_context("counter")}\n")
// output:
// has 'counter' = false
// has 'counter' = true
5.1.45.2. Intercepting log output with onLog
DapiDebugAgent has an onLog method called whenever any
context prints or logs. If onLog returns true, the default
output to stdout is suppressed. If it returns false, output
proceeds normally.
This is how profiling tools, IDE log panels, and custom loggers intercept program output:
var log_intercept_count : int = 0
class LogAgent : DapiDebugAgent {
def override onLog(context : Context?; at : LineInfo const?;
level : int; text : string#) : bool {
log_intercept_count++
return false // don't suppress — let output reach stdout
}
}
def install_log_agent(ctx : Context) {
install_new_debug_agent(new LogAgent(), "log_watcher")
}
[export, pinvoke]
def read_log_count(var result : int?) {
unsafe {
*result = log_intercept_count
}
}
// After forking + installing, each print/to_log triggers onLog
fork_debug_agent_context(@@install_log_agent)
print(" hello through agent\n")
to_log(LOG_INFO, " info message\n")
var count = 0
unsafe {
invoke_in_context(get_debug_agent_context("log_watcher"),
"read_log_count", addr(count))
}
print(" log_intercept_count >= 2: {count >= 2}\n")
// output:
// hello through agent
// info message
// log_intercept_count >= 2: true
5.1.45.3. Calling functions in the agent context
Use invoke_in_context to call [export, pinvoke] functions
in the agent context. get_debug_agent_context(name) returns the
agent’s Context. Functions run in that context, so they see the
agent’s copy of module-level variables — not the caller’s.
The [pinvoke] annotation is required — it enables the context
mutex needed for cross-context invocation.
To return values, pass a pointer to a result variable:
var agent_counter : int = 0
[export, pinvoke]
def agent_increment() {
agent_counter++
}
[export, pinvoke]
def agent_get(var result : int?) {
unsafe {
*result = agent_counter
}
}
def demo_invoke_in_context() {
unsafe {
invoke_in_context(get_debug_agent_context("counter"), "agent_increment")
invoke_in_context(get_debug_agent_context("counter"), "agent_increment")
invoke_in_context(get_debug_agent_context("counter"), "agent_increment")
}
var result = 0
unsafe {
invoke_in_context(get_debug_agent_context("counter"), "agent_get", addr(result))
}
print(" agent_counter (in agent) = {result}\n")
print(" agent_counter (local) = {agent_counter}\n")
// output:
// agent_counter (in agent) = 3
// agent_counter (local) = 0
5.1.45.4. Calling agent methods with invoke_debug_agent_method
invoke_debug_agent_method calls a method on the agent’s class
instance directly — no [export, pinvoke] helper functions needed.
The agent’s self is passed automatically.
Syntax: invoke_debug_agent_method("agent_name", "method", args...)
class CalcAgent : DapiDebugAgent {
accumulator : int = 0
def add(amount : int) {
self.accumulator += amount
}
def get_result(var result : int?) {
unsafe {
*result = self.accumulator
}
}
}
def install_calc_agent(ctx : Context) {
install_new_debug_agent(new CalcAgent(), "calc")
}
fork_debug_agent_context(@@install_calc_agent)
unsafe {
invoke_debug_agent_method("calc", "add", 10)
invoke_debug_agent_method("calc", "add", 20)
invoke_debug_agent_method("calc", "add", 12)
}
var result = 0
unsafe {
invoke_debug_agent_method("calc", "get_result", addr(result))
}
print(" accumulator = {result}\n")
// output:
// accumulator = 42
5.1.45.5. State collection — onCollect and onVariable
onCollect is called when collect_debug_agent_state is triggered.
The agent can report custom variables via report_context_state.
onVariable receives each reported variable — this is how IDE
debuggers show custom watch variables and application diagnostics:
class StateAgent : DapiDebugAgent {
collection_count : int = 0
def override onCollect(var ctx : Context; at : LineInfo) : void {
collection_count++
unsafe {
let tinfo = typeinfo rtti_typeinfo(collection_count)
report_context_state(ctx, "Diagnostics", "collection_count",
unsafe(addr(tinfo)), unsafe(addr(collection_count)))
}
def override onVariable(var ctx : Context; category, name : string;
info : TypeInfo; data : void?) : void {
unsafe {
let value = sprint_data(data, addr(info), print_flags.singleLine)
print(" {category}: {name} = {value}\n")
}
}
// Trigger collection
collect_debug_agent_state(this_context(), get_line_info(1))
// output:
// Diagnostics: collection_count = 1
5.1.45.6. Agent existence checks
has_debug_agent_context(name) checks if a named agent exists.
Always check before accessing the context to avoid panics:
print(" has 'counter' = {has_debug_agent_context("counter")}\n")
print(" has 'missing' = {has_debug_agent_context("missing")}\n")
// output:
// has 'counter' = true
// has 'missing' = false
5.1.45.7. Auto-start module pattern
In modules, agents are installed automatically via a [_macro]
function. Four guards ensure safe, single installation:
[_macro]
def private auto_start() {
if (is_compiling_macros_in_module("my_module") && !is_in_completion()) {
if (!is_in_debug_agent_creation()) {
if (!has_debug_agent_context("my_agent")) {
fork_debug_agent_context(@@my_agent_setup)
}
}
}
}
The guards prevent:
Running outside the module’s own compilation
Running during IDE code completion
Recursive agent creation
Duplicate installation
5.1.45.8. Plain agent as named context host
A common pattern is to create a plain DapiDebugAgent (no
overrides) just to own a named context. Module-level variables
in that context become shared state accessible via
invoke_in_context. This is the foundation of the
[apply_in_context] pattern (Tutorial 46):
var shared_data : int = 0
[export, pinvoke]
def add_data(amount : int) {
shared_data += amount
}
[export, pinvoke]
def get_data(var result : int?) {
unsafe {
*result = shared_data
}
}
def install_data_host(ctx : Context) {
install_new_debug_agent(new DapiDebugAgent(), "data_host")
}
// Multiple calls accumulate in the agent's copy
unsafe {
invoke_in_context(get_debug_agent_context("data_host"), "add_data", 10)
invoke_in_context(get_debug_agent_context("data_host"), "add_data", 20)
}
// output:
// shared_data (in agent) = 30
// shared_data (local) = 0
5.1.45.9. Shutting down a debug agent
delete_debug_agent_context removes an agent by name. It notifies all other
agents via onUninstall, then safely destroys the agent and its context:
// Remove the agent
delete_debug_agent_context("data_host")
print("has 'data_host' = {has_debug_agent_context("data_host")}\n")
// output: has 'data_host' = false
// Deleting a non-existent agent is a safe no-op
delete_debug_agent_context("data_host")
Use this when a profiling session ends, a debug tool is closed, or during test teardown to ensure agents do not leak across test files.
5.1.45.10. Quick reference
|
Clone context and call setup function in clone |
|
Register agent under a global name |
|
Check if named agent exists |
|
Get agent’s Context for invoke_in_context |
|
Remove agent by name (safe no-op if missing) |
|
Call [export, pinvoke] function in agent context |
|
Call a method on the agent’s class instance |
|
Trigger onCollect on all agents |
|
Report variable from onCollect to onVariable |
|
True during fork_debug_agent_context |
|
Log message — routed through onLog if agents exist |
|
Annotation enabling context mutex |
See also
Full source: tutorials/language/45_debug_agents.das
Cross-Context Services with apply_in_context — cross-context services via
[apply_in_context] annotation (Tutorial 46).
Compiling and Running Programs at Runtime — compiling and running programs at runtime (Tutorial 44).
Contexts — language reference for context semantics.
Previous tutorial: Compiling and Running Programs at Runtime
Next tutorial: Cross-Context Services with apply_in_context