Repository: pyodide/pyodide

Issue:
fpcast emulation tracing and removing
We currently build with function pointer cast emulation. It is really hard to remove this because it is hard to know where bad function calls are being made (i.e. where an indirect call is made where the call arguments don't match the target function arguments).

I would suggest that what is needed is some kind of fpcast_emulation with tracing build, then run the full test suite on that, hopefully it would hit the majority of the function pointer casting shenanigans. In theory this was fixed in python in 3.8, so we should be good with core python, but we do bundle some other stuff for images and fonts etc. that might cause issues. The patches to scipy/numpy removed most issues there too, but there may be some that I missed. 

I suspect the easiest way to do this is to make a build which in emscripten replaces indirect calls to a call to a python indirect call trampoline, so that rather than call to fpcast versions, it calls to something in imports which is javascript and does the actual call. I think that side of things would need a change to visitCallIndirect in the binaryen pass to just make it call the javascript trampoline with the signature of the indirect call as a parameter. Then the fpcast trampoline could check that against the signature of what it was calling (using the same , and chuck out a log message if they didn't match. This would be way more efficient if webassembly type reflection was enabled otherwise some manual type reflection cunningness would be needed to get the type of the target. Type reflection is behind a flag in chromium (enable experimental webassembly features). 

I imagine it would be slow as a slow thing, but for testing it would do the job I imagine.

I would suggest it isn't much work to update binaryen to call the trampoline, then creating a build that works in chromium with webassembly reflection enabled. It might be that if one can get pyodide itself loading in chromium and run the test suite, that is enough to exercise the code enough to find out what is the cause of the naughty pointer calls and sort it.

I won't have time to do anything till teaching and marking ends (i.e. june/july at earliest), but if anyone wants to do it, I'm happy to input.


Discussion:
Thanks for opening this issue with a detailed plan @joemarshall !

Personally I also won't have much availability to look into this in the near future, but I very much look forward to getting rid of fpcast emulation. It should also help for Python performance #1120  as far as I understand.
I had a tiny fiddle with this - interestingly pyodide itself now loads, I can run code in the JS console before the python console.html loads, but I can't run anything in the console without hitting a failure somewhere in jsproxy.

Latest chrome canary plus webassembly debugging however means that
a) you get the exception about null pointer or wrong pointer type
b) you actually can get the code lines and stuff that is failing in the C code now. 

Something is broken in python waiting for js promises. I'll have a peek at how that works.

Good news is that it is now debuggable without having to do the logging thing...
Aha, python core seems to mostly work nowadays, but in jsproxy / asyncio it breaks, calling asyncio.ensure_future on a javascript promise doesn't work and causes a crash that I will describe below:

Firstly, a fail case - if I stick this in javascript console, I can reproduce it without having to step through shed loads of console stuff. Just load console.html as normal, then run this in the js console before you type anything in the web page. I think something in jsproxy has the wrong function signature.

```
p=Promise.resolve('Great')
pyodide.runPython('import js,asyncio\nprint(js.p)\nasyncio.ensure_future(js.p)\n')
```

```
Uncaught RuntimeError: null function or function signature mismatch
    at wrap_unaryfunc (typeobject.c:5661)
    at wrapperdescr_raw_call (descrobject.c:483)
    at wrapper_call (descrobject.c:1303)
    at _PyObject_MakeTpCall (call.c:159)
    at _PyObject_Vectorcall (abstract.h:125)
    at call_function (ceval.c:4999)
    at _PyEval_EvalFrameDefault (ceval.c:3475)
    at PyEval_EvalFrameEx (ceval.c:741)
    at gen_send_ex (genobject.c:222)
    at _PyGen_Send (genobject.c:292)
```


Running a python trace (see below), it is hitting the failure within the python code here:
https://github.com/python/cpython/blob/v3.8.2/Lib/asyncio/tasks.py#L684


```
pyodide.runPython("import sys\ndef tf(a,b,c):\n  print(a,b,c)\n  return tf\nsys.settrace(tf)\n")
```

