In this document we describe how the weevil
adapter uses the dapper library to
- correctly read the incoming request messages
- construct the correct response and any event messages
- make sure the correct actions are then performed in the debugger & debuggee
The Debug Adapter Protocol (henceforth DAP) specifies certain invariants
for the messages passed back and forth between a front-end client and a DAP service. The two main ones are
-
all incoming requests from the client have a corresponding response message from the DAP service (or a general error message response if something went wrong). For example the
threads
request should be answered with athreads
response etc. -
all messages have a sequence number
seq
that is a unique identifier for that one message. This uniqueness is per actor (so the stream ofseq
numbers from the client have to be unique within the client and the stream generated by the DAP service have to be unique within the DAP service). Each stream starts atseq=1
and each subsequent new message from that actor increments the previously usedseq
number for that same actor.
In addition all messages have to be passed as JSON strings with a particular (simple) structure.
As described in the main README.md one can leverage the OCaml typechecker to make sure that the correct response type is always paired with its related request type. One can also make use of OCaml's functor machinary to ensure that the seq
calculations are always taken care of and that incoming/outgoing messages are deserialised/serialised (resp.) properly. Details of these approaches are given below.
The main logic for stating what the DAP service should do when it receives each request message are implemented as handlers
(* output sig for linking an input to an output e.g. request -> response *)
module type LINK_T = sig
type in_t
type out_t
type state
val make :
handler:(state:state -> in_t -> out_t Dap_result.t) ->
(state:state -> string -> (string, string) Lwt_result.t)
end
The in_t
and out_t
types are the incoming and outgoing message types, for example:
in_t
would be something like theNextRequest
part of the mainRequest GADT
,
| NextRequest :
(Dap_commands.next, NextArguments.t, Presence.req) RequestMessage.t ->
(Dap_commands.next, NextArguments.t, Presence.req) RequestMessage.t Request.t
out_t
would then be the equivalentNextResponse
part of theResponse GADT
,
| NextResponse :
(Dap_commands.next, EmptyObject.t option, Presence.opt) ResponseMessage.t ->
(Dap_commands.next, EmptyObject.t option, Presence.opt) ResponseMessage.t Response.t
The dapper
library provides a few different implementations of this LINK_T
signature:
-
Request_response - linking a response message to a request message both of which have the same
command
enum and with the correctseq
number (as shown in thenext
example above), -
Raise_event & Raise_error - returning an event message or error reponse message resp. with the correct
seq
number. In these implementations the incoming type is justunit
.
You will notice that the functors that provide these implementations also take care of the string handling, both deserialising incoming JSON strings to a well typed in_t
instance and also serialising the outgoing out_t
to a JSON string.
This all means that in the adapter
library one only has to worry about implementing an appropriate handler
function for each message you want to send back:
handler:(state:state -> in_t -> out_t Dap_result.t)
In particular, at this point one does not have to worry about seq
numbers or string handling. All considerations are from within the well-typed world of OCaml and what state
changes you wish to make.
To continue the next
example shown above please consider the full next
adapter implementation. The DAP specifies that on receipt of a next request the backend should move the debugger forward one step, respond with the next response
and also raise the stopped event. The code is detailed below:
(* module imports not shown *)
module T (S : Types.STATE_READONLY_T) = struct
module On_request = Dap.Next.On_request (S)
module On_stopped = Dap.Next.Raise_stopped (S)
Each adapter
piece is implemented as a functor T
that takes some form of state
(see below) - this is a form of dependency injection to allow for easier unit testing.
Then the two modules corresponding to the next response
and stopped event
are pulled in.
At this point the types for these two modules are fully known and all that remains is to make handlers
for each one:
let next_handler =
On_request.make ~handler:(fun ~state req ->
(* Note that req is a fully realised type here *) ...........................(1)
(* use state and req ...*)
match S.backend_oc state with
| Some oc ->
...
...
(* the only thing the typechecker will let us return is a Next response *)...(2)
let resp =
let command = Dap.Commands.next in
let body = D.EmptyObject.make () in
Dap.Response.default_response_opt command body
in
let ret = Dap.Response.nextResponse resp in
Dap_result.ok ret
(* or an error *)............................................................(3)
| None -> Dap_result.from_error_string "Cannot connect to backend"
)
Notes
-
Here
req
has been specialised to(Dap_commands.next, NextArguments.t, Presence.req) RequestMessage.t Request.t
The well-typed
NextArguments.t
value can be accessed withRequest GADT
machinery like extract -
The underlying functor type constraints ensure that the only thing that can be returned is a
(Dap_commands.next, EmptyObject.t option, Presence.opt) ResponseMessage.t Response.t
-
The
dapper
library also provides a specialisation of the standard OCamlresult
type where theerror
part is given by(Dap_commands.error, ErrorResponse_body.t, Presence.req) ResponseMessage.t Response.t
In all cases, including the error path, the underlying functor machinery also takes care of calculating the correct seq
number for each message and also for JSON string handling.
The stopped
event handler has a similar structure although simpler because it just needs to construct a stopped
event:
let stopped_handler =
On_stopped.make ~handler:(fun ~state:_ _ ->
let ev =
let event = Dap.Events.stopped in
let reason = D.StoppedEvent_body_reason.Step in
let body =
D.StoppedEvent_body.make
~reason
~threadId:Defaults.Vals._THE_THREAD_ID
~preserveFocusHint:true
~allThreadsStopped:true
()
in
Ev.default_event_req event body
in
let ret = Ev.stoppedEvent ev in
Dap_result.ok ret)
As before there is no need to worry about seq
numbers or JSON string handling and the functor type constraints for this dapper
module mean that only values of the
(Dap_events.stopped, StoppedEvent_body.t, Presence.req) EventMessage.t Event.t
type can be returned.
After defining these two handlers we need to 'register' them for this next
adapter action by defining a function handlers
:
let handlers ~state = [
next_handler ~state;
stopped_handler ~state;
]
It is important to note that the adapter machinery will process these handlers in the order that you specify. So in this case the next
request handler will be processed first, followed by the stopped
handler. If the first handler errors then the subsequent handler is ignored.
The last part is the state cleanup:
let on_success ~state:_ = ()
let on_error ~state:_ = ()
end
Each of these adapter modules allow you the opportunity to modify the state
for either the success path or the error path. See below for more details.
TODO