[pypy-commit] pypy stacklet: Rewrite the logic to handle threads in the shadowstack root finder.

arigo noreply at buildbot.pypy.org
Sun Aug 21 13:48:35 CEST 2011


Author: Armin Rigo <arigo at tunes.org>
Branch: stacklet
Changeset: r46683:8452ff8bb035
Date: 2011-08-21 13:53 +0200
http://bitbucket.org/pypy/pypy/changeset/8452ff8bb035/

Log:	Rewrite the logic to handle threads in the shadowstack root finder.
	Based now on custom_trace. This should be better because it allows
	thread that block for a long time to not have their shadowstack
	scanned again and again at every minor collection.

	It should also allow us to reduce virtual memory usage by putting a
	limit on the number of full-blown shadowstacks around (useful on 32
	bits if you have many threads).

	This last benefit is the key to this rewrite, which will make it
	reasonable to use the same code for stacklets too (future work).

diff --git a/pypy/module/thread/os_thread.py b/pypy/module/thread/os_thread.py
--- a/pypy/module/thread/os_thread.py
+++ b/pypy/module/thread/os_thread.py
@@ -15,11 +15,6 @@
 # * The start-up data (the app-level callable and arguments) is
 #   stored in the global bootstrapper object.
 #
-# * The GC is notified that a new thread is about to start; in the
-#   framework GC with shadow stacks, this allocates a fresh new shadow
-#   stack (but doesn't use it yet).  See gc_thread_prepare().  This
-#   has no effect in asmgcc.
-#
 # * The new thread is launched at RPython level using an rffi call
 #   to the C function RPyThreadStart() defined in
 #   translator/c/src/thread*.h.  This RPython thread will invoke the
@@ -33,8 +28,8 @@
 #   operation is called (this is all done by gil.after_external_call(),
 #   called from the rffi-generated wrapper).  The gc_thread_run()
 #   operation will automatically notice that the current thread id was
-#   not seen before, and start using the freshly prepared shadow stack.
-#   Again, this has no effect in asmgcc.
+#   not seen before, and (in shadowstack) it will allocate and use a
+#   fresh new stack.  Again, this has no effect in asmgcc.
 #
 # * Only then does bootstrap() really run.  The first thing it does
 #   is grab the start-up information (app-level callable and args)
@@ -180,7 +175,7 @@
     bootstrapper.acquire(space, w_callable, args)
     try:
         try:
-            thread.gc_thread_prepare()
+            thread.gc_thread_prepare()     # (this has no effect any more)
             ident = thread.start_new_thread(bootstrapper.bootstrap, ())
         except Exception, e:
             bootstrapper.release()     # normally called by the new thread
diff --git a/pypy/rpython/lltypesystem/lloperation.py b/pypy/rpython/lltypesystem/lloperation.py
--- a/pypy/rpython/lltypesystem/lloperation.py
+++ b/pypy/rpython/lltypesystem/lloperation.py
@@ -479,8 +479,9 @@
     # ^^^ returns an address of nursery free pointer, for later modifications
     'gc_adr_of_nursery_top' : LLOp(),
     # ^^^ returns an address of pointer, since it can change at runtime
+    'gc_adr_of_root_stack_base': LLOp(),
     'gc_adr_of_root_stack_top': LLOp(),
-    # ^^^ returns the address of gcdata.root_stack_top (for shadowstack only)
+    # returns the address of gcdata.root_stack_base/top (for shadowstack only)
 
     # for asmgcroot support to get the address of various static structures
     # see translator/c/src/mem.h for the valid indices
diff --git a/pypy/rpython/memory/gctransform/framework.py b/pypy/rpython/memory/gctransform/framework.py
--- a/pypy/rpython/memory/gctransform/framework.py
+++ b/pypy/rpython/memory/gctransform/framework.py
@@ -132,7 +132,6 @@
     return result
 
 class FrameworkGCTransformer(GCTransformer):
-    root_stack_depth = 163840
 
     def __init__(self, translator):
         from pypy.rpython.memory.gc.base import choose_gc_from_config
@@ -742,8 +741,13 @@
                   resultvar=op.result)
 
     def gct_gc_assume_young_pointers(self, hop):
+        if not hasattr(self, 'assume_young_pointers_ptr'):
+            return
         op = hop.spaceop
         v_addr = op.args[0]