```
<frame at 0xc75100, file '/lib/python3.8/asyncio/events.py', line 79, code _run> call None
pyodide.asm.js:3708 <frame at 0xc75100, file '/lib/python3.8/asyncio/events.py', line 80, code _run> line None
pyodide.asm.js:3708 <frame at 0xc75100, file '/lib/python3.8/asyncio/events.py', line 81, code _run> line None
pyodide.asm.js:3708 <frame at 0xc9f3e8, file '/lib/python3.8/asyncio/tasks.py', line 263, code __step> call None
pyodide.asm.js:3708 <frame at 0xc9f3e8, file '/lib/python3.8/asyncio/tasks.py', line 264, code __step> line None
pyodide.asm.js:3708 <frame at 0xca00f0, file '/lib/python3.8/asyncio/futures.py', line 157, code done> call None
pyodide.asm.js:3708 <frame at 0xca00f0, file '/lib/python3.8/asyncio/futures.py', line 163, code done> line None
pyodide.asm.js:3708 <frame at 0xca00f0, file '/lib/python3.8/asyncio/futures.py', line 163, code done> return False
pyodide.asm.js:3708 <frame at 0xc9f3e8, file '/lib/python3.8/asyncio/tasks.py', line 267, code __step> line None
pyodide.asm.js:3708 <frame at 0xc9f3e8, file '/lib/python3.8/asyncio/tasks.py', line 271, code __step> line None
pyodide.asm.js:3708 <frame at 0xc9f3e8, file '/lib/python3.8/asyncio/tasks.py', line 272, code __step> line None
pyodide.asm.js:3708 <frame at 0xc9f3e8, file '/lib/python3.8/asyncio/tasks.py', line 274, code __step> line None
pyodide.asm.js:3708 <frame at 0xc75280, file '/lib/python3.8/asyncio/tasks.py', line 929, code _enter_task> call None
pyodide.asm.js:3708 <frame at 0xc75280, file '/lib/python3.8/asyncio/tasks.py', line 930, code _enter_task> line None
pyodide.asm.js:3708 <frame at 0xc75280, file '/lib/python3.8/asyncio/tasks.py', line 931, code _enter_task> line None
pyodide.asm.js:3708 <frame at 0xc75280, file '/lib/python3.8/asyncio/tasks.py', line 934, code _enter_task> line None
pyodide.asm.js:3708 <frame at 0xc75280, file '/lib/python3.8/asyncio/tasks.py', line 934, code _enter_task> return None
pyodide.asm.js:3708 <frame at 0xc9f3e8, file '/lib/python3.8/asyncio/tasks.py', line 276, code __step> line None
pyodide.asm.js:3708 <frame at 0xc9f3e8, file '/lib/python3.8/asyncio/tasks.py', line 277, code __step> line None
pyodide.asm.js:3708 <frame at 0xc9f3e8, file '/lib/python3.8/asyncio/tasks.py', line 280, code __step> line None
pyodide.asm.js:3708 <frame at 0x89f358, file '/lib/python3.8/asyncio/tasks.py', line 677, code _wrap_awaitable> call None
pyodide.asm.js:3708 <frame at 0x89f358, file '/lib/python3.8/asyncio/tasks.py', line 684, code _wrap_awaitable> line None
```
Aha,

```
slots[cur_slot++] =
      (PyType_Slot){ .slot = Py_am_await, .pfunc = (void*)JsProxy_Await };
```
but JsProxy_Await needs to be a unaryfunction to go in the Py_am_await typeslot

https://docs.python.org/3/c-api/typeobj.html#c.unaryfunc

It doesn't use args, so maybe just a typo.
oh yeah, core python works (or at least the console appears to run okay) if I fix that, even with fpcast turned off! That's cool.
That great news! cc @hoodmane regarding the `JsProxy_Await` function signature.

Does it mean that we could be build CPython at least without `EMULATE_FUNCTION_POINTER_CASTS` or do we need to build all either with or without?
We need to build everything with or without. Bad things will happen otherwise.

