Sprinter: добавить отладку C-исходников и интеграцию VS Code

This commit is contained in:
2026-09-15 17:58:41 +03:00
parent 50c6e56b7b
commit e4695b8281
62 changed files with 7147 additions and 27 deletions
+242
View File
@@ -0,0 +1,242 @@
-- license:BSD-3-Clause
-- Изолированный backend sdbg. Один каталог/процесс-владелец на сессию.
-- Не загружать одновременно с другим мостом управления тем же CPU.
local exports = { name="sdbgbridge", version="0.1.0", description="Sprinter source debugger",
license="BSD-3-Clause", author={name="Sprinter C-Compiler contributors"} }
function exports.startplugin()
local json = require("json")
local lfs = require("lfs")
local directory = assert(os.getenv("SDBG_IPC_DIR"), "Нужен SDBG_IPC_DIR")
local session = assert(os.getenv("SDBG_SESSION_ID"), "Нужен SDBG_SESSION_ID")
local generation, sequence = 0, 0
local events, owned = {}, {}
local state, pending = "initializing", nil
local invalidated=false
local session_started=false
local registers = {"PC","SP","AF","BC","DE","HL","IX","IY","AF2","BC2","DE2","HL2",
"I","R","IM","IFF1","IFF2","PG0","PG1","PG2","PG3","CNF","7FFD","1FFD"}
local function machine() return manager.machine end
local function cpu() return machine().devices[":maincpu"] end
local function now()
local t=machine().time
return t.seconds+t.attoseconds/1e18
end
local function event(kind, body)
sequence=sequence+1
events[#events+1]={seq=sequence,kind=kind,generation=generation,body=body or {}}
if #events>512 then table.remove(events,1) end
end
local function atomic(path, value)
local file=assert(io.open(path..".tmp","wb"))
file:write(json.stringify(value));file:close()
assert(os.rename(path..".tmp",path))
end
local function update()
if invalidated then return end
local observed=machine().debugger.execution_state=="stop" and "stopped" or "running"
-- step() исполняется лишь после возврата periodic callback.
if pending and pending.kind:match("^step") and now()<=pending.time then return end
if observed~=state or (pending and observed=="stopped") then
state=observed
generation=generation+1
local body={pc=cpu().state.PC.value}
if pending then body.reason=pending.kind end
event(state,body)
if state=="stopped" then pending=nil end
end
end
local function stopped(request)
assert(state=="stopped" and not pending,"CPU не остановлен")
assert(request.generation==generation,"Устаревшая generation")
end
local function number(value, limit)
assert(type(value)=="number" and value%1==0 and value>=0 and value<=limit,"Недопустимое число")
return value
end
local invalidate
local function dispatch(request)
assert(request.session==session,"Чужая сессия")
local args=request.args or {}
local command=request.command
if invalidated and command~="hello" and command~="snapshot" and command~="events" then
error("Сессия инвалидирована reset/load")
end
if command=="hello" then
session_started=true
return {protocol=1,session=session,capabilities={snapshot=true,memory=true,
instruction_step=true,step_over=true,step_out=true,
bank_guard=true,deferred_breakpoints=true,console_print=true},state=state}
elseif command=="snapshot" then
local result={state=state,time=now(),paused=machine().paused}
result.keyboards=setmetatable({}, {__jsontype="object"})
for tag,kbd in pairs(machine().natkeyboard.keyboards) do
result.keyboards[tag]=kbd.enabled
end
if state=="stopped" and not pending then
result.registers=setmetatable({}, {__jsontype="object"})
for _,name in ipairs(registers) do
if cpu().state[name] then result.registers[name]=cpu().state[name].value end
end
end
return result
elseif command=="key" then
-- Этот путь нужен для воспроизводимых UI-тестов; обычная клавиатура
-- хоста остаётся у MAME и не проходит через bridge.
assert(state=="running" or (state=="stopped" and args.down==false),
"Нажатие принимается при running CPU, отпускание также при stopped")
local tag=args.tag
assert(type(tag)=="string" and tag:match("^:kbd:ms_naturl:P%d+%.%d+$"),
"Разрешены только порты PC-клавиатуры Sprinter")
local mask=number(args.mask,0xffff)
assert(type(args.down)=="boolean","Нужен флаг down")
local port=machine().ioport.ports[tag]
assert(port,"Порт клавиатуры не найден")
for _,field in pairs(port.fields) do
if field.mask==mask then
field:set_value(args.down and 1 or 0)
return {accepted=true,tag=tag,mask=mask,down=args.down}
end
end
error("Поле клавиатуры не найдено")
elseif command=="events" then
local result={events={},last=sequence,first=events[1] and events[1].seq or sequence+1}
for _,item in ipairs(events) do
if item.seq>(args.after or 0) then result.events[#result.events+1]=item end
end
return result
elseif command=="console_tail" then
local log=machine().debugger.consolelog
local total=#log
local count=number(args.count or 40,200)
local lines={}
for index=math.max(1,total-count+1),total do
lines[#lines+1]=log[index]
end
return {total=total,lines=lines}
elseif command=="pause" then
if state~="stopped" then
pending={kind="pause",time=now()}
machine().debugger.execution_state="stop"
end
return {accepted=true}
elseif command=="continue" or command=="step" or command=="step_over" or
command=="step_out" then
stopped(request)
generation=generation+1
state="running"
event("running",{reason=command})
if command=="step" then
pending={kind="step",time=now()}
cpu().debug:step(1)
elseif command=="step_over" then
pending={kind="step_over",time=now()}
machine().debugger:command("over 1")
elseif command=="step_out" then
pending={kind="step_out",time=now()}
machine().debugger:command("out")
else machine().debugger.execution_state="run" end
return {accepted=true}
elseif command=="memory" then
stopped(request)
local address=number(args.address,0xffff)
assert(args.enabled==nil or type(args.enabled)=="boolean","Неверный enabled")
local length=number(args.length,4096)
assert(address+length<=0x10000,"Чтение за пределами logical memory")
local symbols=emu.symbol_table(cpu())
local bytes={}
for offset=0,length-1 do
-- Этот интерфейс отключает side effects в отличие от space:read_u8.
bytes[#bytes+1]=string.format("%02x",symbols:memory_value(":maincpu","p",address+offset,1,true))
end
return {hex=table.concat(bytes)}
elseif command=="console_print" then
stopped(request)
local text=args.text
assert(type(text)=="string" and #text>=1 and #text<=2048,
"Нужна строка журнала до 2048 байт")
-- В debugger printf уже отформатированный текст — только данные.
-- Убираем управляющие символы и экранируем синтаксис команды/формата.
local safe=text:gsub("[%c]"," "):gsub("\\","\\\\")
:gsub("%%","%%%%"):gsub('"',"'")
machine().debugger:command('printf "'..safe..'"')
return {printed=true}
elseif command=="breakpoint" then
stopped(request)
local address=number(args.address,0xffff)
local condition=""
if args.window~=nil or args.page~=nil then
local window=number(args.window,3)
local page=number(args.page,255)
local ports={[0]=0x82,[1]=0xa2,[2]=0xc2,[3]=0xe2}
-- Дополнительные PG state entries драйвера Sprinter не входят в
-- expression table CPU. Читаем штатный page-port без side effects;
-- числа debugger expression по умолчанию шестнадцатеричные.
condition=string.format("ib@%x==%x",ports[window],page)
end
local id=cpu().debug:bpset(address,condition,"")
owned[id]={address=address,condition=condition}
if args.enabled==false then cpu().debug:bpdisable(id) end
return {id=id,condition=condition,enabled=args.enabled~=false}
elseif command=="activate_breakpoints" then
stopped(request)
local count=0
for id in pairs(owned) do
if cpu().debug:bpenable(id) then count=count+1 end
end
return {enabled=count}
elseif command=="deactivate_breakpoints" then
stopped(request)
local count=0
for id in pairs(owned) do
if cpu().debug:bpdisable(id) then count=count+1 end
end
return {disabled=count}
elseif command=="clear" then
stopped(request)
local id=number(args.id,0x7fffffff)
assert(owned[id],"Точка не принадлежит sdbg")
cpu().debug:bpclear(id);owned[id]=nil
return {cleared=id}
else error("Неподдержанная команда: "..tostring(command)) end
end
invalidate=function(reason)
if invalidated then return end
if not session_started then return end
if reason=="state_load" then
for id in pairs(owned) do cpu().debug:bpclear(id) end
end
owned={}
invalidated=true
generation=generation+1
pending=nil
state="invalidated"
event("invalidated",{reason=reason})
end
-- Subscription-объекты надо удерживать: иначе Lua GC снимет callback.
exports._subscriptions={
emu.add_machine_reset_notifier(function() invalidate("reset") end),
emu.add_machine_post_load_notifier(function() invalidate("state_load") end)
}
emu.register_periodic(function()
if not machine() or not machine().debugger then return end
update()
for name in lfs.dir(directory) do
local id=name:match("^req_(%d+)%.json$")
if id then
local path=directory.."/"..name
local file=io.open(path,"rb")
if file then
local data=file:read("*a");file:close()
local ok,request=pcall(json.parse,data)
local result
if ok and type(request)=="table" then ok,result=pcall(dispatch,request)
else result="Неверный JSON";ok=false end
atomic(directory.."/resp_"..id..".json",{ok=ok,generation=generation,
result=ok and result or nil,error=not ok and tostring(result) or nil,session=session})
os.remove(path)
end
end
end
end)
end
return exports