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