Commits

Antonio Cuni committed 2c9ecfd Merge

merge the refactor-call_release_gil branch. This fixes a nasty bug which
occoured when JITting the call to a cffi function which calls a callback which
causes the failure of guard_not_forced: in that case, during blackholing we
got the wrong result from call_release_gil, because it was not passed to
fail_args.

The two tests which demonstrates the bug are
- rpython/jit/metainterp/test/test_fficall.py::test_guard_not_forced_fails
- pypy/module/pypyjit/test_pypy_c/test__ffi.py::test_cffi_call_guard_not_forced_fails

Comments (0)

Files changed (16)

pypy/module/pypyjit/test_pypy_c/test__ffi.py

         loops = log.loops_by_id('sleep')
         assert len(loops) == 1 # make sure that we actually JITted the loop
 
-
     def test_ctypes_call(self):
         from rpython.rlib.test.test_clibffi import get_libm_name
         def main(libm_name):
         # so far just check that call_release_gil() is produced.
         # later, also check that the arguments to call_release_gil()
         # are constants, and that the numerous raw_mallocs are removed
+
+    def test_cffi_call_guard_not_forced_fails(self):
+        # this is the test_pypy_c equivalent of
+        # rpython/jit/metainterp/test/test_fficall::test_guard_not_forced_fails
+        #
+        # it requires cffi to be installed for pypy in order to run
+        def main():
+            import sys
+            try:
+                import cffi
+            except ImportError:
+                sys.stderr.write('SKIP: cannot import cffi\n')
+                return 0
+                
+            ffi = cffi.FFI()
+
+            ffi.cdef("""
+            typedef void (*functype)(int);
+            int foo(int n, functype func);
+            """)
+
+            lib = ffi.verify("""
+            #include <signal.h>
+            typedef void (*functype)(int);
+
+            int foo(int n, functype func) {
+                if (n >= 2000) {
+                    func(n);
+                }
+                return n*2;
+            }
+            """)
+
+            @ffi.callback("functype")
+            def mycallback(n):
+                if n < 5000:
+                    return
+                # make sure that guard_not_forced fails
+                d = {}
+                f = sys._getframe()
+                while f:
+                    d.update(f.f_locals)
+                    f = f.f_back
+
+            n = 0
+            while n < 10000:
+                res = lib.foo(n, mycallback)  # ID: cfficall
+                # this is the real point of the test: before the
+                # refactor-call_release_gil branch, the assert failed when
+                # res == 5000
+                assert res == n*2
+                n += 1
+            return n
+
+        log = self.run(main, [], import_site=True)
+        assert log.result == 10000
+        loop, = log.loops_by_id('cfficall')
+        assert loop.match_by_id('cfficall', """
+            ...
+            f1 = call_release_gil(..., descr=<Calli 4 ii EF=6 OS=62>)
+            ...
+        """)

rpython/jit/backend/llgraph/runner.py

             # manipulation here (as a hack, instead of really doing
             # the aroundstate manipulation ourselves)
             return self.execute_call_may_force(descr, func, *args)
+        guard_op = self.lltrace.operations[self.current_index + 1]
+        assert guard_op.getopnum() == rop.GUARD_NOT_FORCED
+        self.force_guard_op = guard_op
         call_args = support.cast_call_args_in_order(descr.ARGS, args)
-        FUNC = lltype.FuncType(descr.ARGS, descr.RESULT)
-        func_to_call = rffi.cast(lltype.Ptr(FUNC), func)
-        result = func_to_call(*call_args)
+        #
+        func_adr = llmemory.cast_int_to_adr(func)
+        if hasattr(func_adr.ptr._obj, '_callable'):
+            # this is needed e.g. by test_fficall.test_guard_not_forced_fails,
+            # because to actually force the virtualref we need to llinterp the
+            # graph, not to directly execute the python function
+            result = self.cpu.maybe_on_top_of_llinterp(func, call_args, descr.RESULT)
+        else:
+            FUNC = lltype.FuncType(descr.ARGS, descr.RESULT)
+            func_to_call = rffi.cast(lltype.Ptr(FUNC), func)
+            result = func_to_call(*call_args)
+        del self.force_guard_op
         return support.cast_result(descr.RESULT, result)
 
     def execute_call_assembler(self, descr, *args):

rpython/jit/backend/llsupport/llmodel.py

     def cast_adr_to_int(x):
         return rffi.cast(lltype.Signed, x)
 
