Spiiin's blog

daScript - контексты

В заметке про live-режим для opengl-приложений в daScript я кратко описывал контексты (раздел Архитектура приложения). Здесь разберу их немного более подробно.

Контекст

Программы daScript выполняются в контексте, структуре, которая хранит окружение программы — выделенную память, в которой хранятся код и данные программы, загруженные модули, настройки виртуальной машины.

tutorual06 показывает работу с контекстом в daScript:

//загружаем das-файл и компилируем его
auto program = compileDaScript("dummy.das", fAccess, tout, dummyLibGroup);
//создаём контекст, в котором будет выполняться программа
ContextPtr ctx = make_shared<Context>(program->getContextStackSize());
//симулируем выполнение программу (строит дерево симуляции для выполнения в виртуальной машине)
program->simulate(*ctx, tout);
//находим в контексте скомпилированную функцию
SimFunction *fni = ctx->findFunction("test");
//тут могут быть опциональные стадии проверки сигнатуры функции
verifyCall<float>(fni->debugInfo, dummyLibGroup);
//выполняем функцию в контексте, тип результата -- 128-бит
vec4f res = ctx->evalWithCatch(fni, nullptr);
//приводим результат к C++-типу
float result = cast<float>::to(res);

(Устройство интерпретаторов lua-jit и daScript — немного более детально про симуляцию программы)

Создание и удаление контекстов в daScript дешёво, один из паттернов организации C++-приложения, использующего daScript — выделение нового контекста, выполнение в нём работы и уничтожение.

Контекст можно воспринимать как экземпляр выполняющейся программы. Программа на С++ может иметь несколько контекстов, каждый из которых может выполнять программу на daScript (как скомпилированную из различных das-файлов, так и из одного и того же).

Создание контекста из daScript

API для создания контекстов также доступно в самом daScript, так что скрипт сам может создавать новые контексты и выполнять в них программы.

Пример eval_in_context:

//создание текста программы
let text = build_string <| $(st)
    st |> write("[export]\n")
    st |> write("def eval(var res:int?)\n")
    st |> write("\tunsafe\n")
    st |> write("\t\t*res = 42\n")
    st |> write("\n")
//def eval(var res:int?)
//  unsafe
//      *res = 42

access |> set_file_source("__dummy_file_name", text)
  using <| $(var mg:ModuleGroup)
    using <| $(var cop:CodeOfPolicies)
      //компилируем строку с текстом программы
      compile_file("__dummy_file_name",access,unsafe(addr(mg)), cop) <| $(ok,program,errors)
        //строим дерево симуляции
        simulate(program) <| $ (sok; context; serrors )
          unsafe
            var res:int
            //выполняем функцию eval в контексте скомпилированной программы
            context |> invoke_in_context("eval", addr(res))
            print("{res}\n")

В примере выше в скомплированные программу передаётся адрес из другого контекста. Это небезопасно, так как один контекст ничего не знает про время жизни переменных другом, поэтому операция отмечена как unsafe.

Форк контекста

Кроме возможности иметь раздельные контексты, daScript позволяет склонировать существующий контекст — fork_debug_agent_context. Функция создаёт клон контекста, и выставляет флаг контекста persistent, а затем в этом клонированном контексте выполняет функцию инициализации, переданную аргументом. Обычно в этой функции регистрируется новый DebugAgent — обёртка над контекстом, которая позволяет найти указатель на склонированный контекст по имени (и опционально, изменить поведение с помощью хуков). Склонированный контекст имеет доступ к тому же окружению, что и основной (модули, функции, копии переменных).

Склонированный контекст не копирует состояние из основного!

Пример agent_fork_sample.das

require debugapi

var test_value = "default"

[export] def debug_context_set()
    print("{this_context().name}:{test_value}\n")
    test_value = "debug_context" //изменить значение переменной в контексте debug_context
    print("{this_context().name}:{test_value}\n")

