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 — обёртка, сохраняющая результат в файл, который можно посмотреть в виде флеймграфа.