+    @staticmethod
+    def cast_int_to_ptr(x, TYPE):
+        return rffi.cast(TYPE, x)
+
     def sizeof(self, S):
         return get_size_descr(self.gc_ll_descr, S)
 

rpython/jit/backend/model.py

 import weakref
 from rpython.rlib.debug import debug_start, debug_print, debug_stop
-from rpython.rtyper.lltypesystem import lltype
+from rpython.rtyper.lltypesystem import lltype, llmemory
 
 class CPUTotalTracker(object):
     total_compiled_loops = 0
     def typedescrof(self, TYPE):
         raise NotImplementedError
 
+    @staticmethod
+    def cast_int_to_ptr(x, TYPE):
+        x = llmemory.cast_int_to_adr(x)
+        return llmemory.cast_adr_to_ptr(x, TYPE)
+
     # ---------- the backend-dependent operations ----------
 
     # lltype specific operations

rpython/jit/backend/x86/test/test_fficall.py

 class TestFfiCall(Jit386Mixin, test_fficall.FfiCallTests):
     # for the individual tests see
     # ====> ../../../metainterp/test/test_fficall.py
-    pass
+
+    def _add_libffi_types_to_ll2types_maybe(self):
+        # this is needed by test_guard_not_forced_fails, because it produces a
+        # loop which reads the value of types.* in a variable, then a guard
+        # fail and we switch to blackhole: the problem is that at this point
+        # the blackhole interp has a real integer, but it needs to convert it
+        # back to a lltype pointer (which is handled by ll2ctypes, deeply in
+        # the logic). The workaround is to teach ll2ctypes in advance which
+        # are the addresses of the various types.* structures.
+        # Try to comment this code out and run the test to see how it fails :)
+        from rpython.rtyper.lltypesystem import rffi, lltype, ll2ctypes
+        from rpython.rlib.jit_libffi import types
+        for key, value in types.__dict__.iteritems():
+            if isinstance(value, lltype._ptr):
+                addr = rffi.cast(lltype.Signed, value)
+                ll2ctypes._int2obj[addr] = value

rpython/jit/codewriter/jtransform.py

             assert False, 'unsupported oopspec: %s' % oopspec_name
         return self._handle_oopspec_call(op, args, oopspecindex, extraeffect)
 
+    def rewrite_op_jit_ffi_save_result(self, op):
+        kind = op.args[0].value
+        assert kind in ('int', 'float')
+        return SpaceOperation('libffi_save_result_%s' % kind, op.args[1:], None)
+
     def rewrite_op_jit_force_virtual(self, op):
         return self._do_builtin_call(op)
 

rpython/jit/metainterp/blackhole.py

 from rpython.rlib.rarithmetic import intmask, LONG_BIT, r_uint, ovfcheck
 from rpython.rlib.rtimer import read_timestamp
 from rpython.rlib.unroll import unrolling_iterable
-from rpython.rtyper.lltypesystem import lltype, llmemory, rclass
+from rpython.rtyper.lltypesystem import lltype, llmemory, rclass, rffi
 from rpython.rtyper.lltypesystem.lloperation import llop
+from rpython.rlib.jit_libffi import CIF_DESCRIPTION_P
 
 
 def arguments(*argtypes, **kwds):
     def bhimpl_ll_read_timestamp():
         return read_timestamp()
 
+    @arguments("cpu", "i", "i", "i")
+    def bhimpl_libffi_save_result_int(self, cif_description, exchange_buffer, result):
+        ARRAY = lltype.Ptr(rffi.CArray(lltype.Signed))
+        cif_description = self.cast_int_to_ptr(cif_description, CIF_DESCRIPTION_P)
+        exchange_buffer = self.cast_int_to_ptr(exchange_buffer, rffi.CCHARP)
+        #
+        data_out = rffi.ptradd(exchange_buffer, cif_description.exchange_result)
+        rffi.cast(ARRAY, data_out)[0] = result
+
+    @arguments("cpu", "i", "i", "f")
+    def bhimpl_libffi_save_result_float(self, cif_description, exchange_buffer, result):
+        ARRAY = lltype.Ptr(rffi.CArray(lltype.Float))
+        cif_description = self.cast_int_to_ptr(cif_description, CIF_DESCRIPTION_P)
+        exchange_buffer = self.cast_int_to_ptr(exchange_buffer, rffi.CCHARP)
+        #
+        data_out = rffi.ptradd(exchange_buffer, cif_description.exchange_result)
+        rffi.cast(ARRAY, data_out)[0] = result
+
+
     # ----------
     # helpers to resume running in blackhole mode when a guard failed
 