+        if v_addr.concretetype != llmemory.Address:
+            v_addr = hop.genop('cast_ptr_to_adr',
+                               [v_addr], resulttype=llmemory.Address)
         hop.genop("direct_call", [self.assume_young_pointers_ptr,
                                   self.c_const_gc, v_addr])
 
@@ -762,37 +766,37 @@
         hop.genop("direct_call", [self.get_member_index_ptr, self.c_const_gc,
                                   v_typeid], resultvar=op.result)
 
-    def gct_gc_adr_of_nursery_free(self, hop):
-        if getattr(self.gcdata.gc, 'nursery_free', None) is None:
-            raise NotImplementedError("gc_adr_of_nursery_free only for generational gcs")
+    def _gc_adr_of_gc_attr(self, hop, attrname):
+        if getattr(self.gcdata.gc, attrname, None) is None:
+            raise NotImplementedError("gc_adr_of_%s only for generational gcs"
+                                      % (attrname,))
         op = hop.spaceop
         ofs = llmemory.offsetof(self.c_const_gc.concretetype.TO,
-                                'inst_nursery_free')
+                                'inst_' + attrname)
         c_ofs = rmodel.inputconst(lltype.Signed, ofs)
         v_gc_adr = hop.genop('cast_ptr_to_adr', [self.c_const_gc],
                              resulttype=llmemory.Address)
         hop.genop('adr_add', [v_gc_adr, c_ofs], resultvar=op.result)
 
+    def gct_gc_adr_of_nursery_free(self, hop):
+        self._gc_adr_of_gc_attr(hop, 'nursery_free')
     def gct_gc_adr_of_nursery_top(self, hop):
-        if getattr(self.gcdata.gc, 'nursery_top', None) is None:
-            raise NotImplementedError("gc_adr_of_nursery_top only for generational gcs")
-        op = hop.spaceop
-        ofs = llmemory.offsetof(self.c_const_gc.concretetype.TO,
-                                'inst_nursery_top')
-        c_ofs = rmodel.inputconst(lltype.Signed, ofs)
-        v_gc_adr = hop.genop('cast_ptr_to_adr', [self.c_const_gc],
-                             resulttype=llmemory.Address)
-        hop.genop('adr_add', [v_gc_adr, c_ofs], resultvar=op.result)
+        self._gc_adr_of_gc_attr(hop, 'nursery_top')
 
-    def gct_gc_adr_of_root_stack_top(self, hop):
+    def _gc_adr_of_gcdata_attr(self, hop, attrname):
         op = hop.spaceop
         ofs = llmemory.offsetof(self.c_const_gcdata.concretetype.TO,
-                                'inst_root_stack_top')
+                                'inst_' + attrname)
         c_ofs = rmodel.inputconst(lltype.Signed, ofs)
         v_gcdata_adr = hop.genop('cast_ptr_to_adr', [self.c_const_gcdata],
                                  resulttype=llmemory.Address)
         hop.genop('adr_add', [v_gcdata_adr, c_ofs], resultvar=op.result)
 
+    def gct_gc_adr_of_root_stack_base(self, hop):
+        self._gc_adr_of_gcdata_attr(hop, 'root_stack_base')
+    def gct_gc_adr_of_root_stack_top(self, hop):
+        self._gc_adr_of_gcdata_attr(hop, 'root_stack_top')
+
     def gct_gc_x_swap_pool(self, hop):
         raise NotImplementedError("old operation deprecated")
     def gct_gc_x_clone(self, hop):
@@ -938,24 +942,28 @@
                                   v_size])
 
     def gct_gc_thread_prepare(self, hop):
-        assert self.translator.config.translation.thread
-        if hasattr(self.root_walker, 'thread_prepare_ptr'):
-            hop.genop("direct_call", [self.root_walker.thread_prepare_ptr])
+        pass   # no effect any more
 
     def gct_gc_thread_run(self, hop):
         assert self.translator.config.translation.thread
         if hasattr(self.root_walker, 'thread_run_ptr'):
+            livevars = self.push_roots(hop)
             hop.genop("direct_call", [self.root_walker.thread_run_ptr])
+            self.pop_roots(hop, livevars)
 
     def gct_gc_thread_start(self, hop):
         assert self.translator.config.translation.thread
         if hasattr(self.root_walker, 'thread_start_ptr'):