There's also a patch needed in cpython with dictionaries which makes the tests fail right now. Can't reverse the items or values in a dictionary, there are casts in dictobject between for a function taking one parameter to a pycfunction which should take two args and ignore the last. It is fixed for reversing the dict keys, but not for the items. Needs fixing in upstream python too.
> Needs fixing in upstream python too.

Hmm, this might be a good occasion to migrate to Python 3.9 as well https://github.com/pyodide/pyodide/issues/978.
> It doesn't use args, so maybe just a typo.

I got confused because `METH_NOARGS` functions are supposed to take an argument that they ignore.
Yeah, that's the same issue in cpython dict. I've pushed that fix to python core (https://bugs.python.org/issue44114), and it is going to be backported to 3.9 but not 3.8, so will still need a patch in pyodide until it works with 3.9. Tests for core python pass with this patch.

Still crashes on import of quite a few packages though. There are a bunch of similar errors in some of the C packages. They're quite easy to find, but it is going to be a bit whack-a-mole to find them all. Numpy for example has some multiarray getters which ignore extra void* parameters.

Incidentally, I think there would be value in trying to get cpython maintainers to add CI for webassembly into core cpython testing, so that core python at least doesn't rely on undefined c behaviour. I don't know how one does that though, I only know how to push little patches to python.
> They're quite easy to find, but it is going to be a bit whack-a-mole to find them all.
> [..]
> Incidentally, I think there would be value in trying to get cpython maintainers to add CI for webassembly into core cpython testing, so that core python at least doesn't rely on undefined c behaviour

GCC has an option to warn on invalid function casts `-Wcast-function-type`, wouldn't that be sufficient for this issue? That's how they detected the issue initially in CPython https://bugs.python.org/issue33012 

Asking to compile with `-Werror=cast-function-type` would be much easier than setting up a WASM build, particularly for all other packages. For instance numpy built with `CFLAGS="-Werror=cast-function-type" python setup.py install` allows to detect some of those errors I think.

> We need to build everything with or without. Bad things will happen otherwise.

Could you elaborate on why? We can fix CPython and the core packages, but my concern is how to make sure things won't break if users compile some third party package themselves. So if anything could be done to improve this situation in emscripten it would likely be easier than trying to fix the whole Python ecosystem.
> to add CI for webassembly into core cpython

https://bugs.python.org/issue40280 would probably be a first step
I thought `-Werror=cast-function-type` would do the job. Two things don't work with it though -

1) cpython relies on incorrect function casts being cool anywhere it passes a python function, e.g. PyMethodDef.ml_meth is defined as a PyCFunction, but can actually be: _PyCFunctionFast, PyCMethod, PyCFunctionWithKeywords etc. So you basically can't make a python module without doing at least some naughty casting behaviour right now.
 
2) I don't think it works in emsdk, at least I can build things with it turned on and bad things still happen. I think -werror=blah is a bit flaky in emcc, but I can't work out why well enough to put a bug report anywhere (llvm? emscripten?)
From https://bugs.python.org/issue33012 this needs fixing before anything else. It would need care to make sure that all c python modules didn't need updating though I think, so I don't see it happening soon...

> This is not fixing the underlying issue but hiding it. The right fix would be to use a union for ml_meth providing members for the 3 different function. So the developer could assign them correctly and the compiler would warn if he would do something wrong. Casting to (void *) is just hiding the problem not fixing it!
Oh, and the reason fpcast is an all or nothing deal is because it changes the core linking of emscripten modules. If fpcast is on, any time you make a function pointer, it will point to the fpcast version, and any calls to function pointers will expect the signature to be the fixed parameter fpcast version. So you won't be able to pass function pointers between non-fpcast and fpcast modules, which means python modules have to be built with the same mode as the core python.
Hey,

This branch here builds with fpcast turned off, and in chrome and firefox it even loads numpy alright and you can do things with a numpy array. All tests for numpy pass in firefox also. 

Module loading is faster.

I had to up the memory to make it work, now it is 41mb. I'm not 100% sure if that is because of all the debug settings I have on.

https://github.com/joemarshall/pyodide/tree/fpcast

