This document introduces the concept of custom inspectors which allows you to plug your own logic to decode children properties debugging.
This document is a tutorial, it just introduces concepts and best practices. For details about used functions, see the debugger.introspection
API documentation.
There are two main uses cases:
- Enumerate properties of userdata object (which debugger cannot inspect at all out of the box)
- Customize debugging of tables to hide internal fields and reduce noise
The provided API tries to be as simple as possible by hiding most debugger <=> IDE communication logic such as recursion limit, pagination, … However it is not yet considerated as final and may change without notice.
Inspector modules
The inspectors are module distinct from the debugger and must be registered in order to be active. It can be done before or after starting debugging.
For simplicity, inspector modules should register themselves into debugger core and do not return anything. So attaching a module is simply a require
or dofile
call. This violates the no global side effects policy of Lua modules, but for debugging purposes, I think we can live with that :)
Probing objects
First, we need to know what object we want to inspect. For this, two distinct modes are available.
Metatable matching
This matching mode works by associating an inspector function to a given metatable. It requires you to know in advance what metatables are expected (for example, all your Car objects share the same metatable).
For this we simply add our inspector function to debugger.introspection.inspectors
table with the metatable as key. Your module should look like:
local introspection = require 'debugger.introspection'
local function my_inspector(name, value, parent, fullname)
-- my inspection logic
end
.inspectors[my_metatable] = my_inspector introspection
Generic probe
Some libraries use a lot of different metatables or create them on-the-fly. In this situation, it can became tricky to list all possible metatables for a given object family.
To handle such cases, you can add a probe function that will be called for any value (table or userdata) when a specific metatable inspector cannot be found.
As you can imagine, this method is munch slower than previous one so do not abuse this feature by attaching many such probes or making expensive processing on them.
Generic probes are added to debugger with debugger.introspection.add_probe
function. A typical module should look like:
local introspection = require 'debugger.introspection'
local function my_inspector(name, value, parent, fullname)
if is_my_object(value)
-- my inspection logic
end
return nil -- unknown object
end
.add_probe(my_inspector) introspection
Inspection logic
Now we’re able to detect values to inspect, let’s generate our properties. This what inspector functions do.
These functions generate properties that will be sent to IDE with the debugger.introspection.property
function. They are called with a value to inspect and must generate a debugger property for it (and possibly its children for complex data structures).
Like we saw in previous part, inspector functions always takes 4 parameters and their return values are defined too.
These arguments are:
- name is the property name that appear on the name column of debugger view. This name is usually given by parent object (that is the inspector that called your inspector)
- value is the actual value to inspect
- parent is the parent property of the value (a debugger data structure, not
value
’s parent !) - fullname this is the expression that will be evaluated by the debugger to get
value
again (either to query its children or to set it).
Generating properties
The main task of the inspector is to call debugger.introspection.property
to generate a property corresponding to value
. This function will check if the value should be sent to IDE, depending on this, it returns:
- A Property object if the property have be sent to IDE, in this case you can inspect children properties (if any) and return that object to caller
nil
if value will not be sent to IDE (too deep recursion for instance). In this case, you should not try to inspect children and return anil
value immediately.
The inspector that is used for most primitive types is simply:
local function default_inspector(name, value, parent, fullname)
return introspection.property(name, type(value), tostring(value), parent, fullname)
end
Inspecting children properties
Now let’s inspect more complex types with child properties. This is done by either by calling debugger.introspection.property
more than once if you want to fully handle children inspection or by calling debugger.introspection.inspect
to dispatch an arbitrary value to appropriate inspector.
As you can imagine, the parent
of these sub-properties is the object returned by debugger.introspection.property
. For fullname
(the expression used to retrieve the value in future calls), you can use debugger.introspection.make_fullname
to generate a valid Lua expression However, this method works only if your property can be accessed by index.
If the value is retrieved by some other way (like calling a get_my_prop
function) there is currently no supported way to handle it, see Limitations section).
Let’s show an example with a Car that have x, y, z and speed properties that we want to expose and some internal id that we will use only for string representation:
local function car_inspector(name, value, parent, fullname)
local prop = introspection.property(name, 'Car', 'Car #' .. value.id, parent, fullname)
-- do not inspect children if the parent is not generated
if not prop then return nil end
-- use regular introspection for x,y,z
.inspect('x', value.x, prop, introspection.make_fullname(fullname, 'x'))
introspection.inspect('y', value.y, prop, introspection.make_fullname(fullname, 'y'))
introspection.inspect('z', value.z, prop, introspection.make_fullname(fullname, 'z'))
introspection
-- generate directly speed property
.property('speed', 'Speed', tostring(value.speed), prop, introspection.make_fullname(fullname, 'speed'))
introspection
return prop
end
Samples
Write a module for a debugger can be tricky, that is why we thought samples could help. We will continue on the car use case. Here the idea is to provide debugger display modules for the following code.
local C = {}
local mt = {
__index = function(self, key)
return self[string.format('_%s',key or '')] or 'Nameless'
end
}
local id = 1
function C.newcar(name)
local car = { _id = id , _name = name , _type = 'Car' }
= id + 1
id setmetatable(car, mt)
return car
end
return C
car.lua
if os.getenv('DEBUG_MODE') then
require 'carintrospection'
end
local car = require 'car'
local c = car.newcar('Chevrolet')
print ( c.name )
main.lua
Metatable matching
Some insight about writing a metatable matching display module. This is the faster method as it is call only when adequate.
local introspection = require 'debugger.introspection'
local function car_inspector(name, value, parent, fullname)
local carlabel = string.format('%s (car:%d)', value.name, value.id)
local property = introspection.property(name, 'Car', carlabel, parent, fullname)
-- Inspect children
.inspect('id', tostring(value.id), property, introspection.make_fullname(fullname, 'id'))
introspection.inspect('name', value.name, property, introspection.make_fullname(fullname, 'name'))
introspectionreturn property
end
local car = require 'car'
local mt = getmetatable(car.newcar('Debugger inspector'))
if mt then
.inspectors[ mt ] = car_inspector
introspectionend
Generic probe
Here is a generic probe display module for given sample. This method is applicable to a wider range of use cases. Use it with caution, as it call for each value displayed by the debugger.
local introspection = require 'debugger.introspection'
local function carprobe(name, value, parent, fullname)
if value._type ~= 'Car' then
return nil
end
local carlabel = string.format('%s (car:%d)', value.name, value.id)
local property = introspection.property(name, 'Car', carlabel, parent, fullname)
-- Inspect children
.inspect('id', tostring(value.id), property, introspection.make_fullname(fullname, 'id'))
introspection.inspect('name', value.name, property, introspection.make_fullname(fullname, 'name'))
introspectionreturn property
end
.add_probe( carprobe ) introspection
Limitations
As we’ve seen before, the debugger engine is currently unable to use anything else than indexation to retrieve values or set values after inspection phase, so you will be able to use this feature only if your userdata (or table) implements __index
metamethod correctly.
For setting data, the debugger makes basically dostring(fullname .. ' = ' .. newvalue)
in a sandboxed environment. So, __newindex
metamethod must be implemented if you want to modify data.