[Lldb-commits] [lldb] [lldb/Python] Reimplement PythonCallable::GetArgInfo without executing Python code (PR #213378)
Med Ismail Bennani via lldb-commits
lldb-commits at lists.llvm.org
Fri Jul 31 16:48:42 PDT 2026
https://github.com/medismailben created https://github.com/llvm/llvm-project/pull/213378
b05a5d09547b added an arity-trimming step to the shared `ScriptedPythonInterface::Dispatch`: extensions are now allowed to define methods with trailing parameters as optional (`num_children(self)` vs. `num_children(self, max_count)`), so before calling into a method, `Dispatch` needs to know how many positional arguments it actually accepts and drop any trailing ones we'd otherwise pass. That check calls `PythonCallable::GetArgInfo`, which ran a whole embedded Python script through `inspect.signature` on every call, since every scripted-extension dispatch goes through it.
Everything `GetArgInfo` needs is available as plain data attributes, with no Python bytecode execution required: `__func__`/`__self__` to unwrap bound methods, `__call__`/`__init__` to resolve classes and callable instances, and `__code__`'s `co_argcount`/`co_flags` for the actual positional-argument count and varargs bit. This removes the `inspect.signature` cost entirely rather than just amortizing it, and closes the C-stack-overflow path outright.
rdar://183776556
>From f4cfe9d6dcb4c5b518f5fd98d1c97f1c92e35c56 Mon Sep 17 00:00:00 2001
From: Med Ismail Bennani <ismail at bennani.ma>
Date: Fri, 31 Jul 2026 16:46:15 -0700
Subject: [PATCH] [lldb/Python] Reimplement PythonCallable::GetArgInfo without
executing Python code
b05a5d09547b added an arity-trimming step to the shared Dispatch<T>():
extensions are are allowed to define methods with trailing parameters as
optional (`num_children(self)` vs. `num_children(self, max_count)`), so
before calling into a scripted method, Dispatch<T>() needs to know how
many positional arguments it actually accepts and drop any trailing
ones we'd otherwise pass. That check calls PythonCallable::GetArgInfo(),
which ran a whole embedded Python script through inspect.signature() on
every call, since every scripted-extension dispatch goes through it.
Everything GetArgInfo() needs is available as plain data attributes,
with no Python bytecode execution required: __func__/__self__ to unwrap
bound methods, __call__/__init__ to resolve classes and callable
instances, and __code__'s co_argcount/co_flags for the actual
positional-argument count and varargs bit. This removes the
inspect.signature() cost entirely rather than just amortizing it, and
closes the C-stack-overflow path outright.
rdar://183776556
Signed-off-by: Med Ismail Bennani <ismail at bennani.ma>
---
.../Python/PythonDataObjects.cpp | 92 ++++++++++++-------
1 file changed, 60 insertions(+), 32 deletions(-)
diff --git a/lldb/source/Plugins/ScriptInterpreter/Python/PythonDataObjects.cpp b/lldb/source/Plugins/ScriptInterpreter/Python/PythonDataObjects.cpp
index fba98abf9e83b..7ac8085337e94 100644
--- a/lldb/source/Plugins/ScriptInterpreter/Python/PythonDataObjects.cpp
+++ b/lldb/source/Plugins/ScriptInterpreter/Python/PythonDataObjects.cpp
@@ -818,43 +818,71 @@ bool PythonCallable::Check(PyObject *py_obj) {
return PyCallable_Check(py_obj);
}
-static const char get_arg_info_script[] = R"(
-from inspect import signature, Parameter, ismethod
-from collections import namedtuple
-ArgInfo = namedtuple('ArgInfo', ['count', 'has_varargs'])
-def main(f):
- count = 0
- varargs = False
- for parameter in signature(f).parameters.values():
- kind = parameter.kind
- if kind in (Parameter.POSITIONAL_ONLY,
- Parameter.POSITIONAL_OR_KEYWORD):
- count += 1
- elif kind == Parameter.VAR_POSITIONAL:
- varargs = True
- elif kind in (Parameter.KEYWORD_ONLY,
- Parameter.VAR_KEYWORD):
- pass
- else:
- raise Exception(f'unknown parameter kind: {kind}')
- return ArgInfo(count, varargs)
-)";
-
Expected<PythonCallable::ArgInfo> PythonCallable::GetArgInfo() const {
ArgInfo result = {};
if (!IsValid())
return nullDeref();
- // no need to synchronize access to this global, we already have the GIL
- static PythonScript get_arg_info(get_arg_info_script);
- Expected<PythonObject> pyarginfo = get_arg_info(*this);
- if (!pyarginfo)
- return pyarginfo.takeError();
- long long count =
- cantFail(As<long long>(pyarginfo.get().GetAttribute("count")));
- bool has_varargs =
- cantFail(As<bool>(pyarginfo.get().GetAttribute("has_varargs")));
- result.max_positional_args = has_varargs ? ArgInfo::UNBOUNDED : count;
+ // Resolve to the underlying plain function. Bound instance/class methods
+ // expose it via `__func__`, with `__self__` telling us the self/cls
+ // argument is supplied automatically and shouldn't be counted below. A
+ // class used as a constructor (e.g. `CreatePluginObject` resolving a
+ // scripted extension's class object) has neither: calling it invokes
+ // `__init__`, which always takes an implicit `self`. A plain instance
+ // used as a callable (defining `__call__` instead of being a function
+ // itself, e.g. `command script add -f some_module.some_callable_obj`)
+ // also has neither, but should be introspected via `__call__`, not the
+ // unrelated `__init__` used to construct that instance in the first
+ // place.
+ PythonObject func = *this;
+ bool implicit_first_arg = false;
+ if (HasAttribute("__self__")) {
+ implicit_first_arg = true;
+ Expected<PythonObject> func_or_err = GetAttribute("__func__");
+ if (!func_or_err)
+ return func_or_err.takeError();
+ func = func_or_err.get();
+ } else if (!HasAttribute("__code__")) {
+ implicit_first_arg = true;
+ Expected<PythonObject> unwrapped_or_err = PyType_Check(m_py_obj)
+ ? GetAttribute("__init__")
+ : GetAttribute("__call__");
+ if (!unwrapped_or_err)
+ return unwrapped_or_err.takeError();
+ func = unwrapped_or_err.get();
+ // `__call__` resolved via an instance is itself a bound method, so
+ // recurse once to unwrap it the same way as any other bound method.
+ if (func.HasAttribute("__self__")) {
+ Expected<PythonObject> inner_func_or_err = func.GetAttribute("__func__");
+ if (!inner_func_or_err)
+ return inner_func_or_err.takeError();
+ func = inner_func_or_err.get();
+ }
+ }
+
+ Expected<PythonObject> code_or_err = func.GetAttribute("__code__");
+ if (!code_or_err)
+ return code_or_err.takeError();
+ PythonObject code = code_or_err.get();
+
+ Expected<long long> argcount =
+ As<long long>(code.GetAttribute("co_argcount"));
+ if (!argcount)
+ return argcount.takeError();
+ Expected<long long> flags = As<long long>(code.GetAttribute("co_flags"));
+ if (!flags)
+ return flags.takeError();
+
+ // CO_VARARGS: set when the function accepts a `*args`-style catch-all.
+ // This flag value is part of the long-standing `inspect`/`dis` module
+ // constants and hasn't changed since Python 2.
+ constexpr long long CO_VARARGS = 0x04;
+ if (*flags & CO_VARARGS) {
+ result.max_positional_args = ArgInfo::UNBOUNDED;
+ } else {
+ long long count = *argcount - (implicit_first_arg ? 1 : 0);
+ result.max_positional_args = count > 0 ? static_cast<unsigned>(count) : 0;
+ }
return result;
}
More information about the lldb-commits
mailing list