-
-
Notifications
You must be signed in to change notification settings - Fork 136
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Better error handling for calls to Lua functions from Python #189
base: master
Are you sure you want to change the base?
Conversation
@scoder I think these are really important changes, could you review it? |
* Added section in README about handling errors * LuaError raised in Lua is not "unpacked", but kept as _PyException to preserve traceback and original value
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Very nice.
One issue is that it seems to become less clear when exceptions and Python frames (which can hold on to arbitrarily large amounts of data) are getting cleaned up – and if at all. Cannot say if that's a real problem in practice.
elif isinstance(err, BaseException): | ||
raise err |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This behaviour wasn't there before, right? Why should there be a Python exception on the stack? And why should we special-case it here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note that we only reach here if result is not 0, that means that an error occurred and that the error object is on top of the stack. In this case, the error object is a wrapped Python object. We check if it is a base exception so we can simply re-raise it. This accounts for the following use case:
-- in Lua
function raiseKeyError(s) error(python.builtins.KeyError(s)) end
... from Python ...
lua.globals().raiseKeyError("something") # raises KeyError("something")
"__file__": filename, | ||
"__lupa_exception__": exc, | ||
} | ||
code = compile("\n" * (lineno - 1) + "raise __lupa_exception__", filename, "exec") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a fairly heavy operation, especially when done multiple times. Can't we reuse the result somehow?
Cython creates its own code+frame objects as well, using the C-API. I think we can do it similarly. Note that only the frame really needs to know the line number in the end, less so the code object.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You're right, the __Pyx_AddTraceback
function does something with Frame and Code objects. I will look into that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So, I took a look at the code generated by Cython, and it's very messy. I think this solution in pure Python is more portable (although not optimal). Could we leave this improvement for a future PR? It's not a big performance bottleneck because, unless someone is constantly throwing and catching errors in a tight loop, code objects and frames will be built on rare occasions.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a related discussion going on on python-dev
, so I asked and there seems to be general interest in improving the C-API for this. Not something for right now, but once that's available and used (and backported) by Cython, Lupa (and probably several other projects that handle foreign languages, DSLs, templates, …) could benefit from it.
Co-authored-by: scoder <[email protected]>
So, I've tested the garbage collector of Lua and Python submitted to many errors with the following script. import gc
import lupa
pygcdata = []
luagcdata = []
def checkgc():
luagccount = checkluagc()
while gc.collect() != 0:
# collect garbage from Python and Lua
luagccount = checkluagc()
# print Python object count
pygcdata.append(len(gc.get_objects()))
luagcdata.append(luagccount*1024)
def showgcdata():
import matplotlib.pyplot as plt
plt.title('GC utilization by Lua and Python')
plt.plot(pygcdata, label='Python (#objects)')
plt.plot(luagcdata, label='Lua (bytes)')
plt.legend()
plt.show()
lua = lupa.LuaRuntime()
checkluagc = lua.eval('''function()
while true do
before = collectgarbage('count')
collectgarbage()
after = collectgarbage('count')
if before == after then
return after
end
end
end''')
s = 'error()'
checkgc()
for i in range(1000):
try: lua.eval(s)
except: pass
finally: checkgc()
showgcdata() Here are the results: Imgur |
There was a related discussion going on on python-dev, so I asked and there seems to be general interest
in improving the C-API for this. Not something for right now, but once that's available and used (and backported)
by Cython, Lupa (and probably several other projects that handle foreign languages, DSLs, templates, …)
could benefit from it.
That's awesome to hear! It would be really of great help for projects like ours and many others like Jinja.
|
Problem 1: error objects and stack tracebacks
Currently, when you call a Lua function from Python, it uses the
debug.traceback
message handler, which adds the stack traceback to the error message string. This pollutes the original error message. In these circumstances, if you are handling Lua errors from Python, you need to search for a substring instead of a cleaner equality check. So, we need to keep the error object intact. Well, how are we going to add the Lua traceback to theLuaError
exception? Well, we can convert the Lua traceback (which is obtainable via the Lua debug library) into Python traceback objects and link them nicely.Solution: add a message handler that creates a Python exception according to the error object and adds a Python stack traceback extracted from the Lua debug library (
lua_getstack
andlua_getinfo
). When calling Python functions from Lua, the exception information (obtained fromsys.exc_info
) is stored inside an instance of the (new)_PyException
class, which is wrapped in a Lua userdatum.Problem 2: error re-raising is not re-entrant
Currently, when you call a Python function from Lua, and it raises a Python exception, it is converted to a Lua error and stored in
_raised_exception
inside theLuaRuntime
instance. It is easy to see that this solution is not re-entrant, that is, it doesn't work for arbitrarily recursive calls between Lua and Python. So, instead of storing exception information (which includes the stack traceback) in theLuaRuntime
instance, we need to propagate exception information via the error object, which is unique to each protected call in Lua.Solution: Handle
_PyException
andBaseException
instances raised from protected calls to Lua functions from Python.Problem 3: clearing the stack
I never understood why Lupa clears the stack before it calls a Lua function from Python or vice versa. The Lua stack can be indexed either from the bottom (positive) and from the top (negative), which makes manipulating only the top
n
values very easy.Solution: Use negative indices to navigate through the top-most values in the Lua stack.
Problem 4: type checking Python objects from Lua
Thanks to
python.builtins.type
the user is able to check the type of Python objects from Lua. However, this does not tell whether the object is a wrapped Python object or not. Ergo,python.builtins.type(nil)
andpython.builtins.type(python.none)
output the same type,NoneType
.Solution: Add
python.is_object
for checking if a Lua value is a wrapped Python object or notAdditional changes
python.is_error
for checking if a Lua value is a wrapped_PyException
instancelock_runtime
(before Safer handling of the Lua stack #188) and addtry_lock_runtime
(returns boolean)_LuaObject.__dealloc__
and_LuaIter.__dealloc__
(it's OK to callluaL_unref
withLUA_NOREF
orLUA_REFNIL
)lua_xmove
inresume_lua_thread