Things that don't work:
1) If I turn wasm_bigint on, bad things happen. Not sure why. Could be a memory thing.
2) Chrome tests fail with memory errors. Not sure if it just needs even more memory (I already doubled it), this might be because I have all sorts of debugging things turned on, I will try a non-debug build to see.
Loading numpy in completely release build with no fpcast, total time for loadpackage plus "import numpy" is 1.8 seconds.

Loading it in the old build on the same machine, approx 10 seconds.
That's great @joemarshall !

>  Could be a memory thing.

Yes, I also encountered a a few more errors in the test suite with the debug build in the past, so it might not be representative.

For the numpy patch,

> This isn't used in numpy but needs to be there even if it is ignored.

So that's not something that could be contributed upstream? I happens if numpy is built with gcc using that patch. I can check tomorrow.

> It would need care to make sure that all c python modules didn't need updating though I think, so I don't see it happening soon...

Breaking C Python modules would indeed likely be a no go. But so in the end it's not an issue since you managed to make it work?

> Loading numpy in completely release build with no fpcast, total time for loadpackage plus "import numpy" is 1.8 seconds.
>
> Loading it in the old build on the same machine, approx 10 seconds.

Wow, that huge progress! I'll try to experiment a bit more with your branch later this week. 
The numpy patch I think could go upstream, what I meant there is that the argument needs to be on the function even if nothing ever uses it.

