5.1.46. Cross-Context Services with apply_in_context
This tutorial covers [apply_in_context] — a macro annotation that
rewrites functions so their bodies execute in a named debug agent
context, while callers invoke them with normal function syntax.
Prerequisites: Debug Agents (Tutorial 45) for
fork_debug_agent_context and install_new_debug_agent.
options gen2
require daslib/apply_in_context
require debugapi
5.1.46.1. Setting up a named context
First, create a debug agent context to host shared state.
A plain DapiDebugAgent with no overrides is sufficient —
the agent exists solely to own a named context:
var counter : int = 0
def install_service(ctx : Context) {
install_new_debug_agent(new DapiDebugAgent(), "counter_service")
}
def init_counter_service() {
if (!has_debug_agent_context("counter_service")) {
fork_debug_agent_context(@@install_service)
}
}
5.1.46.2. The [apply_in_context] annotation
Functions annotated with [apply_in_context(agent_name)] have
their body rewritten to execute in the named agent context.
From the caller’s perspective, these look like normal functions:
[apply_in_context(counter_service)]
def increment() : int {
counter++
return counter
}
[apply_in_context(counter_service)]
def get_counter() : int {
return counter
}
[apply_in_context(counter_service)]
def add_to_counter(amount : int) {
counter += amount
}
init_counter_service()
print(" increment() = {increment()}\n")
print(" increment() = {increment()}\n")
print(" get_counter() = {get_counter()}\n")
add_to_counter(10)
print(" after add = {get_counter()}\n")
print(" local counter = {counter}\n")
// output:
// increment() = 1
// increment() = 2
// get_counter() = 2
// after add = 12
// local counter = 0
The agent context’s counter is modified — the caller’s local
copy stays at zero.
5.1.46.3. Argument constraints
Arguments that cross context boundaries must use types that
can be safely marshalled. Reference-type arguments must be
marked implicit:
string implicit— strings are reference types in daslangvar x : int& implicit— explicit reference parameters
Value types (int, float, bool) work without annotation:
[apply_in_context(counter_service)]
def set_counter_name(name : string implicit) {
print(" counter named '{name}', value = {counter}\n")
}
[apply_in_context(counter_service)]
def read_counter(var result : int& implicit) {
result = counter
}
set_counter_name("my_counter")
// output:
// counter named 'my_counter', value = 12
var val = 0
read_counter(val)
print(" read_counter() -> val = {val}\n")
// output:
// read_counter() -> val = 12
5.1.46.4. A cache service
A practical use case: a shared cache backed by a table that lives in the agent context. Any module or context can call put / get / has without worrying about which context they’re in:
var cache : table<string; int>
def install_cache(ctx : Context) {
install_new_debug_agent(new DapiDebugAgent(), "my_cache")
}
def init_cache_service() {
if (!has_debug_agent_context("my_cache")) {
fork_debug_agent_context(@@install_cache)
}
}
[apply_in_context(my_cache)]
def cache_put(key : string implicit; value : int) {
cache |> insert(key, value)
}
[apply_in_context(my_cache)]
def cache_get(key : string implicit) : int {
return cache?[key] ?? -1
}
[apply_in_context(my_cache)]
def cache_has(key : string implicit) : bool {
return key_exists(cache, key)
}
[apply_in_context(my_cache)]
def cache_size() : int {
return length(cache)
}
init_cache_service()
cache_put("width", 1920)
cache_put("height", 1080)
print(" cache size = {cache_size()}\n")
print(" width = {cache_get("width")}\n")
print(" local cache length = {length(cache)}\n")
// output:
// cache size = 2
// width = 1920
// local cache length = 0
5.1.46.5. How it works under the hood
The [apply_in_context] macro rewrites each function into
three parts:
Caller function (keeps original name) — verifies the agent exists, creates a result variable (if needed), then calls
invoke_in_contextto dispatch into the agent context.CONTEXT`func_name (runs in the agent context) — verifies it’s in the correct context, then calls the clone.
CONTEXT_CLONE`func_name (the original body) — contains the implementation code.
For a function with a return value, the expansion is similar to:
def get_counter() : int {
verify(has_debug_agent_context("counter_service"))
var __res__ : int
unsafe {
invoke_in_context(
get_debug_agent_context("counter_service"),
"counter_service`get_counter",
addr(__res__)
)
}
return __res__
}
The annotation adds [pinvoke] to the generated context
function automatically, enabling the context mutex required
for cross-context calls.
5.1.46.6. Quick reference
|
Rewrite function to execute in named agent context |
|
Required for string arguments crossing contexts |
|
Required for reference arguments crossing contexts |
|
Import the annotation macro |
|
Set up the named context (see Tutorial 45) |
See also
Full source: tutorials/language/46_apply_in_context.das
Debug Agents — creating and installing debug agents (Tutorial 45).
Compiling and Running Programs at Runtime — invoke_in_context basics
(Tutorial 44).
Contexts — language reference for context semantics.
Previous tutorial: Debug Agents
Next tutorial: Data Walking with DapiDataWalker