Skip to content

Latest commit

 

History

History
192 lines (135 loc) · 9 KB

adapter.md

File metadata and controls

192 lines (135 loc) · 9 KB

The weevil Debug Adapter

Introduction

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

Messaging invariants

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 a threads 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 of seq 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 at seq=1 and each subsequent new message from that actor increments the previously used seq 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.

Handlers

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 the NextRequest part of the main Request 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 equivalent NextResponse part of the Response 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 correct seq number (as shown in the next 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 just unit.

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

  1. 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 with Request GADT machinery like extract

  2. 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
  3. The dapper library also provides a specialisation of the standard OCaml result type where the error 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.

State

TODO