[export]
def main
    this_context().name := "my_context"
    fork_debug_agent_context <| @@(var new_context : Context)
        //вызывается в новом контексте, устанавливаем DebugAgent, который сохранит ссылку на новый контекст
        install_new_debug_agent(new DapiDebugAgent(), "debug_context")

    print("{this_context().name}:{test_value}\n")
    //меняем значение глобальной переменной
    test_value = "my_context"

    print("{this_context().name}:{test_value}\n")
    //ждём создания контекста агента
    while !has_debug_agent_context("debug_context")
        pass
    unsafe
        //изменяем значение переменной в контексте debug_context
        get_debug_agent_context("debug_context") |> invoke_in_context("debug_context_set")
   
    print("{this_context().name}:{test_value}\n")

Выведет:

my_context:default                            // в основном контексте test_value имеет значение по умолчанию
my_context:my_context // изменили значение в основном контексте
debug agent debug_context:default // <--в склонированном контексте test_value имеет значение по умолчанию
debug agent debug_context:debug_context // изменили значение в склонированном контексте
my_context:my_context // в основном контексте значение test_value не изменилось

Аннотация для функций apply_in_context позволяет вызывать функцию в другом контексте прозрачно для вызывающего кода. Пример выше можно переписать так:

require daslib/apply_in_context

[apply_in_context(debug_context)]
def debug_context_set()
    print("{this_context().name}:{test_value}\n")
    test_value = "debug_context" //изменить значение переменной в контексте debug_context
    print("{this_context().name}:{test_value}\n")

...//тот же код создания агента

while !has_debug_agent_context("debug_context")
    pass
debug_context_set() //функция будет вызвана в контексте debug_context

Пример создания отдельного контекста — модуль live, пример клонирования — opengl_cache.

Переопределение поведения debug-агентов

Можно вызвать функции, определённые в классе-наследнике debug-агента (invoke_debug_agent_function), но намного более интересной возможностью является переопределения методов. Виртуальная машина daScript отслеживает наличие зарегистрированных отладочных агентов и передаёт им информацию в ходе выполнения программы — интерфейс DapiDebugAgent.

Простые примеры определения кастомных debug-агентов:
logger_agent — добавление кастомного префикса при логгировании
insturment_function — профилирование вызовов функций.
instrument — программный брейкпоинт и пошаговая трассировка

options debugger = true    //настройка контекста для работы с хуками debug-агентов
require debugapi           //интерфейсы для кастомных debug-агентов
require rtti               //получение информации о контексте выполнения (доступные модули/функции/переменные/etc)

//программный брейкпоинт, вызывает хук onBreakpoint в debug-агенте
breakpoint
//устанавливает номер строчки, для которой будет вызван хук onInstrument
instrument_node
//устанавливает режим трассирования, хук onSingleStep будет вызван для каждой строчки выполнения до отключения
set_single_step
//устанавливает фильтр на функции, для которых будет вызван хук onInstrumentFunction
instrument_all_functions
//вызывает хук onLog
to_log
//вызывает хук onCollect
collect_debug_agent_state
//вызывает хук onVariable у установленного DapiStackWalker
report_context_state

Более сложные примеры:
stackwalk — более продвинутая версия instrument, устанавливает кастомный DapiStackWalker и DapiDataWalker, печатает значение локальных переменных на каждом шаге
context_state_example — еще немного более полная версия информации о переменных, уже более похожая на информацию для отладчика
opengl_state — в хуке onCollect печатает информацию о состояниях OpenGL
decs_state — собирает информацию из модуля decs (entity-component-system)
ast_debug — информация о состоянии expression tree, для отладки макросов

Инструмнты основе debug-агентов

Полноценный отладчикdebug (debug-агент + сервер для связи с IDE) (плагин для vscode):
(необходимо определить #define DAS_DEBUGGER 1 если это по каким-то причинам не определилось в das_config.h автоматически)

Более продвинутый пример instrument_function — обёртка, сохраняющая результат в файл, который можно посмотреть в виде флеймграфа.