diff --git a/src/GDBDebugSession.ts b/src/GDBDebugSession.ts index e8365577..49d977cc 100644 --- a/src/GDBDebugSession.ts +++ b/src/GDBDebugSession.ts @@ -128,6 +128,9 @@ class ThreadWithStatus implements DebugProtocol.Thread { const ignoreCountRegex = /\s|>/g; const arrayRegex = /.*\[[\d]+\].*/; const arrayChildRegex = /[\d]+/; +const numberRegex = /^-?\d+(?:\.\d*)?$/; // match only numbers (integers and floats) +const cNumberTypeRegex = /\b(?:char|short|int|long|float|double)$/; // match C number types +const cBoolRegex = /\bbool$/; // match boolean export function hexToBase64(hex: string): string { // The buffer will ignore incomplete bytes (unpaired digits), so we need to catch that early @@ -1347,8 +1350,12 @@ export class GDBDebugSession extends LoggingDebugSession { } } if (varobj) { + const result = + args.context === 'variables' && Number(varobj.numchild) + ? await this.getChildElements(varobj, args.frameId) + : varobj.value; response.body = { - result: varobj.value, + result, type: varobj.type, variablesReference: parseInt(varobj.numchild, 10) > 0 @@ -1371,6 +1378,49 @@ export class GDBDebugSession extends LoggingDebugSession { } } + protected async getChildElements(varobj: VarObjType, frameHandle: number) { + if (Number(varobj.numchild) > 0) { + const objRef: ObjectVariableReference = { + type: 'object', + frameHandle: frameHandle, + varobjName: varobj.varname, + }; + const childVariables: DebugProtocol.Variable[] = + await this.handleVariableRequestObject(objRef); + const value = arrayChildRegex.test(varobj.type) + ? childVariables.map((child) => + this.convertValue(child) + ) + : childVariables.reduce< + Record + >( + (accum, child) => ( + (accum[child.name] = this.convertValue(child)), accum + ), + {} + ); + return JSON.stringify(value, null, 2); + } + return varobj.value; + } + + protected convertValue(variable: DebugProtocol.Variable) { + const varValue = variable.value; + const varType = String(variable.type); + if (cNumberTypeRegex.test(varType)) { + if (numberRegex.test(varValue)) { + return Number(varValue); + } else { + // probably a string/other representation + return String(varValue); + } + } else if (cBoolRegex.test(varType)) { + return Boolean(varValue); + } else { + return varValue; + } + } + /** * Implement the cdt-gdb-adapter/Memory request. */ @@ -1872,6 +1922,7 @@ export class GDBDebugSession extends LoggingDebugSession { } variables.push({ name: varobj.expression, + evaluateName: varobj.expression, value, type: varobj.type, memoryReference: `&(${varobj.expression})`, @@ -1944,6 +1995,7 @@ export class GDBDebugSession extends LoggingDebugSession { } variables.push({ name: varobj.expression, + evaluateName: varobj.expression, value, type: varobj.type, memoryReference: `&(${varobj.expression})`, @@ -2000,6 +2052,10 @@ export class GDBDebugSession extends LoggingDebugSession { printValues: mi.MIVarPrintValues.all, }); } + // Grab the full path of parent. + const topLevelPathExpression = + varobj?.expression ?? + (await this.getFullPathExpression(parentVarname)); // iterate through the children for (const child of children.children) { @@ -2011,10 +2067,13 @@ export class GDBDebugSession extends LoggingDebugSession { name, printValues: mi.MIVarPrintValues.all, }); + // Append the child path to the top level full path. + const parentClassName = `${topLevelPathExpression}.${child.exp}`; for (const objChild of objChildren.children) { const childName = `${name}.${objChild.exp}`; variables.push({ name: objChild.exp, + evaluateName: `${parentClassName}.${objChild.exp}`, value: objChild.value ? objChild.value : objChild.type, type: objChild.type, variablesReference: @@ -2045,8 +2104,7 @@ export class GDBDebugSession extends LoggingDebugSession { if (isArrayParent || isArrayChild) { // can't use a relative varname (eg. var1.a.b.c) to create/update a new var so fetch and track these // vars by evaluating their path expression from GDB - const exprResponse = await mi.sendVarInfoPathExpression( - this.gdb, + const fullPath = await this.getFullPathExpression( child.name ); // create or update the var in GDB @@ -2054,13 +2112,13 @@ export class GDBDebugSession extends LoggingDebugSession { frame.frameId, frame.threadId, depth, - exprResponse.path_expr + fullPath ); if (!arrobj) { const varCreateResponse = await mi.sendVarCreate( this.gdb, { - expression: exprResponse.path_expr, + expression: fullPath, frameId: frame.frameId, threadId: frame.threadId, } @@ -2069,7 +2127,7 @@ export class GDBDebugSession extends LoggingDebugSession { frame.frameId, frame.threadId, depth, - exprResponse.path_expr, + fullPath, true, false, varCreateResponse @@ -2090,8 +2148,13 @@ export class GDBDebugSession extends LoggingDebugSession { varobjName = arrobj.varname; } const variableName = isArrayChild ? name : child.exp; + const evaluateName = + isArrayParent || isArrayChild + ? await this.getFullPathExpression(child.name) + : `${topLevelPathExpression}.${child.exp}`; variables.push({ name: variableName, + evaluateName, value, type: child.type, variablesReference: @@ -2108,6 +2171,16 @@ export class GDBDebugSession extends LoggingDebugSession { return Promise.resolve(variables); } + /** Query GDB using varXX name to get complete variable name */ + protected async getFullPathExpression(inputVarName: string) { + const exprResponse = await mi.sendVarInfoPathExpression( + this.gdb, + inputVarName + ); + // result from GDB looks like (parentName).field so remove (). + return exprResponse.path_expr.replace(/[()]/g, ''); + } + // Register view // Assume that the register name are unchanging over time, and the same across all threadsf private registerMap = new Map(); diff --git a/src/integration-tests/test-programs/vars.c b/src/integration-tests/test-programs/vars.c index 56ed5575..a3168d67 100644 --- a/src/integration-tests/test-programs/vars.c +++ b/src/integration-tests/test-programs/vars.c @@ -5,11 +5,18 @@ struct bar int b; }; +struct baz +{ + float w; + double v; +}; + struct foo { int x; int y; struct bar z; + struct baz aa; }; int main() @@ -17,11 +24,13 @@ int main() int a = 1; int b = 2; int c = a + b; // STOP HERE - struct foo r = {1, 2, {3, 4}}; + struct foo r = {1, 2, {3, 4}, {3.1415, 1234.5678}}; int d = r.x + r.y; int e = r.z.a + r.z.b; int f[] = {1, 2, 3}; int g = f[0] + f[1] + f[2]; // After array init int rax = 1; + const unsigned char h[] = {0x01, 0x10, 0x20}; + const unsigned char k[] = "hello"; // char string setup return 0; } diff --git a/src/integration-tests/var.spec.ts b/src/integration-tests/var.spec.ts index 3a9b1fda..45d366cb 100644 --- a/src/integration-tests/var.spec.ts +++ b/src/integration-tests/var.spec.ts @@ -30,11 +30,12 @@ describe('Variables Test Suite', function () { let scope: Scope; const varsProgram = path.join(testProgramsDir, 'vars'); const varsSrc = path.join(testProgramsDir, 'vars.c'); - const numVars = 9; // number of variables in the main() scope of vars.c + const numVars = 11; // number of variables in the main() scope of vars.c const lineTags = { 'STOP HERE': 0, 'After array init': 0, + 'char string setup': 0, }; const hexValueRegex = /^0x[\da-fA-F]+$/; @@ -215,7 +216,7 @@ describe('Variables Test Suite', function () { expect( children.body.variables.length, 'There is a different number of child variables than expected' - ).to.equal(3); + ).to.equal(4); verifyVariable(children.body.variables[0], 'x', 'int', '1', { hasMemoryReference: false, }); @@ -250,7 +251,7 @@ describe('Variables Test Suite', function () { expect( children.body.variables.length, 'There is a different number of child variables than expected' - ).to.equal(3); + ).to.equal(4); verifyVariable(children.body.variables[0], 'x', 'int', '25', { hasMemoryReference: false, }); @@ -308,7 +309,7 @@ describe('Variables Test Suite', function () { expect( children.body.variables.length, 'There is a different number of child variables than expected' - ).to.equal(3); + ).to.equal(4); verifyVariable(children.body.variables[2], 'z', 'struct bar', '{...}', { hasChildren: true, hasMemoryReference: false, @@ -328,6 +329,23 @@ describe('Variables Test Suite', function () { verifyVariable(subChildren.body.variables[1], 'b', 'int', '4', { hasMemoryReference: false, }); + + // Evaluate the child structure foo.bar of r + let res = await dc.evaluateRequest({ + context: 'variables', + expression: 'r.z', + frameId: scope.frame.id, + }); + expect(res.body.result).eq('{\n "a": 3,\n "b": 4\n}'); + + // Evaluate the child structure foo.baz of r + res = await dc.evaluateRequest({ + context: 'variables', + expression: 'r.aa', + frameId: scope.frame.id, + }); + expect(res.body.result).eq('{\n "w": 3.1415,\n "v": 1234.5678\n}'); + // set the variables to something different const setAinHex = await dc.setVariableRequest({ name: 'a', @@ -392,7 +410,7 @@ describe('Variables Test Suite', function () { }); expect(br.success).to.equal(true); await dc.continue({ threadId: scope.thread.id }, 'breakpoint', { - line: 24, + line: lineTags['After array init'], path: varsSrc, }); scope = await getScopes(dc); @@ -474,7 +492,7 @@ describe('Variables Test Suite', function () { // step the program and see that the values were passed to the program and evaluated. await dc.next( { threadId: scope.thread.id }, - { path: varsSrc, line: 25 } + { path: varsSrc, line: lineTags['After array init'] + 1 } ); scope = await getScopes(dc); expect( @@ -489,4 +507,52 @@ describe('Variables Test Suite', function () { ).to.equal(numVars); verifyVariable(vars.body.variables[7], 'g', 'int', '66'); }); + + it('can evaluate char array elements (string)', async function () { + // skip ahead to array initialization + const br = await dc.setBreakpointsRequest({ + source: { path: varsSrc }, + breakpoints: [{ line: lineTags['char string setup'] }], + }); + expect(br.success).to.equal(true); + await dc.continue({ threadId: scope.thread.id }, 'breakpoint', { + line: lineTags['char string setup'], + path: varsSrc, + }); + // step the program and see that the values were passed to the program and evaluated. + await dc.next( + { threadId: scope.thread.id }, + { path: varsSrc, line: lineTags['char string setup'] + 1 } + ); + scope = await getScopes(dc); + expect( + scope.scopes.body.scopes.length, + 'Unexpected number of scopes returned' + ).to.equal(2); + // assert we can see the array and its elements + const vr = scope.scopes.body.scopes[0].variablesReference; + const vars = await dc.variablesRequest({ variablesReference: vr }); + expect( + vars.body.variables.length, + 'There is a different number of variables than expected' + ).to.equal(numVars); + // Evaluate the non-string char array + let res = await dc.evaluateRequest({ + context: 'variables', + expression: 'h', + frameId: scope.frame.id, + }); + expect(res.body.result).eq( + '[\n "1 \'\\\\001\'",\n "16 \'\\\\020\'",\n "32 \' \'"\n]' + ); + // Evaluate the string char array + res = await dc.evaluateRequest({ + context: 'variables', + expression: 'k', + frameId: scope.frame.id, + }); + expect(res.body.result).eq( + '[\n "104 \'h\'",\n "101 \'e\'",\n "108 \'l\'",\n "108 \'l\'",\n "111 \'o\'",\n "0 \'\\\\000\'"\n]' + ); + }); });