Source-Python-Dev-Team / Source.Python

This plugin aims to use boost::python and create an easily accessible wrapper around the Source Engine API for scripter use.
http://forums.sourcepython.com
GNU General Public License v3.0
163 stars 31 forks source link

KeyError: '__builtins__' #385

Closed jordanbriere closed 3 years ago

jordanbriere commented 3 years ago

While playing around #384, I ran across an issue that I was able to reproduce randomly with the following steps:

from players.entity import Player

Player(1).give_named_item('weapon_awp')

Randomly results into:

[SP] Caught an Exception:
Traceback (most recent call last):
  File "..\addons\source-python\packages\source-python\plugins\command.py", line 164, in load_plugin
    plugin = self.manager.load(plugin_name)
  File "..\addons\source-python\packages\source-python\plugins\manager.py", line 207, in load
    plugin._load()
  File "..\addons\source-python\packages\source-python\plugins\instance.py", line 74, in _load
    self.module = import_module(self.import_name)
  File "..\addons\source-python\plugins\testing\testing.py", line 3, in <module>
    Player(1).give_named_item('weapon_awp')
  File "..\addons\source-python\packages\source-python\entities\helpers.py", line 108, in __call__
    *self.wrapper(self.wrapped_self, *args, **kwargs))
  File "..\addons\source-python\packages\source-python\memory\helpers.py", line 334, in __call__
    return super().__call__(self._this, *args)
  File "<string>", line 1, in <lambda>

KeyError: '__builtins__'

Not sure yet why exactly this happens, but it is also reproducible on master.

jordanbriere commented 3 years ago

Okay, after a quick investigation, it appears to be caused by a thread/frame issue related to hibernation combined with the compiled expression bound to CFunction::__call__ through raw_method.

Basically, when that expression is compiled by the interpreter at that time, PyEval_GetGlobals returns NULL resulting into the globals/locals being empty dictionaries. Normally, this is not really an issue, but since the compiled expression returns a nested lambda, and that globals == locals, it tries to resolve its namespace from the original scope instead of the calling frame.

jordanbriere commented 3 years ago

Weirdly enough, it is reproducible 100% of the times by adding the following import:

import entities.hooks
from players.entity import Player

Player(1).give_named_item('weapon_awp')

Not really sure why that is at this point, but raw_method fixes it while also being slightly faster. Tested the following:

"""
struct Foo { object dummy(boost::python::tuple args, dict kwargs) { return object(); } };
class_<Foo>("Foo")
    .def("bar", raw_method(&Foo::dummy))
    .def("baz", eval("lambda method: lambda *args, **kw: method(args[0], args[1:], kw)")(
        make_function(&Foo::dummy))
    )
;
"""

from time import *

foo = Foo()

t = time()
for i in range(10000000):
    foo.bar()
print('Without eval:', time() - t)

t = time()
for i in range(10000000):
    foo.baz()
print('With eval:', time() - t)

Results:

Without eval: 4.681713581085205
With eval: 5.766758680343628