rpython/jit/metainterp/history.py

 
     def show(self, errmsg=None):
         "NOT_RPYTHON"
-        from rpython.jit.metainterp.graphpage import display_loops
-        display_loops([self], errmsg)
+        from rpython.jit.metainterp.graphpage import display_procedures
+        display_procedures([self], errmsg)
 
     def check_consistency(self):     # for testing
         "NOT_RPYTHON"

rpython/jit/metainterp/pyjitpl.py

     def opimpl_ll_read_timestamp(self):
         return self.metainterp.execute_and_record(rop.READ_TIMESTAMP, None)
 
+    @arguments("box", "box", "box")
+    def opimpl_libffi_save_result_int(self, box_cif_description, box_exchange_buffer,
+                                      box_result):
+        from rpython.rtyper.lltypesystem import llmemory
+        from rpython.rlib.jit_libffi import CIF_DESCRIPTION_P
+        from rpython.jit.backend.llsupport.ffisupport import get_arg_descr
+
+        cif_description = box_cif_description.getint()
+        cif_description = llmemory.cast_int_to_adr(cif_description)
+        cif_description = llmemory.cast_adr_to_ptr(cif_description,
+                                                   CIF_DESCRIPTION_P)
+
+        kind, descr, itemsize = get_arg_descr(self.metainterp.cpu, cif_description.rtype)
+        
+        if kind != 'v':
+            ofs = cif_description.exchange_result
+            assert ofs % itemsize == 0     # alignment check (result)
+            self.metainterp.history.record(rop.SETARRAYITEM_RAW,
+                                           [box_exchange_buffer,
+                                            ConstInt(ofs // itemsize), box_result],
+                                           None, descr)
+
+    opimpl_libffi_save_result_float = opimpl_libffi_save_result_int
+
     # ------------------------------
 
     def setup_call(self, argboxes):
                                 box_arg, descr)
             arg_boxes.append(box_arg)
         #
-        kind, descr, itemsize = get_arg_descr(self.cpu, cif_description.rtype)
-        if kind == 'i':
-            box_result = history.BoxInt()
-        elif kind == 'f':
-            box_result = history.BoxFloat()
-        else:
-            assert kind == 'v'
-            box_result = None
+        box_result = op.result
         self.history.record(rop.CALL_RELEASE_GIL,
                             [op.getarg(2)] + arg_boxes,
                             box_result, calldescr)
         #
         self.history.operations.extend(extra_guards)
         #
-        if box_result is not None:
-            ofs = cif_description.exchange_result
-            assert ofs % itemsize == 0     # alignment check (result)
-            self.history.record(rop.SETARRAYITEM_RAW,
-                                [box_exchange_buffer,
-                                 ConstInt(ofs // itemsize), box_result],
-                                None, descr)
+        # note that the result is written back to the exchange_buffer by the
+        # special op libffi_save_result_{int,float}
 
     def direct_call_release_gil(self):
         op = self.history.operations.pop()

rpython/jit/metainterp/test/test_fficall.py

-
 import py
+from _pytest.monkeypatch import monkeypatch
 import ctypes, math
 from rpython.rtyper.lltypesystem import lltype, rffi
+from rpython.rtyper.annlowlevel import llhelper
 from rpython.jit.metainterp.test.support import LLJitMixin
 from rpython.rlib import jit
-from rpython.rlib.jit_libffi import types, CIF_DESCRIPTION, FFI_TYPE_PP
+from rpython.rlib import jit_libffi
+from rpython.rlib.jit_libffi import (types, CIF_DESCRIPTION, FFI_TYPE_PP,
+                                     jit_ffi_call, jit_ffi_save_result)
 from rpython.rlib.unroll import unrolling_iterable
 from rpython.rlib.rarithmetic import intmask
 
-
 def get_description(atypes, rtype):
     p = lltype.malloc(CIF_DESCRIPTION, len(atypes),
                       flavor='raw', immortal=True)
         p.atypes[i] = atypes[i]
     return p
 
+class FakeFFI(object):
+    """
+    Context manager to monkey patch jit_libffi with our custom "libffi-like"
+    function
+    """
+    
+    def __init__(self, fake_call_impl_any):
+        self.fake_call_impl_any = fake_call_impl_any
+        self.monkey = monkeypatch()
+        
+    def __enter__(self, *args):
+        self.monkey.setattr(jit_libffi, 'jit_ffi_call_impl_any', self.fake_call_impl_any)
+
+    def __exit__(self, *args):
+        self.monkey.undo()
+
 
 class FfiCallTests(object):
 
 
         unroll_avalues = unrolling_iterable(avalues)
 
-        @jit.oopspec("libffi_call(cif_description,func_addr,exchange_buffer)")
-        def fake_call(cif_description, func_addr, exchange_buffer):
+        def fake_call_impl_any(cif_description, func_addr, exchange_buffer):
             ofs = 16
             for avalue in unroll_avalues:
                 TYPE = rffi.CArray(lltype.typeOf(avalue))
                 rffi.cast(lltype.Ptr(TYPE), data)[0] = avalue
                 ofs += 16
 
-            fake_call(cif_description, func_addr, exbuf)
+            jit_ffi_call(cif_description, func_addr, exbuf)
 
             if rvalue is None:
                 res = 654321
             lltype.free(exbuf, flavor='raw')
             return res
 
-        res = f()
-        assert res == rvalue or (res, rvalue) == (654321, None)
-        res = self.interp_operations(f, [])
-        assert res == rvalue or (res, rvalue) == (654321, None)
-        self.check_operations_history(call_may_force=0,
-                                      call_release_gil=1)
+        with FakeFFI(fake_call_impl_any):
+            res = f()
+            assert res == rvalue or (res, rvalue) == (654321, None)
+            res = self.interp_operations(f, [])
+            assert res == rvalue or (res, rvalue) == (654321, None)
+            self.check_operations_history(call_may_force=0,
+                                          call_release_gil=1)
 
-    def test_simple_call(self):
+    def test_simple_call_int(self):
         self._run([types.signed] * 2, types.signed, [456, 789], -42)
 
     def test_many_arguments(self):
         self._run([types.signed], types.sint8, [456],
                   rffi.cast(rffi.SIGNEDCHAR, -42))
 
+    def _add_libffi_types_to_ll2types_maybe(self):
+        # not necessary on the llgraph backend, but needed for x86.
+        # see rpython/jit/backend/x86/test/test_fficall.py
+        pass
+
+    def test_guard_not_forced_fails(self):
+        self._add_libffi_types_to_ll2types_maybe()
+        FUNC = lltype.FuncType([lltype.Signed], lltype.Signed)
+
+        cif_description = get_description([types.slong], types.slong)
+        cif_description.exchange_args[0] = 16
+        cif_description.exchange_result = 32
+
+        ARRAY = lltype.Ptr(rffi.CArray(lltype.Signed))
+
+        @jit.dont_look_inside
+        def fn(n):
+            if n >= 50:
+                exctx.m = exctx.topframeref().n # forces the frame
+            return n*2
+
+        # this function simulates what a real libffi_call does: reading from
+        # the buffer, calling a function (which can potentially call callbacks
+        # and force frames) and write back to the buffer
+        def fake_call_impl_any(cif_description, func_addr, exchange_buffer):
+            # read the args from the buffer
+            data_in = rffi.ptradd(exchange_buffer, 16)
+            n = rffi.cast(ARRAY, data_in)[0]
+            #
+            # logic of the function
+            func_ptr = rffi.cast(lltype.Ptr(FUNC), func_addr)
+            n = func_ptr(n)
+            #
+            # write the result to the buffer
+            data_out = rffi.ptradd(exchange_buffer, 32)
+            rffi.cast(ARRAY, data_out)[0] = n
+
+        def do_call(n):
+            func_ptr = llhelper(lltype.Ptr(FUNC), fn)
+            exbuf = lltype.malloc(rffi.CCHARP.TO, 48, flavor='raw', zero=True)
+            data_in = rffi.ptradd(exbuf, 16)
+            rffi.cast(ARRAY, data_in)[0] = n
+            jit_ffi_call(cif_description, func_ptr, exbuf)
+            data_out = rffi.ptradd(exbuf, 32)
+            res = rffi.cast(ARRAY, data_out)[0]
+            lltype.free(exbuf, flavor='raw')
+            return res
+
+        #
+        #
+        class XY:
+            pass
+        class ExCtx:
+            pass
+        exctx = ExCtx()
+        myjitdriver = jit.JitDriver(greens = [], reds = ['n'])
+        def f():
+            n = 0
+            while n < 100:
+                myjitdriver.jit_merge_point(n=n)
+                xy = XY()
+                xy.n = n
+                exctx.topframeref = vref = jit.virtual_ref(xy)
+                res = do_call(n) # this is equivalent of a cffi call which
+                                 # sometimes forces a frame
+
+                # when n==50, fn() will force the frame, so guard_not_forced
+                # fails and we enter blackholing: this test makes sure that
+                # the result of call_release_gil is kept alive before the
+                # libffi_save_result, and that the corresponding box is passed
+                # in the fail_args. Before the fix, the result of
+                # call_release_gil was simply lost and when guard_not_forced
+                # failed, and the value of "res" was unpredictable.
+                # See commit b84ff38f34bd and subsequents.
+                assert res == n*2
+                jit.virtual_ref_finish(vref, xy)
+                exctx.topframeref = jit.vref_None
+                n += 1
+            return n
+
+        with FakeFFI(fake_call_impl_any):
+            assert f() == 100
+            res = self.meta_interp(f, [])
+            assert res == 100
+        
 
 class TestFfiCall(FfiCallTests, LLJitMixin):
     def test_jit_ffi_vref(self):

rpython/rlib/jit_libffi.py

 
 from rpython.rtyper.lltypesystem import lltype, rffi
+from rpython.rtyper.extregistry import ExtRegistryEntry
 from rpython.rlib import clibffi, jit
+from rpython.rlib.nonconst import NonConstant
 
 
 FFI_CIF = clibffi.FFI_CIFP.TO
     return rffi.cast(lltype.Signed, res)
 
 
-@jit.oopspec("libffi_call(cif_description, func_addr, exchange_buffer)")
+# =============================
+# jit_ffi_call and its helpers
+# =============================
+
+## Problem: jit_ffi_call is turned into call_release_gil by pyjitpl. Before
+## the refactor-call_release_gil branch, the resulting code looked like this:
+##
+##     buffer = ...
+##     i0 = call_release_gil(...)
+##     guard_not_forced()
+##     setarray_item_raw(buffer, ..., i0)
+##
+## The problem is that the result box i0 was generated freshly inside pyjitpl,
+## and the codewriter did not know about its liveness: the result was that i0
+## was not in the fail_args of guard_not_forced. See
+## test_fficall::test_guard_not_forced_fails for a more detalied explanation
+## of the problem.
+##
+## The solution is to create a new separate operation libffi_save_result whose
+## job is to write the result in the exchange_buffer: during normal execution
+## this is a no-op because the buffer is already filled by libffi, but during
+## jitting the behavior is to actually write into the buffer.
+##
+## The result is that now the jitcode looks like this:
+##
+##     %i0 = libffi_call_int(...)
+##     -live-
+##     libffi_save_result_int(..., %i0)
+##
+## the "-live-" is the key, because it make sure that the value is not lost if
+## guard_not_forced fails.
+
+
 def jit_ffi_call(cif_description, func_addr, exchange_buffer):
     """Wrapper around ffi_call().  Must receive a CIF_DESCRIPTION_P that
     describes the layout of the 'exchange_buffer'.
     """
+    if cif_description.rtype == types.void:
+        jit_ffi_call_impl_void(cif_description, func_addr, exchange_buffer)
+    elif cif_description.rtype == types.double:
+        result = jit_ffi_call_impl_float(cif_description, func_addr, exchange_buffer)
+        jit_ffi_save_result('float', cif_description, exchange_buffer, result)
+    else:
+        result = jit_ffi_call_impl_int(cif_description, func_addr, exchange_buffer)
+        jit_ffi_save_result('int', cif_description, exchange_buffer, result)
+
+
+# we must return a NonConstant else we get the constant -1 as the result of
+# the flowgraph, and the codewriter does not produce a box for the
+# result. Note that when not-jitted, the result is unused, but when jitted the
+# box of the result contains the actual value returned by the C function.
+
+@jit.oopspec("libffi_call(cif_description,func_addr,exchange_buffer)")
+def jit_ffi_call_impl_int(cif_description, func_addr, exchange_buffer):
+    jit_ffi_call_impl_any(cif_description, func_addr, exchange_buffer)
+    return NonConstant(-1)
+
+@jit.oopspec("libffi_call(cif_description,func_addr,exchange_buffer)")
+def jit_ffi_call_impl_float(cif_description, func_addr, exchange_buffer):
+    jit_ffi_call_impl_any(cif_description, func_addr, exchange_buffer)
+    return NonConstant(-1.0)
+
+@jit.oopspec("libffi_call(cif_description,func_addr,exchange_buffer)")
+def jit_ffi_call_impl_void(cif_description, func_addr, exchange_buffer):
+    jit_ffi_call_impl_any(cif_description, func_addr, exchange_buffer)
+    return None
+
+def jit_ffi_call_impl_any(cif_description, func_addr, exchange_buffer):
+    """
+    This is the function which actually calls libffi. All the rest if just
+    infrastructure to convince the JIT to pass a typed result box to
+    jit_ffi_save_result
+    """
     buffer_array = rffi.cast(rffi.VOIDPP, exchange_buffer)
     for i in range(cif_description.nargs):
         data = rffi.ptradd(exchange_buffer, cif_description.exchange_args[i])
     clibffi.c_ffi_call(cif_description.cif, func_addr,
                        rffi.cast(rffi.VOIDP, resultdata),
                        buffer_array)
+    return -1
+
+
+
+def jit_ffi_save_result(kind, cif_description, exchange_buffer, result):
+    """
+    This is a no-op during normal execution, but actually fills the buffer
+    when jitted
+    """
+    pass
+
+class Entry(ExtRegistryEntry):
+    _about_ = jit_ffi_save_result
+
+    def compute_result_annotation(self, kind_s, *args_s):
+        from rpython.annotator import model as annmodel
+        assert isinstance(kind_s, annmodel.SomeString)
+        assert kind_s.const in ('int', 'float')
+
+    def specialize_call(self, hop):
+        hop.exception_cannot_occur()
+        vlist = hop.inputargs(lltype.Void, *hop.args_r[1:])
+        return hop.genop('jit_ffi_save_result', vlist,
+                         resulttype=lltype.Void)
+    
 
 # ____________________________________________________________
 

rpython/rtyper/lltypesystem/lloperation.py

     'jit_is_virtual':       LLOp(canrun=True),
     'jit_force_quasi_immutable': LLOp(canrun=True),
     'jit_record_known_class'  : LLOp(canrun=True),
+    'jit_ffi_save_result':  LLOp(canrun=True),
     'get_exception_addr':   LLOp(),
     'get_exc_value_addr':   LLOp(),
     'do_malloc_fixedsize_clear':LLOp(canmallocgc=True),

rpython/rtyper/lltypesystem/opimpl.py

 def op_jit_record_known_class(x, y):
     pass
 
+def op_jit_ffi_save_result(*args):
+    pass
+
 def op_get_group_member(TYPE, grpptr, memberoffset):
     from rpython.rtyper.lltypesystem import llgroup
     assert isinstance(memberoffset, llgroup.GroupMemberOffset)

rpython/translator/c/funcgen.py

     def OP_JIT_FORCE_QUASI_IMMUTABLE(self, op):
         return '/* JIT_FORCE_QUASI_IMMUTABLE %s */' % op
 
+    def OP_JIT_FFI_SAVE_RESULT(self, op):
+        return '/* JIT_FFI_SAVE_RESULT %s */' % op
+
     def OP_GET_GROUP_MEMBER(self, op):
         typename = self.db.gettype(op.result.concretetype)
         return '%s = (%s)_OP_GET_GROUP_MEMBER(%s, %s);' % (

rpython/translator/cli/opcodes.py

     'jit_force_virtual':        DoNothing,
     'jit_force_quasi_immutable':Ignore,
     'jit_is_virtual':           [PushPrimitive(ootype.Bool, False)],
+    'jit_ffi_save_result':      Ignore,
     }
 
 # __________ numeric operations __________

rpython/translator/jvm/opcodes.py

     'jit_force_virtual':        DoNothing,
     'jit_force_quasi_immutable': Ignore,
     'jit_is_virtual':           PushPrimitive(ootype.Bool, False),
+    'jit_ffi_save_result':      Ignore,
 
     'debug_assert':              [], # TODO: implement?
     'debug_start_traceback':    Ignore,