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 daslang

  • var 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:

  1. Caller function (keeps original name) — verifies the agent exists, creates a result variable (if needed), then calls invoke_in_context to dispatch into the agent context.

  2. CONTEXT`func_name (runs in the agent context) — verifies it’s in the correct context, then calls the clone.

  3. 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

[apply_in_context(name)]

Rewrite function to execute in named agent context

string implicit

Required for string arguments crossing contexts

var x : T& implicit

Required for reference arguments crossing contexts

require daslib/apply_in_context

Import the annotation macro

fork_debug_agent_context + install_new_...

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 Runtimeinvoke_in_context basics (Tutorial 44).

Contexts — language reference for context semantics.

Previous tutorial: Debug Agents

Next tutorial: Data Walking with DapiDataWalker