Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 30 additions & 1 deletion Lib/test/test_capi/test_emscripten.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import signal
import unittest
from test.support import is_emscripten

if not is_emscripten:
raise unittest.SkipTest("Emscripten-only test")

from _testinternalcapi import emscripten_set_up_async_input_device
from _testinternalcapi import (
emscripten_check_signal_buffer,
emscripten_set_up_async_input_device,
)
from pathlib import Path


Expand All @@ -23,3 +27,28 @@ def test_emscripten_async_input_device(self):
self.assertEqual(f.readline().strip(), "ab")
self.assertEqual(f.readline().strip(), "fi")
self.assertEqual(f.readline().strip(), "xy")


class EmscriptenSignalBufferTest(unittest.TestCase):
def check_signal_buffer(self, shared):
received = []

def handler(signum, frame):
received.append(signum)

old_handler = signal.signal(signal.SIGUSR1, handler)
self.addCleanup(signal.signal, signal.SIGUSR1, old_handler)

left_in_buffer = emscripten_check_signal_buffer(signal.SIGUSR1, shared)
for _ in range(1000):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need this loop at all? It looks to me like the signal is triggered immediately by _Py_CheckEmscriptenSignals();. Also, emscripten_check_signal_buffer() turns signal handling back off so it certainly won't trigger later...

@alganet alganet Sep 19, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I confess I just added the test because I was ashamed to contribute without one, but I also think it's not necessary. There's no way to reasonably test the thread stuff, so it's mostly a placeholder. I would be +1 for removing it.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think having some check that this mechanism at least sort of works is not a bad thing.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe add a comment that it's not a realistic test of the feature but it at least executes the code path to make sure it's not completely broken.

if received:
break

self.assertEqual(left_in_buffer, 0)
self.assertEqual(received, [signal.SIGUSR1])

def test_shared_buffer(self):
self.check_signal_buffer(shared=True)

def test_unshared_buffer(self):
self.check_signal_buffer(shared=False)
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
On Emscripten, a signal written to the signal buffer while the interpreter
was clearing it is no longer lost.
32 changes: 32 additions & 0 deletions Modules/_testinternalcapi.c
Original file line number Diff line number Diff line change
Expand Up @@ -2945,6 +2945,7 @@ incref_decref_delayed(PyObject *self, PyObject *op)

#ifdef __EMSCRIPTEN__
#include "emscripten.h"
#include "pycore_emscripten_signal.h" // Py_EMSCRIPTEN_SIGNAL_HANDLING

EM_JS(int, emscripten_set_up_async_input_device_js, (void), {
let idx = 0;
Expand Down Expand Up @@ -2972,6 +2973,36 @@ emscripten_set_up_async_input_device(PyObject *self, PyObject *Py_UNUSED(ignored
Py_RETURN_FALSE;
}
}

EM_JS(void, emscripten_set_signal_buffer_js, (int signum, int shared), {
Module._Py_saved_signal_buffer = Module.Py_EmscriptenSignalBuffer;
Module.Py_EmscriptenSignalBuffer = shared
? new Uint8Array(new SharedArrayBuffer(1))
: new Uint8Array(1);
Module.Py_EmscriptenSignalBuffer[0] = signum;
});

EM_JS(int, emscripten_restore_signal_buffer_js, (void), {
const value = Module.Py_EmscriptenSignalBuffer[0];
Module.Py_EmscriptenSignalBuffer = Module._Py_saved_signal_buffer;
delete Module._Py_saved_signal_buffer;
return value;
});

static PyObject *
emscripten_check_signal_buffer(PyObject *self, PyObject *args)
{
int signum, shared;
if (!PyArg_ParseTuple(args, "ip", &signum, &shared)) {
return NULL;
}
int handling = Py_EMSCRIPTEN_SIGNAL_HANDLING;
emscripten_set_signal_buffer_js(signum, shared);
Py_EMSCRIPTEN_SIGNAL_HANDLING = 1;
_Py_CheckEmscriptenSignals();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe use CheckInterrupt() here since it should work and it's public api.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll look into it!

Py_EMSCRIPTEN_SIGNAL_HANDLING = handling;
return PyLong_FromLong(emscripten_restore_signal_buffer_js());
}
#endif

static PyObject *
Expand Down Expand Up @@ -3383,6 +3414,7 @@ static PyMethodDef module_functions[] = {
GET_NEXT_DICT_KEYS_VERSION_METHODDEF
#ifdef __EMSCRIPTEN__
{"emscripten_set_up_async_input_device", emscripten_set_up_async_input_device, METH_NOARGS},
{"emscripten_check_signal_buffer", emscripten_check_signal_buffer, METH_VARARGS},
#endif
{"simple_pending_call", simple_pending_call, METH_O},
{"set_vectorcall_nop", set_vectorcall_nop, METH_O},
Expand Down
4 changes: 1 addition & 3 deletions Python/emscripten_signal.c
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,7 @@ EM_JS(int, _Py_CheckEmscriptenSignals_Helper, (void), {
return 0;
}
try {
let result = Module.Py_EmscriptenSignalBuffer[0];
Module.Py_EmscriptenSignalBuffer[0] = 0;
return result;
return Atomics.exchange(Module.Py_EmscriptenSignalBuffer, 0, 0);
} catch(e) {
#if !defined(NDEBUG)
console.warn("Error occurred while trying to read signal buffer:", e);
Expand Down
Loading