I still see weird memory errors in chrome tests in release build, even on things that look to work fine in the browser. It is weird, because I would expect fpcast errors to make complete crashes not memory errors, so I suspect it is something to do with how the compile happens differently without fpcast that means that things need more memory somehow (and maybe there is a selenium setting relating to memory that I don't know about?).
I've chucked a pull request into numpy now for that getter patch. 

I rebuilt without debug and with -O3 rather than -Os and it happily runs numpy tests. 

Things that fail:
matplotlib
optlib (chrome only)
scipy
sklearn
pytest
pywavelets (chrome only)
zarr (chrome only)






The big problem is scipy and fortran. F2C generates a lot of code that looks like: `int dpvd_(S_fp fcn, integer *n, integer *m, integer *np,)`
using types from f2c.h like S_fp, which are defined as `typedef /* Subroutine */ int (*S_fp)();` i.e. without arguments, then called with whatever arguments are required.

There is an option behind an ifdef __cplusplus to define them as varargs functions, like so:
```
#ifdef __cplusplus
typedef int /* Unknown procedure type */ (*U_fp)(...);
```
I don't know what if anything is ever calling these - i.e. whether they're exposed from python at all, and how it handles function pointers.

There is one thing in ARPACK which causes python to fail, the debug and timing structures are initialized using one of these pointers, it fails in `f2pyinitdebug_` in `packages/scipy/build/scipy-0.17.1/build/src.linux-x86_64-3.8/build/src.linux-x86_64-3.8/scipy/sparse/linalg/eigen/arpack/_arpack-f2pywrappers.c`, because it passes in something with arguments casted to an (*int)(), then calls that with a load of arguments. I think changing f2py could fix that one. It may be that these function pointer methods aren't actually called by anything python, but I don't quite know.
Aha, I fixed those, was just the return value. Because it's c, if happily makes up the parameters to the call okay. 50% of scipy tests work now. Something not working in scipy linalg tests still, but probably fixable.
Aww, that's great news! Thanks for your work on this @joemarshall !

For scipy, it might be also worth considering updating the version before doing much more work on fixing function signatures there. I started to work on it in https://github.com/pyodide/pyodide/pull/1293 but it's a lot of patches to migrate. It's a bit orthogonal, but adding more scipy patches is also not going to make that work easier.
It turned out to be an incorrect signature in numpy actually. Now that is fixed, most of the tests pass. 

matplotlib still fails with incorrect function signatures, as does test_pytest and test_skimage, but the rest of the packages passed tests. 

I have updated it on my server, but my students mostly do quite simple things, so aren't going to stress test things.

I think that it *might* be worth pushing this across to main pyodide, but on the other hand there is a risk that it makes odd errors happen in packages that aren't caught by the tests, especially in big packages that don't have high test coverage.
Great! We are planning a 0.18 release end of July and I think it might be a bit short to merge it and test it extensively for this release. I would be +1 to merge it just after the release and include it in 0.19. We can do alpha releases in between 0.18 and 0.19 with it if necessary as well. 

Could you open a PR from your branch? It might also need some adjustments since in other changes @hoodmane  migrated to Python 3.9 and added ctypes support.
> there is a risk that it makes odd errors happen in packages that aren't caught by the tests, especially in big packages that don't have high test coverage.

If it isn't tested we should assume that it's currently broken and that it will be broken in different ways after whatever changes you make.
Yes, in general we should start running package test suites at least in a CI cron job / or on demand https://github.com/pyodide/pyodide/issues/69
I've done a pull request now. I got the 3.9 stuff merged in, but I haven't pulled in the latest commits so it doesn't clean merge right now.

Resolving patch:
diff --git a/packages/fpcast-test/fpcast-test/setup.py b/packages/fpcast-test/fpcast-test/setup.py
new file mode 100644
--- /dev/null
+++ b/packages/fpcast-test/fpcast-test/setup.py
@@ -0,0 +1,18 @@
+from setuptools import setup, Extension
+
+setup(
+    name="fpcast-test",
+    version="0.1.1",
+    author="Hood Chatham",
+    author_email="roberthoodchatham@gmail.com",
+    description="Test function pointer casts.",
+    long_description_content_type="text/markdown",
+    classifiers=[
+        "Programming Language :: Python :: 3",
+        "License :: OSI Approved :: MIT License",
+        "Operating System :: OS Independent",
+    ],
+    # packages=["fpcast_test"],
+    ext_modules=[Extension("fpcast_test", ["fpcast-test.c"])]
+    # python_requires='>=3.6',
+)
diff --git a/pyodide-build/pyodide_build/common.py b/pyodide-build/pyodide_build/common.py
--- a/pyodide-build/pyodide_build/common.py
+++ b/pyodide-build/pyodide_build/common.py
@@ -14,7 +14,7 @@ def _parse_package_subset(query: Optional[str]) -> Set[str]:
 
     Supports folowing meta-packages,
      - 'core': corresponds to packages needed to run the core test suite
-       {"micropip", "pyparsing", "pytz", "packaging", "Jinja2"}. This is the default option
+       {"micropip", "pyparsing", "pytz", "packaging", "Jinja2", "fpcast-test"}. This is the default option
        if query is None.
      - 'min-scipy-stack': includes the "core" meta-package as well as some of the
        core packages from the scientific python stack and their dependencies:
@@ -32,7 +32,15 @@ def _parse_package_subset(query: Optional[str]) -> Set[str]:
     if query is None:
         query = "core"
 
-    core_packages = {"micropip", "pyparsing", "pytz", "packaging", "Jinja2", "regex"}
+    core_packages = {
+        "micropip",
+        "pyparsing",
+        "pytz",
+        "packaging",
+        "Jinja2",
+        "regex",
+        "fpcast-test",
+    }
     core_scipy_packages = {
         "numpy",
         "scipy",


Test patch:
diff --git a/docs/development/testing.md b/docs/development/testing.md
--- a/docs/development/testing.md
+++ b/docs/development/testing.md
@@ -31,12 +31,6 @@ There are 3 test locations that are collected by pytest,
   (do not require selenium to run)
 - `packages/*/test_*`: package specific tests.
 
-Additionally you can run emsdk specific tests with,
-
-```
-make -C emsdk test
-```
-
 ### Running the JavaScript test suite
 
 To run tests on the JavaScript Pyodide package using Mocha, run the following commands,
diff --git a/emsdk/tests/__init__.py b/emsdk/tests/__init__.py
deleted file mode 100644
diff --git a/emsdk/tests/common.py b/emsdk/tests/common.py
deleted file mode 100644
--- a/emsdk/tests/common.py
+++ /dev/null
@@ -1,33 +0,0 @@
-MAIN_C = r"""
-#include <stdio.h>
-#include <dlfcn.h>
-#include <setjmp.h>
-
-void never_called()
-{
-    jmp_buf buf;
-    int i=setjmp(buf);
-    longjmp(buf,1);
-}
-
-
-int main() {
-  puts("hello from main");
-  void *handle = dlopen("library.wasm", RTLD_NOW);
-  if (!handle) {
-    puts("cannot load side module");
-    puts(dlerror());
-    return 1;
-  }
-  typedef void (*type_v)();
-  type_v side_func = (type_v) dlsym(handle, "foo");
-  if (!side_func) {
-    puts("cannot load side function");
-    puts(dlerror());
-    return 1;
-  } else {
-    side_func();
-  }
-  return 0;
-}
-"""
diff --git a/emsdk/tests/test_dyncall.py b/emsdk/tests/test_dyncall.py
deleted file mode 100644
--- a/emsdk/tests/test_dyncall.py
+++ /dev/null
@@ -1,74 +0,0 @@
-import subprocess
-from . import common
-
-
-def test_dyncall(tmpdir):
-    with tmpdir.as_cwd():
-        with open("library.c", "w") as f:
-            f.write(
-                """\
-#include <stdio.h>
-#include <stdlib.h>
-#include <setjmp.h>
-#include <assert.h>
-
-// This can be any function that has a signature not found in main.
-__attribute__ ((noinline)) int indirect_function(int a, float b, int c, double d) {
-  return a;
-}
-
-typedef int (*type_iifid) (int, float, int, double);
-
-void foo() {
-  // Hack to force inclusion of malloc
-  volatile int x = (int) malloc(1);
-  free((void *) x);
-
-  type_iifid fp = &indirect_function;
-
-  jmp_buf buf;
-  int i = setjmp(buf);
-
-  printf("%d\\n", i);
-  assert(fp(i, 0, 0, 0) == i);
-
-  if (i == 0) longjmp(buf, 1);
-
-}
-"""
-            )
-        with open("main.c", "w") as f:
-            f.write(common.MAIN_C)
-
-        subprocess.run(
-            [
-                "emcc",
-                "-g4",
-                "-s",
-                "SIDE_MODULE=1",
-                "library.c",
-                "-o",
-                "library.wasm",
-                "-s",
-                "EMULATE_FUNCTION_POINTER_CASTS=1",
-                "-s",
-                "EXPORT_ALL=1",
-            ],
-            check=True,
-        )
-        subprocess.run(
-            [
-                "emcc",
-                "-g4",
-                "-s",
-                "MAIN_MODULE=1",
-                "main.c",
-                "--embed-file",
-                "library.wasm",
-                "-s",
-                "EMULATE_FUNCTION_POINTER_CASTS=1",
-            ],
-            check=True,
-        )
-        out = subprocess.run(["node", "a.out.js"], capture_output=True, check=True)
-        assert out.stdout == b"hello from main\n0\n1\n"
diff --git a/emsdk/tests/test_emulate.py b/emsdk/tests/test_emulate.py
deleted file mode 100644
--- a/emsdk/tests/test_emulate.py
+++ /dev/null
@@ -1,55 +0,0 @@
-import subprocess
-from . import common
-
-
-def test_emulate_function(tmpdir):
-    with tmpdir.as_cwd():
-        with open("library.c", "w") as f:
-            f.write(
-                """\
-#include <stdio.h>
-
-// emulate function pointer casts - this has the
-// wrong arguments and return type
-int foo(int extra_args) {
-  puts("hello from library");
-  return 0;
-}"""
-            )
-        with open("main.c", "w") as f:
-            f.write(common.MAIN_C)
-
-        subprocess.run(
-            [
-                "emcc",
-                "-g",
-                "-s",
-                "SIDE_MODULE=1",
-                "library.c",
-                "-o",
-                "library.wasm",
-                "-s",
-                "EMULATE_FUNCTION_POINTER_CASTS=1",
-                "-s",
-                "EXPORT_ALL=1",
-            ],
-            check=True,
-        )
-        subprocess.run(
-            [
-                "emcc",
-                "-g",
-                "-s",
-                "MAIN_MODULE=1",
-                "main.c",
-                "--embed-file",
-                "library.wasm",
-                "-s",
-                "EMULATE_FUNCTION_POINTER_CASTS=1",
-                "-s",
-                "EXPORT_ALL=1",
-            ],
-            check=True,
-        )
-        out = subprocess.run(["node", "a.out.js"], capture_output=True, check=True)
-        assert out.stdout == b"hello from main\nhello from library\n"
diff --git a/packages/cffi_example/test_cffi_example.py b/packages/cffi_example/test_cffi_example.py
--- a/packages/cffi_example/test_cffi_example.py
+++ b/packages/cffi_example/test_cffi_example.py
@@ -2,6 +2,13 @@
 from pyodide_build.testing import run_in_pyodide
 
 
+CHROME_FAIL_v90_MSG = (
+    "Doesn't work in chrome v89 or v90, I think because of "
+    "https://bugs.chromium.org/p/chromium/issues/detail?id=1200031. "
+    "Confirmed locally to work in v91 and v96, and to break on v90."
+)
+
+
 @pytest.mark.parametrize(
     "pattern,name,flags,expected",
     [
@@ -13,6 +20,8 @@
 )
 def test_fnmatch(selenium_module_scope, pattern, name, flags, expected):
     selenium = selenium_module_scope
+    if selenium.browser == "chrome":
+        pytest.xfail(CHROME_FAIL_v90_MSG)
     selenium.load_package("cffi_example")
     result = selenium.run(
         f"""
@@ -23,7 +32,11 @@ def test_fnmatch(selenium_module_scope, pattern, name, flags, expected):
     assert result == expected
 
 
-@run_in_pyodide(packages=["cffi_example"], module_scope=True)
+@run_in_pyodide(
+    packages=["cffi_example"],
+    module_scope=True,
+    xfail_browsers={"chrome": CHROME_FAIL_v90_MSG},
+)
 def test_person():
     from cffi_example.person import Person
 
diff --git a/packages/fpcast-test/test_fpcast_test.py b/packages/fpcast-test/test_fpcast_test.py
new file mode 100644
--- /dev/null
+++ b/packages/fpcast-test/test_fpcast_test.py
@@ -0,0 +1,46 @@
+from pyodide_build.testing import run_in_pyodide
+
+
+@run_in_pyodide(packages=["fpcast-test"])
+def test_fpcasts():
+    import fpcast_test
+
+    fpcast_test.noargs0()
+    fpcast_test.noargs1()
+    fpcast_test.noargs2()
+    fpcast_test.noargs3()
+
+    fpcast_test.varargs0()
+    fpcast_test.varargs1()
+    fpcast_test.varargs2()
+    fpcast_test.varargs3()
+
+    fpcast_test.kwargs0()
+    fpcast_test.kwargs1()
+    fpcast_test.kwargs2()
+    fpcast_test.kwargs3()
+
+    fpcast_test.Callable0()()
+    fpcast_test.Callable1()()
+    fpcast_test.Callable2()()
+    fpcast_test.Callable3()()
+
+    t = fpcast_test.TestType()
+    t.noargs0()
+    t.noargs1()
+    t.noargs2()
+    t.noargs3()
+
+    t.varargs0()
+    t.varargs1()
+    t.varargs2()
+    t.varargs3()
+
+    t.kwargs0()
+    t.kwargs1()
+    t.kwargs2()
+    t.kwargs3()
+
+    t.getset0
+    t.getset1
+    t.getset1 = 5
diff --git a/pyodide-build/pyodide_build/tests/test_common.py b/pyodide-build/pyodide_build/tests/test_common.py
--- a/pyodide-build/pyodide_build/tests/test_common.py
+++ b/pyodide-build/pyodide_build/tests/test_common.py
@@ -42,6 +42,7 @@ def test_parse_package_subset():
         "Jinja2",
         "micropip",
         "regex",
+        "fpcast-test",
     }
     # by default core packages are built
     assert _parse_package_subset(None) == _parse_package_subset("core")
@@ -53,6 +54,7 @@ def test_parse_package_subset():
         "Jinja2",
         "micropip",
         "regex",
+        "fpcast-test",
         "numpy",
         "scipy",
         "pandas",