+            # only with asmgcc.  Note that this is actually called after
+            # the first gc_thread_run() in the new thread.
             hop.genop("direct_call", [self.root_walker.thread_start_ptr])
 
     def gct_gc_thread_die(self, hop):
         assert self.translator.config.translation.thread
         if hasattr(self.root_walker, 'thread_die_ptr'):
+            livevars = self.push_roots(hop)
             hop.genop("direct_call", [self.root_walker.thread_die_ptr])
+            self.pop_roots(hop, livevars)
 
     def gct_gc_thread_before_fork(self, hop):
         if (self.translator.config.translation.thread
@@ -970,8 +978,10 @@
     def gct_gc_thread_after_fork(self, hop):
         if (self.translator.config.translation.thread
             and hasattr(self.root_walker, 'thread_after_fork_ptr')):
+            livevars = self.push_roots(hop)
             hop.genop("direct_call", [self.root_walker.thread_after_fork_ptr]
                                      + hop.spaceop.args)
+            self.pop_roots(hop, livevars)
 
     def gct_gc_get_type_info_group(self, hop):
         return hop.cast_result(self.c_type_info_group)
diff --git a/pypy/rpython/memory/gctransform/shadowstack.py b/pypy/rpython/memory/gctransform/shadowstack.py
--- a/pypy/rpython/memory/gctransform/shadowstack.py
+++ b/pypy/rpython/memory/gctransform/shadowstack.py
@@ -1,17 +1,16 @@
 from pypy.rpython.memory.gctransform.framework import BaseRootWalker
 from pypy.rpython.memory.gctransform.framework import sizeofaddr
-from pypy.rlib.debug import ll_assert
-from pypy.rpython.lltypesystem import llmemory
+from pypy.rpython.annlowlevel import llhelper
+from pypy.rpython.lltypesystem import lltype, llmemory
+from pypy.rpython.lltypesystem.lloperation import llop
 from pypy.annotation import model as annmodel
 
 
 class ShadowStackRootWalker(BaseRootWalker):
     need_root_stack = True
-    collect_stacks_from_other_threads = None
 
     def __init__(self, gctransformer):
         BaseRootWalker.__init__(self, gctransformer)
-        self.rootstacksize = sizeofaddr * gctransformer.root_stack_depth
         # NB. 'self' is frozen, but we can use self.gcdata to store state
         gcdata = self.gcdata
 
@@ -49,6 +48,8 @@
                     addr += sizeofaddr
             self.rootstackhook = default_walk_stack_root
 
+        self.shadow_stack_pool = ShadowStackPool(gcdata)
+
     def push_stack(self, addr):
         top = self.incr_stack(1)
         top.address[0] = addr
@@ -57,151 +58,104 @@
         top = self.decr_stack(1)
         return top.address[0]
 
-    def allocate_stack(self):
-        return llmemory.raw_malloc(self.rootstacksize)
-
     def setup_root_walker(self):
-        stackbase = self.allocate_stack()
-        ll_assert(bool(stackbase), "could not allocate root stack")
-        self.gcdata.root_stack_top  = stackbase
-        self.gcdata.root_stack_base = stackbase
+        self.shadow_stack_pool.initial_setup()
         BaseRootWalker.setup_root_walker(self)
 
     def walk_stack_roots(self, collect_stack_root):
         gcdata = self.gcdata
         self.rootstackhook(collect_stack_root,
                            gcdata.root_stack_base, gcdata.root_stack_top)
-        if self.collect_stacks_from_other_threads is not None:
-            self.collect_stacks_from_other_threads(collect_stack_root)
 
     def need_stacklet_support(self):
+        XXXXXX   # FIXME
         # stacklet support: BIG HACK for rlib.rstacklet
         from pypy.rlib import _stacklet_shadowstack
         _stacklet_shadowstack._shadowstackrootwalker = self # as a global! argh
 
     def need_thread_support(self, gctransformer, getfn):
-        XXXXXX   # FIXME
         from pypy.module.thread import ll_thread    # xxx fish
         from pypy.rpython.memory.support import AddressDict
         from pypy.rpython.memory.support import copy_without_null_values
         gcdata = self.gcdata
         # the interfacing between the threads and the GC is done via
-        # three completely ad-hoc operations at the moment:
-        # gc_thread_prepare, gc_thread_run, gc_thread_die.
-        # See docstrings below.
+        # two completely ad-hoc operations at the moment:
+        # gc_thread_run and gc_thread_die.  See docstrings below.
 
-        def get_aid():
-            """Return the thread identifier, cast to an (opaque) address."""
-            return llmemory.cast_int_to_adr(ll_thread.get_ident())
+        shadow_stack_pool = self.shadow_stack_pool
+
+        # this is a dict {tid: SHADOWSTACKREF}, where the tid for the
+        # current thread may be missing so far
+        gcdata.thread_stacks = None
+
+        # Return the thread identifier, as an integer.
+        get_tid = ll_thread.get_ident
 
         def thread_setup():
-            """Called once when the program starts."""
-            aid = get_aid()
-            gcdata.main_thread = aid
-            gcdata.active_thread = aid
-            gcdata.thread_stacks = AddressDict()     # {aid: root_stack_top}
-            gcdata._fresh_rootstack = llmemory.NULL
-            gcdata.dead_threads_count = 0
-
-        def thread_prepare():
-            """Called just before thread.start_new_thread().  This
-            allocates a new shadow stack to be used by the future
-            thread.  If memory runs out, this raises a MemoryError
-            (which can be handled by the caller instead of just getting
-            ignored if it was raised in the newly starting thread).
-            """
-            if not gcdata._fresh_rootstack:
-                gcdata._fresh_rootstack = self.allocate_stack()
-                if not gcdata._fresh_rootstack:
-                    raise MemoryError
+            tid = get_tid()
+            gcdata.main_tid = tid
+            gcdata.active_tid = tid
 
         def thread_run():
             """Called whenever the current thread (re-)acquired the GIL.
             This should ensure that the shadow stack installed in
             gcdata.root_stack_top/root_stack_base is the one corresponding
             to the current thread.
+            No GC operation here, e.g. no mallocs or storing in a dict!
             """
-            aid = get_aid()
-            if gcdata.active_thread != aid:
-                switch_shadow_stacks(aid)
+            tid = get_tid()
+            if gcdata.active_tid != tid:
+                switch_shadow_stacks(tid)
 
         def thread_die():
             """Called just before the final GIL release done by a dying
             thread.  After a thread_die(), no more gc operation should
             occur in this thread.
             """
-            aid = get_aid()
-            if aid == gcdata.main_thread:
+            tid = get_tid()
+            if tid == gcdata.main_tid:
                 return   # ignore calls to thread_die() in the main thread
                          # (which can occur after a fork()).
-            gcdata.thread_stacks.setitem(aid, llmemory.NULL)
-            old = gcdata.root_stack_base
-            if gcdata._fresh_rootstack == llmemory.NULL:
-                gcdata._fresh_rootstack = old
+            # we need to switch somewhere else, so go to main_tid
+            gcdata.active_tid = gcdata.main_tid
+            thread_stacks = gcdata.thread_stacks
+            new_ref = thread_stacks[gcdata.active_tid]
+            try:
+                del thread_stacks[tid]
+            except KeyError:
+                pass
+            # no more GC operation from here -- switching shadowstack!
+            shadow_stack_pool.forget_current_state()
+            shadow_stack_pool.restore_state_from(new_ref)
+
+        def switch_shadow_stacks(new_tid):
+            # we have the wrong shadowstack right now, but it should not matter
+            thread_stacks = gcdata.thread_stacks
+            try:
+                if thread_stacks is None:
+                    gcdata.thread_stacks = thread_stacks = {}
+                    raise KeyError
+                new_ref = thread_stacks[new_tid]
+            except KeyError:
+                new_ref = NULL_SHADOWSTACKREF
+            try:
+                old_ref = thread_stacks[gcdata.active_tid]
+            except KeyError:
+                # first time we ask for a SHADOWSTACKREF for this active_tid
+                old_ref = shadow_stack_pool.allocate()
+                thread_stacks[gcdata.active_tid] = old_ref
+            #
+            # no GC operation from here -- switching shadowstack!
+            shadow_stack_pool.save_current_state_away(old_ref)
+            if new_ref:
+                shadow_stack_pool.restore_state_from(new_ref)
             else:
-                llmemory.raw_free(old)
-            install_new_stack(gcdata.main_thread)
-            # from time to time, rehash the dictionary to remove
-            # old NULL entries
-            gcdata.dead_threads_count += 1
-            if (gcdata.dead_threads_count & 511) == 0:
-                copy = copy_without_null_values(gcdata.thread_stacks)
-                gcdata.thread_stacks.delete()
-                gcdata.thread_stacks = copy
-
-        def switch_shadow_stacks(new_aid):
-            save_away_current_stack()
-            install_new_stack(new_aid)
+                shadow_stack_pool.start_fresh_new_state()
+            # done
+            #
+            gcdata.active_tid = new_tid
         switch_shadow_stacks._dont_inline_ = True
 
-        def save_away_current_stack():
-            old_aid = gcdata.active_thread
-            # save root_stack_base on the top of the stack
-            self.push_stack(gcdata.root_stack_base)
-            # store root_stack_top into the dictionary
-            gcdata.thread_stacks.setitem(old_aid, gcdata.root_stack_top)
-
-        def install_new_stack(new_aid):
-            # look for the new stack top
-            top = gcdata.thread_stacks.get(new_aid, llmemory.NULL)
-            if top == llmemory.NULL:
-                # first time we see this thread.  It is an error if no
-                # fresh new stack is waiting.
-                base = gcdata._fresh_rootstack
-                gcdata._fresh_rootstack = llmemory.NULL
-                ll_assert(base != llmemory.NULL, "missing gc_thread_prepare")
-                gcdata.root_stack_top = base
-                gcdata.root_stack_base = base
-            else:
-                # restore the root_stack_base from the top of the stack
-                gcdata.root_stack_top = top
-                gcdata.root_stack_base = self.pop_stack()
-            # done
-            gcdata.active_thread = new_aid
-
-        def collect_stack(aid, stacktop, callback):
-            if stacktop != llmemory.NULL and aid != gcdata.active_thread:
-                # collect all valid stacks from the dict (the entry
-                # corresponding to the current thread is not valid)
-                gc = self.gc
-                rootstackhook = self.rootstackhook
-                end = stacktop - sizeofaddr
-                addr = end.address[0]
-                while addr != end:
-                    rootstackhook(callback, gc, addr)
-                    addr += sizeofaddr
-
-        def collect_more_stacks(callback):
-            ll_assert(get_aid() == gcdata.active_thread,
-                      "collect_more_stacks(): invalid active_thread")
-            gcdata.thread_stacks.foreach(collect_stack, callback)
-
-        def _free_if_not_current(aid, stacktop, _):
-            if stacktop != llmemory.NULL and aid != gcdata.active_thread:
-                end = stacktop - sizeofaddr
-                base = end.address[0]
-                llmemory.raw_free(base)
-
         def thread_after_fork(result_of_fork, opaqueaddr):
             # we don't need a thread_before_fork in this case, so
             # opaqueaddr == NULL.  This is called after fork().
@@ -209,28 +163,112 @@
                 # We are in the child process.  Assumes that only the
                 # current thread survived, so frees the shadow stacks
                 # of all the other ones.
-                gcdata.thread_stacks.foreach(_free_if_not_current, None)
-                # Clears the dict (including the current thread, which
-                # was an invalid entry anyway and will be recreated by
-                # the next call to save_away_current_stack()).
                 gcdata.thread_stacks.clear()
                 # Finally, reset the stored thread IDs, in case it
                 # changed because of fork().  Also change the main
                 # thread to the current one (because there is not any
                 # other left).
-                aid = get_aid()
-                gcdata.main_thread = aid
-                gcdata.active_thread = aid
+                tid = get_tid()
+                gcdata.main_tid = tid
+                gcdata.active_tid = tid
 
         self.thread_setup = thread_setup
-        self.thread_prepare_ptr = getfn(thread_prepare, [], annmodel.s_None)
         self.thread_run_ptr = getfn(thread_run, [], annmodel.s_None,
-                                    inline=True)
-        # no thread_start_ptr here
-        self.thread_die_ptr = getfn(thread_die, [], annmodel.s_None)
+                                    inline=True, minimal_transform=False)
+        self.thread_die_ptr = getfn(thread_die, [], annmodel.s_None,
+                                    minimal_transform=False)
         # no thread_before_fork_ptr here
         self.thread_after_fork_ptr = getfn(thread_after_fork,
                                            [annmodel.SomeInteger(),
                                             annmodel.SomeAddress()],
-                                           annmodel.s_None)
-        self.collect_stacks_from_other_threads = collect_more_stacks
+                                           annmodel.s_None,
+                                           minimal_transform=False)
+        self.has_thread_support = True
+
+# ____________________________________________________________
+
+class ShadowStackPool(object):
+    """Manages a pool of shadowstacks.  The MAX most recently used
+    shadowstacks are fully allocated and can be directly jumped into.
+    The rest are stored in a more virtual-memory-friendly way, i.e.
+    with just the right amount malloced.  Before they can run, they
+    must be copied into a full shadowstack.
+    """
+    _alloc_flavor_ = "raw"
+    root_stack_depth = 163840
+    root_stack_size = sizeofaddr * root_stack_depth
+
+    #MAX = 20  not implemented yet
+
+    def __init__(self, gcdata):
+        self.unused_full_stack = llmemory.NULL
+        self.gcdata = gcdata
+
+    def initial_setup(self):
+        self._prepare_unused_stack()
+        self.start_fresh_new_state()
+
+    def allocate(self):
+        """Allocate an empty SHADOWSTACKREF object."""
+        return lltype.malloc(SHADOWSTACKREF, zero=True)
+
+    def save_current_state_away(self, shadowstackref):
+        """Save the current state away into 'shadowstackref'.
+        This either works, or raise MemoryError and nothing is done.
+        To do a switch, first call save_current_state_away() or
+        forget_current_state(), and then call restore_state_from()
+        or start_fresh_new_state().
+        """
+        self._prepare_unused_stack()
+        shadowstackref.base = self.gcdata.root_stack_base
+        shadowstackref.top  = self.gcdata.root_stack_top
+        #shadowstackref.fullstack = True
+        llop.gc_assume_young_pointers(lltype.Void, shadowstackref)
+        self.gcdata.root_stack_top = llmemory.NULL  # to detect missing restore
+
+    def forget_current_state(self):
+        if self.unused_full_stack:
+            llmemory.raw_free(self.unused_full_stack)
+        self.unused_full_stack = self.gcdata.root_stack_base
+        self.gcdata.root_stack_top = llmemory.NULL  # to detect missing restore
+
+    def restore_state_from(self, shadowstackref):
+        self.gcdata.root_stack_base = shadowstackref.base
+        self.gcdata.root_stack_top  = shadowstackref.top
+        shadowstackref.base = llmemory.NULL
+        shadowstackref.top  = llmemory.NULL
+
+    def start_fresh_new_state(self):
+        self.gcdata.root_stack_base = self.unused_full_stack
+        self.gcdata.root_stack_top  = self.unused_full_stack
+        self.unused_full_stack = llmemory.NULL
+
+    def _prepare_unused_stack(self):
+        if self.unused_full_stack == llmemory.NULL:
+            self.unused_full_stack = llmemory.raw_malloc(self.root_stack_size)
+            if self.unused_full_stack == llmemory.NULL:
+                raise MemoryError
+
+
+SHADOWSTACKREFPTR = lltype.Ptr(lltype.GcForwardReference())
+SHADOWSTACKREF = lltype.GcStruct('ShadowStackRef',
+                                 ('base', llmemory.Address),
+                                 ('top', llmemory.Address),
+                                 #('fullstack', lltype.Bool),
+                                 rtti=True)
+SHADOWSTACKREFPTR.TO.become(SHADOWSTACKREF)
+NULL_SHADOWSTACKREF = lltype.nullptr(SHADOWSTACKREF)
+
+def customtrace(obj, prev):
+    # a simple but not JIT-ready version
+    if not prev:
+        prev = llmemory.cast_adr_to_ptr(obj, SHADOWSTACKREFPTR).top
+    if prev != llmemory.cast_adr_to_ptr(obj, SHADOWSTACKREFPTR).base:
+        return prev - sizeofaddr
+    else:
+        return llmemory.NULL
+
+CUSTOMTRACEFUNC = lltype.FuncType([llmemory.Address, llmemory.Address],
+                                  llmemory.Address)
+customtraceptr = llhelper(lltype.Ptr(CUSTOMTRACEFUNC), customtrace)
+lltype.attachRuntimeTypeInfo(SHADOWSTACKREF, customtraceptr=customtraceptr)


More information about the pypy-commit mailing list