RemedyBG»Blog

RemedyBG's Debug Protocol

RemedyBG's debug-protocol for requesting and receiving data has now been fleshed out. The way it works is pretty straight-forward and was inspired by Casey's discussion of debugger architecture from a while back.

To open a new inspector, the user makes a single call into RemedyBG. This call takes an inspector expression along with a user provided callback that gets triggered in a few different cases.

First, we'll talk a little bit about the inspector expression strings themselves. Briefly, an expression is structured as

{expression:field_1;field_2;...;field_n}

where 'expression' can be any of the pseudo-expressions (%callstack, %modules, %threads, %watches, %breakpoints, %registers, or %processes), a C-style array, or a linked-list in which the expression is augmented with the name of the field to allow traversal as

{expression(next_field);field_1;field_2;...;field_n}

The pseudo-expressions act as if they were C-structs with their own fields. For example, %callstack has the fields 'ModuleName', 'FunctionName', 'Parameters', and 'LineNumber'. These fields can be specified in any order and are all optional. If you only want to see the function and line-number, the expression would look like

{%callstack:FunctionName;LineNumber}

One additional twist is that the fields can themselves be enumerable expressions! For instance, in the case of %callstack, the 'Parameters' field is actually an enumerable expression in which you can pick and choose which fields you are interested in. For example, to request all the (currently) available fields and sub-fields of the %callstack pseudo-expression you would pass in

{%callstack:ModuleName;FunctionName;{Parameters:Type;Name;Value};LineNumber}

That is how you ask RemedyBG to give you the data you want. After the expression is parsed and validated, the user receives various callbacks. The data sent in these callbacks are sent as binary blobs. I decided to send the data as a binary blob for a couple of reasons. First, sending the data back as a data structure with pointers (or even just offsets) was messy and error-prone. Sending a blob made the construction and parsing of the data on the other end much simpler. Second, this will allow the future possibility of remote debugging without having to change a bunch of stuff. In any case, sending the data as a binary blob made more sense.

The first reason the user supplied callback can be triggered, after RemedyBG parses the inspector expression and does some validation, is the description of the data layout. This includes the number of fields, their display names, and so forth. This is only, at the moment, triggered once since the data layout cannot change.

The user supplied callback can also be triggered whenever the data changes (pretty much anytime the debugger halts). This data contains the actual types (in the case a client wants to render a type in a certain way, for instance) along with their values. Though not currently implemented, this data will also contain flags about whether the data is actually valid, or stale, and so forth.

With those two callbacks a client can create a generic inspect window that doesn't have to know anything about call stacks or breakpoints or anything else. To put this idea to the test while developing the protocol I implemented the call stack inspect window. The code used to display the call stack is completely generic and will be able to handle the rest of the enumerable expressions as they are implemented without any change. This also includes the fields that are themselves enumerable. For instance, %callstack's 'Parameters' field (you can see this in the new screen shot I posted for the project page).

Thanks.
Simon Anciaux,
Could you show all the user code necessary to implement the call stack (even if it's not final)?
Here you go @mrmixer. There are obviously additional things that will be need to be fixed/tweaked/added (such as handling stale/invalid flags), but following is the currently implementation for the inspect window.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
static void DrawInspectWnd(InspectWndContext* iw_ctx)
{
   if (iw_ctx->data_layout_size > 0)
   {
      u8* data_layout = iw_ctx->data_layout;

      s32 sig = ReadAndAdvance(s32, data_layout);
      Assert(sig == 'GBDR');

      ImGui::SetNextWindowPos(ImVec2(0, 0), ImGuiCond_FirstUseEver);
      ImGui::SetNextWindowSize(ImVec2(1024, 400), ImGuiCond_FirstUseEver);

      s16 window_name_len = ReadAndAdvance(s16, data_layout);

      bool open;
      if (!ImGui::Begin((char*)data_layout, &open))
      {
         // optimization if the window is collapsed
         ImGui::End();
         return;
      }

      data_layout += window_name_len;

      bool is_read_only = true;  // TODO: Get this from the data layout descriptor

      ImGui::PushFont(s_ui_state.header_font);

      s16 num_fields = ReadAndAdvance(s16, data_layout);
      const char* ID = IDForInspectWindow(iw_ctx);
      ImGui::Columns(num_fields, ID);
      for (s32 field_desc_idx = 0; field_desc_idx < num_fields; ++field_desc_idx)
      {
         s16 num_subfields = ReadAndAdvance(s16, data_layout);
         s16data_name_len = ReadAndAdvance(s16, data_layout);
         ImGui::Text((char*)data_layout);
         ImGui::NextColumn();
         data_layout += data_name_len;
         for (s32 sfi = 0; sfi < num_subfields; ++sfi)
         {
            // NOTE: We are not drawing sub-headers in an inspect window so just skip over the
            // sub-expression field names.
            s16 num_subfields = ReadAndAdvance(short, data_layout);
            s16 data_name_len = ReadAndAdvance(short, data_layout);
            data_layout += data_name_len;
         }
      }
      ImGui::Separator();

      ImGui::PopFont();
      
      ImGui::PushFont(s_ui_state.normal_font);

      if (iw_ctx->data_size)
      {
         u8* data = iw_ctx->data;

         s32 sig = ReadAndAdvance(s32, data);
         Assert(sig == 'GBDR');

         s16 num_rows = ReadAndAdvance(s16, data);
         for (s32 row = 0; row < num_rows; ++row)
         {
            s16 num_fields = ReadAndAdvance(s16, data);
            for (s32 field = 0; field < num_fields; ++field)
            {
               rdbg_InspectorProtocolTypes type = (rdbg_InspectorProtocolTypes)ReadAndAdvance(short, data);
               if (type == Type_SubExpression)  // NOTE: Currently only supporting a single nested level
               {
                  s16 subexpr_rows = ReadAndAdvance(s16, data);

                  for (s32 subexpr_row = 0; subexpr_row < subexpr_rows; ++subexpr_row)
                  {
                     ImVec2 start, end;
                     s16 subexpr_num_fields = ReadAndAdvance(s16, data);
                     float width_per_field = ImGui::GetContentRegionAvailWidth() * 1.0f / subexpr_num_fields;
                     for (s32 subfield = 0; subfield < subexpr_num_fields; ++subfield)
                     {
                        start = ImGui::GetCursorPos();
                        rdbg_InspectorProtocolTypes subfield_type = (rdbg_InspectorProtocolTypes)ReadAndAdvance(short, data);

                        DisplayValue(subfield_type, &data);

                        if (subfield < subexpr_num_fields - 1)
                        {
                           end = ImGui::GetCursorPos();
                           float rest = width_per_field - (end.x - start.x);
                           if (rest < 0.0f)
                              rest = 0.0f;

                           ImGui::SameLine(rest);
                        }
                     }
                  }
               }
               else
               {
                  DisplayValue(type, &data);
               }

               ImGui::NextColumn();
            }
            ImGui::Separator();
         }
      }

      ImGui::PopFont();

      ImGui::End();
   }
}
Simon Anciaux,
Thanks