I'm trying to make a C++ logger class for embedded python script with pybind11. How can I retrieve the line number where a C++ function is called from python?
I have something like this in C++:
class PythonLogger
{
public:
PythonLogger(const std::string& filename) { /* opens log file */ }
~PythonLogger() { /* closes log file */ }
void log(const std::string& msg) {
// writes formated log message to log file:
// [<current time>] [<script file name>:<line number>]: msg
// "line number" here should be the line number where
// this function is called inside python script
}
private:
<some file class> log_file;
};
typedef std::shared_ptr<PythonLogger> PythonLoggerPtr;
PYBIND11_EMBEDDED_MODULE(PythonLogger, m) {
py::class_<PythonLogger, PythonLoggerPtr>(m, "Logger")
.def("debug", &PythonLogger::debug);
}
int main()
{
py::scoped_interpreter guard{};
PythonLoggerPtr logger = std::make_shared<PythonLogger>("python_log.log");
try
{
auto script = py::module_::import("script");
script.import("PythonLogger");
script.attr("logger") = logger;
auto func = script.attr("func");
func();
}
catch (const std::exception& e)
{
std::print("{}\n", e.what());
}
}
Please ignore that I didn't actually include any headers in this code.
In script.py:
def func():
logger.debug("debug message")
And if I run this code, it should write this to the log file:
[<current time>] [script.py:2]: debug message
One possibility is to inspect the Python call stack from the C++ function and grab the info about the caller from there. This approach might involve a noticeable overhead -- I haven't measured it, but it would be a good idea before you use this in production.
You could do this using the standard
inspectmodule, for example by callinginspect.stack().This function returns a list of frame information objects, and we're interested in the first one.
Now, we want to grab attributes
filename(a string) andlineno(an integer).Next, cast them into C++ types.
And now you can generate your desired log message.
Example code:
Python script
test_script.pyused by the above example:Example output:
Notes
One improvement would be to cache the
inspect.stackfunction in persistent logger object, so you don't need to fetch it for every message.Another would be to rewrite it to directly use Python C API to extract the relevant frame info without round-trip to the
inspectimplementation. One some further inspection, this might be a moving target relying on implementation details that can (and do) change over time. Some avenues for research nevertheless:Alternate Approach
After reading through the code of the
inspectmodule, I've arrived at following approach usingsys._getframeand theframeobject directly:The rest would be the same.
Cache the result of
sys_mod.attr("_getframe")to avoid fetching it every time.However, if you do cache that function, you will probably have to make sure the cached object's lifetime doesn't exceed the lifetime of the interpreter. I'll leave it up to the reader to figure that out and handle.