{ Copyright (c) 2015 by Nikolay Nikolov This unit provides a wrapper around GDB and implements parsing of the GDB/MI command result records. See the file COPYING.FPC, included in this distribution, for details about the copyright. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. **********************************************************************} unit gdbmiwrap; {$MODE objfpc}{$H+} {$ASSERTIONS on} {$I globdir.inc} interface uses SysUtils, Classes, GDBMIProc; type {$ifdef TARGET_IS_64BIT} { force 64bit if target compilation CPU is 64-bit address CPU } CORE_ADDR = Qword; {$else} CORE_ADDR = PtrUInt; {$endif} TGDBMI_TupleValue = class; TGDBMI_ListValue = class; TGDBMI_Value = class function AsString: string; function AsInt64: Int64; function AsQWord: QWord; function AsLongInt: LongInt; function AsLongWord: LongWord; function AsCoreAddr: CORE_ADDR; function AsTuple: TGDBMI_TupleValue; function AsList: TGDBMI_ListValue; end; { "C string\n" } TGDBMI_StringValue = class(TGDBMI_Value) FStringValue: string; public constructor Create(const S: string); property StringValue: string read FStringValue; end; (* {...} or [...] *) TGDBMI_TupleOrListValue = class(TGDBMI_Value) private FNames: array of string; FValues: array of TGDBMI_Value; function GetValue(const AName: string): TGDBMI_Value; public destructor Destroy; override; procedure Clear; procedure Add(AName: string; AValue: TGDBMI_Value); function HasNames: Boolean; function IsEmpty: Boolean; property Values [const AName: string]: TGDBMI_Value read GetValue; default; end; (* {} or {variable=value,variable=value,variable=value} *) TGDBMI_TupleValue = class(TGDBMI_TupleOrListValue) end; { [] or [value,value,value] or [variable=value,variable=value,variable=value] } TGDBMI_ListValue = class(TGDBMI_TupleOrListValue) private function GetCount: LongInt; function GetValueAt(AIndex: LongInt): TGDBMI_Value; public property Count: LongInt read GetCount; property ValueAt [AIndex: LongInt]: TGDBMI_Value read GetValueAt; end; TGDBMI_AsyncOutput = class FAsyncClass: string; FParameters: TGDBMI_TupleValue; public constructor Create; destructor Destroy; override; procedure Clear; property AsyncClass: string read FAsyncClass write FAsyncClass; property Parameters: TGDBMI_TupleValue read FParameters; end; TGDBMI_ResultRecord = class(TGDBMI_AsyncOutput) public function Success: Boolean; end; TGDBMI_AsyncOutput_List = array of TGDBMI_AsyncOutput; TGDBWrapper = class private FProcess: TGDBProcess; FRawResponse: TStringList; FConsoleStream: TStringList; FExecAsyncOutput: TGDBMI_AsyncOutput; FResultRecord: TGDBMI_ResultRecord; function IsAlive: Boolean; procedure ReadResponse; public NotifyAsyncOutput: TGDBMI_AsyncOutput_List; constructor Create; destructor Destroy; override; procedure Command(S: string); procedure WaitForProgramStop; property RawResponse: TStringList read FRawResponse; property ConsoleStream: TStringList read FConsoleStream; property ExecAsyncOutput: TGDBMI_AsyncOutput read FExecAsyncOutput; property ResultRecord: TGDBMI_ResultRecord read FResultRecord write FResultRecord; property Alive: Boolean read IsAlive; end; function QuoteString(S: string): string; function C2PascalNumberPrefix(const S: string): string; implementation function QuoteString(S: string): string; var I: LongInt; begin I := 1; Result := ''; while I <= Length(S) do begin case S[I] of '''': Result := Result + '\'''; '"': Result := Result + '\"'; #10: Result := Result + '\n'; #13: Result := Result + '\r'; #9: Result := Result + '\t'; #11: Result := Result + '\v'; #8: Result := Result + '\b'; #12: Result := Result + '\f'; #7: Result := Result + '\a'; '\': Result := Result + '\\'; '?': Result := Result + '\?'; else Result := Result + S[I]; end; Inc(I); end; Result := '"' + Result + '"'; end; function C2PascalNumberPrefix(const S: string): string; begin { hex: 0x -> $ } if (Length(S) >= 3) and (s[1] = '0') and ((s[2] = 'x') or (s[2] = 'X')) then exit('$' + Copy(S, 3, Length(S) - 2)); { oct: 0 -> & } if (Length(S) >= 2) and (s[1] = '0') and ((s[2] >= '0') and (s[2] <= '7')) then exit('&' + Copy(S, 2, Length(S) - 1)); Result := S; end; function TGDBMI_Value.AsString: string; begin Result := (self as TGDBMI_StringValue).StringValue; end; function TGDBMI_Value.AsInt64: Int64; begin Result := StrToInt64(C2PascalNumberPrefix(AsString)); end; function TGDBMI_Value.AsQWord: QWord; begin Result := StrToQWord(C2PascalNumberPrefix(AsString)); end; function TGDBMI_Value.AsLongInt: LongInt; begin Result := StrToInt(C2PascalNumberPrefix(AsString)); end; function TGDBMI_Value.AsLongWord: LongWord; const SInvalidInteger = '"%s" is an invalid integer'; var S: string; Error: LongInt; begin S := C2PascalNumberPrefix(AsString); Val(S, Result, Error); if Error <> 0 then raise EConvertError.CreateFmt(SInvalidInteger,[S]); end; function TGDBMI_Value.AsCoreAddr: CORE_ADDR; begin {$if defined(TARGET_IS_64BIT)} Result := AsQWord; {$elseif defined(CPU64)} Result := AsQWord; {$else} Result := AsLongWord; {$endif} end; function TGDBMI_Value.AsTuple: TGDBMI_TupleValue; begin Result := self as TGDBMI_TupleValue; end; function TGDBMI_Value.AsList: TGDBMI_ListValue; begin Result := self as TGDBMI_ListValue; end; constructor TGDBMI_StringValue.Create(const S: string); begin FStringValue := S; end; destructor TGDBMI_TupleOrListValue.Destroy; begin Clear; inherited Destroy; end; procedure TGDBMI_TupleOrListValue.Clear; var I: LongInt; begin SetLength(FNames, 0); for I := Low(FValues) to High(FValues) do FreeAndNil(FValues[I]); SetLength(FValues, 0); end; procedure TGDBMI_TupleOrListValue.Add(AName: string; AValue: TGDBMI_Value); begin Assert(AValue <> nil); Assert(IsEmpty or (HasNames = (AName <> ''))); if AName <> '' then begin SetLength(FNames, Length(FNames) + 1); FNames[Length(FNames) - 1] := AName; end; SetLength(FValues, Length(FValues) + 1); FValues[Length(FValues) - 1] := AValue; end; function TGDBMI_TupleOrListValue.HasNames: Boolean; begin Result := Length(FNames) > 0; end; function TGDBMI_TupleOrListValue.IsEmpty: Boolean; begin Result := Length(FValues) = 0; end; function TGDBMI_TupleOrListValue.GetValue(const AName: string): TGDBMI_Value; var I: LongInt; begin for I := Low(FNames) to High(FNames) do if FNames[I] = AName then begin Result := FValues[I]; exit; end; Result := nil; end; function TGDBMI_ListValue.GetCount: LongInt; begin Result := Length(FValues); end; function TGDBMI_ListValue.GetValueAt(AIndex: LongInt): TGDBMI_Value; begin Assert((AIndex >= Low(FValues)) and (AIndex <= High(FValues))); Result := FValues[AIndex]; end; constructor TGDBMI_AsyncOutput.Create; begin FParameters := TGDBMI_TupleValue.Create; end; destructor TGDBMI_AsyncOutput.Destroy; begin FParameters.Free; inherited Destroy; end; procedure TGDBMI_AsyncOutput.Clear; begin AsyncClass := ''; Parameters.Clear; end; function TGDBMI_ResultRecord.Success: Boolean; begin { according to the GDB docs, 'done' and 'running' should be treated identically by clients } Result := (AsyncClass='done') or (AsyncClass='running'); end; function ParseCString(const CStr: string; var NextCharPos: LongInt): string; begin if (NextCharPos <= Length(CStr)) and (CStr[NextCharPos] = '"') then Inc(NextCharPos); Result := ''; while NextCharPos <= Length(CStr) do begin if CStr[NextCharPos] = '"' then begin Inc(NextCharPos); exit; end else if CStr[NextCharPos] = '\' then begin Inc(NextCharPos); if NextCharPos <= Length(CStr) then case CStr[NextCharPos] of '''': Result := Result + ''''; '"': Result := Result + '"'; 'n': Result := Result + #10; 'r': Result := Result + #13; 't': Result := Result + #9; 'v': Result := Result + #11; 'b': Result := Result + #8; 'f': Result := Result + #12; 'a': Result := Result + #7; '\': Result := Result + '\'; '?': Result := Result + '?'; {\0, \000, \xhhh} end; end else Result := Result + CStr[NextCharPos]; Inc(NextCharPos); end; end; function ParseIdentifier(const S: string; var NextCharPos: LongInt): string; begin Result := ''; while (NextCharPos <= Length(S)) and (S[NextCharPos] in ['A'..'Z', 'a'..'z', '0'..'9', '-']) do begin Result := Result + S[NextCharPos]; Inc(NextCharPos); end; end; function ParseValue(const S: string; var NextCharPos: LongInt): TGDBMI_Value; var CStr: string; Tuple: TGDBMI_TupleValue; List: TGDBMI_ListValue; Name: string; Value: TGDBMI_Value; begin Assert(NextCharPos <= Length(S)); case S[NextCharPos] of '"': begin CStr := ParseCString(S, NextCharPos); Result := TGDBMI_StringValue.Create(CStr); end; '{': begin Inc(NextCharPos); Assert(NextCharPos <= Length(S)); Tuple := TGDBMI_TupleValue.Create; Result := Tuple; while (NextCharPos <= Length(S)) and (S[NextCharPos] <> '}') do begin Name := ParseIdentifier(S, NextCharPos); Assert(NextCharPos <= Length(S)); Assert(S[NextCharPos] = '='); Inc(NextCharPos); Value := ParseValue(S, NextCharPos); Tuple.Add(Name, Value); Assert(NextCharPos <= Length(S)); Assert(S[NextCharPos] in [',', '}']); if S[NextCharPos] = ',' then Inc(NextCharPos); end; if (NextCharPos <= Length(S)) and (S[NextCharPos] = '}') then Inc(NextCharPos); end; '[': begin Inc(NextCharPos); Assert(NextCharPos <= Length(S)); List := TGDBMI_ListValue.Create; Result := List; if S[NextCharPos] in ['"', '{', '['] then begin { list of values, no names } while (NextCharPos <= Length(S)) and (S[NextCharPos] <> ']') do begin Value := ParseValue(S, NextCharPos); List.Add('', Value); Assert(NextCharPos <= Length(S)); Assert(S[NextCharPos] in [',', ']']); if S[NextCharPos] = ',' then Inc(NextCharPos); end; end else begin { list of name=value pairs (like a tuple) } while (NextCharPos <= Length(S)) and (S[NextCharPos] <> ']') do begin Name := ParseIdentifier(S, NextCharPos); Assert(NextCharPos <= Length(S)); Assert(S[NextCharPos] = '='); Inc(NextCharPos); Value := ParseValue(S, NextCharPos); List.Add(Name, Value); Assert(NextCharPos <= Length(S)); Assert(S[NextCharPos] in [',', ']']); if S[NextCharPos] = ',' then Inc(NextCharPos); end; end; if (NextCharPos <= Length(S)) and (S[NextCharPos] = ']') then Inc(NextCharPos); end; else Assert(False); end; end; procedure ParseAsyncOutput(const S: string; AsyncOutput: TGDBMI_AsyncOutput; var NextCharPos: LongInt); var Name: string; Value: TGDBMI_Value; begin AsyncOutput.Clear; AsyncOutput.AsyncClass := ParseIdentifier(S, NextCharPos); while NextCharPos <= Length(S) do begin Assert(S[NextCharPos] = ','); Inc(NextCharPos); Name := ParseIdentifier(S, NextCharPos); Assert(NextCharPos <= Length(S)); Assert(S[NextCharPos] = '='); Inc(NextCharPos); Value := ParseValue(S, NextCharPos); AsyncOutput.Parameters.Add(Name, Value); end; end; function TGDBWrapper.IsAlive: Boolean; begin Result := Assigned(FProcess) and FProcess.Alive; end; procedure TGDBWrapper.ReadResponse; var S: string; I: LongInt; NextCharPos: LongInt; NAO: TGDBMI_AsyncOutput; begin FRawResponse.Clear; FConsoleStream.Clear; ExecAsyncOutput.Clear; for I := Low(NotifyAsyncOutput) to High(NotifyAsyncOutput) do FreeAndNil(NotifyAsyncOutput[I]); SetLength(NotifyAsyncOutput, 0); if not FProcess.Alive then exit; repeat S := FProcess.GDBReadLn; FRawResponse.Add(S); if Length(S) >= 1 then case S[1] of '~': begin NextCharPos := 2; FConsoleStream.Add(ParseCString(S, NextCharPos)); end; '*': begin NextCharPos := 2; ParseAsyncOutput(S, ExecAsyncOutput, NextCharPos); end; '^': begin NextCharPos := 2; ParseAsyncOutput(S, ResultRecord, NextCharPos); end; '=': begin NextCharPos := 2; NAO := TGDBMI_AsyncOutput.Create; try ParseAsyncOutput(S, NAO, NextCharPos); SetLength(NotifyAsyncOutput, Length(NotifyAsyncOutput) + 1); NotifyAsyncOutput[Length(NotifyAsyncOutput) - 1] := NAO; NAO := nil; finally NAO.Free; end; end; end; until (S = '(gdb) ') or (S = '(gdb)') or not FProcess.Alive; end; constructor TGDBWrapper.Create; begin FRawResponse := TStringList.Create; FConsoleStream := TStringList.Create; FProcess := TGDBProcess.Create; FExecAsyncOutput := TGDBMI_AsyncOutput.Create; FResultRecord := TGDBMI_ResultRecord.Create; ReadResponse; end; destructor TGDBWrapper.Destroy; begin if Alive then Command('-gdb-exit'); FProcess.Free; FResultRecord.Free; FExecAsyncOutput.Free; FConsoleStream.Free; FRawResponse.Free; end; procedure TGDBWrapper.Command(S: string); begin FProcess.GDBWriteLn(S); ReadResponse; end; procedure TGDBWrapper.WaitForProgramStop; begin repeat ReadResponse; until (ExecAsyncOutput.AsyncClass = 'stopped') or not FProcess.Alive; end; end.