Skip to content

Commit 852f446

Browse files
committed
fix(native): build stable ABI wheels
1 parent 4812388 commit 852f446

5 files changed

Lines changed: 288 additions & 14 deletions

File tree

csrc/axidev_io_module.c

Lines changed: 155 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99

1010
static PyObject *listener_callback = NULL;
11+
static PyObject *mouse_listener_callback = NULL;
1112

1213

1314
static void clear_listener_callback(void) {
@@ -17,6 +18,13 @@ static void clear_listener_callback(void) {
1718
}
1819

1920

21+
static void clear_mouse_listener_callback(void) {
22+
PyObject *callback = mouse_listener_callback;
23+
mouse_listener_callback = NULL;
24+
Py_XDECREF(callback);
25+
}
26+
27+
2028
static PyObject *bool_result(bool value) {
2129
if (value) {
2230
Py_RETURN_TRUE;
@@ -72,6 +80,31 @@ static PyObject *build_listener_event_dict(
7280
}
7381

7482

83+
static PyObject *build_mouse_state_dict(const axidev_io_mouse_state_t *state) {
84+
return Py_BuildValue("{s:i,s:i,s:i,s:d,s:d,s:K}", "x", state->x, "y",
85+
state->y, "buttons", (int)state->buttons, "scroll_x",
86+
state->scroll_x, "scroll_y", state->scroll_y,
87+
"timestamp_ms", (unsigned long long)state->timestamp_ms);
88+
}
89+
90+
91+
static PyObject *unicode_as_utf8_bytes(PyObject *value, const char **text) {
92+
PyObject *bytes = PyUnicode_AsUTF8String(value);
93+
if (bytes == NULL) {
94+
*text = NULL;
95+
return NULL;
96+
}
97+
98+
*text = PyBytes_AsString(bytes);
99+
if (*text == NULL) {
100+
Py_DECREF(bytes);
101+
return NULL;
102+
}
103+
104+
return bytes;
105+
}
106+
107+
75108
static void listener_bridge(
76109
uint32_t codepoint, axidev_io_keyboard_key_with_modifier_t key_mod,
77110
bool pressed, void *user_data) {
@@ -81,7 +114,7 @@ static void listener_bridge(
81114
if (listener_callback != NULL) {
82115
PyObject *event = build_listener_event_dict(codepoint, key_mod, pressed);
83116
if (event != NULL) {
84-
PyObject *result = PyObject_CallOneArg(listener_callback, event);
117+
PyObject *result = PyObject_CallFunctionObjArgs(listener_callback, event, NULL);
85118
if (result == NULL) {
86119
PyErr_WriteUnraisable(listener_callback);
87120
} else {
@@ -94,6 +127,27 @@ static void listener_bridge(
94127
}
95128

96129

130+
static void mouse_listener_bridge(const axidev_io_mouse_state_t *state,
131+
void *user_data) {
132+
(void)user_data;
133+
134+
PyGILState_STATE gil_state = PyGILState_Ensure();
135+
if (mouse_listener_callback != NULL && state != NULL) {
136+
PyObject *event = build_mouse_state_dict(state);
137+
if (event != NULL) {
138+
PyObject *result = PyObject_CallFunctionObjArgs(mouse_listener_callback, event, NULL);
139+
if (result == NULL) {
140+
PyErr_WriteUnraisable(mouse_listener_callback);
141+
} else {
142+
Py_DECREF(result);
143+
}
144+
Py_DECREF(event);
145+
}
146+
}
147+
PyGILState_Release(gil_state);
148+
}
149+
150+
97151
static PyObject *mod_initialize(PyObject *self, PyObject *Py_UNUSED(args)) {
98152
(void)self;
99153
return bool_result(axidev_io_keyboard_initialize());
@@ -103,7 +157,9 @@ static PyObject *mod_initialize(PyObject *self, PyObject *Py_UNUSED(args)) {
103157
static PyObject *mod_free(PyObject *self, PyObject *Py_UNUSED(args)) {
104158
(void)self;
105159
axidev_io_listener_stop();
160+
axidev_io_mouse_listener_stop();
106161
clear_listener_callback();
162+
clear_mouse_listener_callback();
107163
axidev_io_keyboard_free();
108164
Py_RETURN_NONE;
109165
}
@@ -256,19 +312,23 @@ static PyObject *mod_release_all_modifiers(PyObject *self,
256312

257313
static PyObject *mod_type_text(PyObject *self, PyObject *args) {
258314
PyObject *text_obj = NULL;
315+
PyObject *text_bytes = NULL;
259316
const char *text = NULL;
317+
bool ok;
260318
(void)self;
261319

262320
if (!PyArg_ParseTuple(args, "U", &text_obj)) {
263321
return NULL;
264322
}
265323

266-
text = PyUnicode_AsUTF8(text_obj);
267-
if (text == NULL) {
324+
text_bytes = unicode_as_utf8_bytes(text_obj, &text);
325+
if (text_bytes == NULL) {
268326
return NULL;
269327
}
270328

271-
return bool_result(axidev_io_keyboard_type_text(text));
329+
ok = axidev_io_keyboard_type_text(text);
330+
Py_DECREF(text_bytes);
331+
return bool_result(ok);
272332
}
273333

274334

@@ -354,6 +414,73 @@ static PyObject *mod_is_listening(PyObject *self, PyObject *Py_UNUSED(args)) {
354414
}
355415

356416

417+
static PyObject *mod_mouse_poll(PyObject *self, PyObject *Py_UNUSED(args)) {
418+
axidev_io_mouse_state_t state;
419+
bool ok;
420+
(void)self;
421+
422+
Py_BEGIN_ALLOW_THREADS
423+
ok = axidev_io_mouse_poll(&state);
424+
Py_END_ALLOW_THREADS
425+
if (!ok) {
426+
Py_RETURN_NONE;
427+
}
428+
return build_mouse_state_dict(&state);
429+
}
430+
431+
432+
static PyObject *mod_start_mouse_listener(PyObject *self, PyObject *args) {
433+
PyObject *callback = NULL;
434+
PyObject *previous = NULL;
435+
bool ok = false;
436+
(void)self;
437+
438+
if (!PyArg_ParseTuple(args, "O", &callback)) {
439+
return NULL;
440+
}
441+
442+
if (!PyCallable_Check(callback)) {
443+
PyErr_SetString(PyExc_TypeError, "listener must be callable");
444+
return NULL;
445+
}
446+
447+
Py_INCREF(callback);
448+
previous = mouse_listener_callback;
449+
mouse_listener_callback = callback;
450+
451+
if (axidev_io_mouse_listener_is_listening()) {
452+
Py_XDECREF(previous);
453+
Py_RETURN_TRUE;
454+
}
455+
456+
ok = axidev_io_mouse_listener_start(mouse_listener_bridge, NULL);
457+
if (!ok) {
458+
mouse_listener_callback = previous;
459+
Py_DECREF(callback);
460+
return bool_result(false);
461+
}
462+
463+
Py_XDECREF(previous);
464+
Py_RETURN_TRUE;
465+
}
466+
467+
468+
static PyObject *mod_stop_mouse_listener(PyObject *self,
469+
PyObject *Py_UNUSED(args)) {
470+
(void)self;
471+
axidev_io_mouse_listener_stop();
472+
clear_mouse_listener_callback();
473+
Py_RETURN_NONE;
474+
}
475+
476+
477+
static PyObject *mod_is_mouse_listening(PyObject *self,
478+
PyObject *Py_UNUSED(args)) {
479+
(void)self;
480+
return bool_result(axidev_io_mouse_listener_is_listening());
481+
}
482+
483+
357484
static PyObject *mod_key_to_string(PyObject *self, PyObject *args) {
358485
int key = 0;
359486
char *text = NULL;
@@ -377,19 +504,23 @@ static PyObject *mod_key_to_string(PyObject *self, PyObject *args) {
377504

378505
static PyObject *mod_string_to_key(PyObject *self, PyObject *args) {
379506
PyObject *text_obj = NULL;
507+
PyObject *text_bytes = NULL;
380508
const char *text = NULL;
509+
axidev_io_keyboard_key_t key;
381510
(void)self;
382511

383512
if (!PyArg_ParseTuple(args, "U", &text_obj)) {
384513
return NULL;
385514
}
386515

387-
text = PyUnicode_AsUTF8(text_obj);
388-
if (text == NULL) {
516+
text_bytes = unicode_as_utf8_bytes(text_obj, &text);
517+
if (text_bytes == NULL) {
389518
return NULL;
390519
}
391520

392-
return PyLong_FromLong((long)axidev_io_keyboard_string_to_key(text));
521+
key = axidev_io_keyboard_string_to_key(text);
522+
Py_DECREF(text_bytes);
523+
return PyLong_FromLong((long)key);
393524
}
394525

395526

@@ -419,7 +550,9 @@ static PyObject *mod_key_to_string_with_modifier(PyObject *self, PyObject *args)
419550

420551
static PyObject *mod_string_to_key_with_modifier(PyObject *self, PyObject *args) {
421552
PyObject *text_obj = NULL;
553+
PyObject *text_bytes = NULL;
422554
const char *text = NULL;
555+
bool ok;
423556
axidev_io_keyboard_key_with_modifier_t key_mod = {
424557
.key = AXIDEV_IO_KEY_UNKNOWN,
425558
.mods = AXIDEV_IO_MOD_NONE,
@@ -430,12 +563,14 @@ static PyObject *mod_string_to_key_with_modifier(PyObject *self, PyObject *args)
430563
return NULL;
431564
}
432565

433-
text = PyUnicode_AsUTF8(text_obj);
434-
if (text == NULL) {
566+
text_bytes = unicode_as_utf8_bytes(text_obj, &text);
567+
if (text_bytes == NULL) {
435568
return NULL;
436569
}
437570

438-
if (!axidev_io_keyboard_string_to_key_with_modifier(text, &key_mod)) {
571+
ok = axidev_io_keyboard_string_to_key_with_modifier(text, &key_mod);
572+
Py_DECREF(text_bytes);
573+
if (!ok) {
439574
Py_RETURN_NONE;
440575
}
441576

@@ -506,28 +641,32 @@ static PyObject *mod_log_is_enabled(PyObject *self, PyObject *args) {
506641
static PyObject *mod_log_message(PyObject *self, PyObject *args) {
507642
unsigned int level = 0;
508643
PyObject *message_obj = NULL;
644+
PyObject *message_bytes = NULL;
509645
const char *message = NULL;
510646
(void)self;
511647

512648
if (!PyArg_ParseTuple(args, "IU", &level, &message_obj)) {
513649
return NULL;
514650
}
515651

516-
message = PyUnicode_AsUTF8(message_obj);
517-
if (message == NULL) {
652+
message_bytes = unicode_as_utf8_bytes(message_obj, &message);
653+
if (message_bytes == NULL) {
518654
return NULL;
519655
}
520656

521657
axidev_io_log_message((axidev_io_log_level_t)level, "axidev_io", 0, "%s",
522658
message);
659+
Py_DECREF(message_bytes);
523660
Py_RETURN_NONE;
524661
}
525662

526663

527664
static void module_free(void *module) {
528665
(void)module;
529666
axidev_io_listener_stop();
667+
axidev_io_mouse_listener_stop();
530668
clear_listener_callback();
669+
clear_mouse_listener_callback();
531670
axidev_io_keyboard_free();
532671
}
533672

@@ -554,6 +693,10 @@ static PyMethodDef module_methods[] = {
554693
{"start_listener", mod_start_listener, METH_VARARGS, NULL},
555694
{"stop_listener", mod_stop_listener, METH_NOARGS, NULL},
556695
{"is_listening", mod_is_listening, METH_NOARGS, NULL},
696+
{"mouse_poll", mod_mouse_poll, METH_NOARGS, NULL},
697+
{"start_mouse_listener", mod_start_mouse_listener, METH_VARARGS, NULL},
698+
{"stop_mouse_listener", mod_stop_mouse_listener, METH_NOARGS, NULL},
699+
{"is_mouse_listening", mod_is_mouse_listening, METH_NOARGS, NULL},
557700
{"key_to_string", mod_key_to_string, METH_VARARGS, NULL},
558701
{"string_to_key", mod_string_to_key, METH_VARARGS, NULL},
559702
{"key_to_string_with_modifier", mod_key_to_string_with_modifier, METH_VARARGS,

setup.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
UPSTREAM_ROOT / "src" / "core" / "context.c",
2020
UPSTREAM_ROOT / "src" / "core" / "log.c",
2121
UPSTREAM_ROOT / "src" / "internal" / "utf.c",
22+
UPSTREAM_ROOT / "src" / "mouse" / "mouse_common.c",
2223
UPSTREAM_ROOT / "src" / "vendor" / "stb_ds_impl.c",
2324
UPSTREAM_ROOT / "src" / "keyboard" / "common" / "key_utils.c",
2425
UPSTREAM_ROOT / "src" / "keyboard" / "common" / "keymap.c",
@@ -29,6 +30,7 @@
2930
UPSTREAM_ROOT / "src" / "keyboard" / "common" / "windows_keymap.c",
3031
UPSTREAM_ROOT / "src" / "keyboard" / "sender" / "sender_windows.c",
3132
UPSTREAM_ROOT / "src" / "keyboard" / "listener" / "listener_windows.c",
33+
UPSTREAM_ROOT / "src" / "mouse" / "mouse_windows.c",
3234
]
3335

3436
LINUX_SOURCES = [
@@ -37,6 +39,7 @@
3739
UPSTREAM_ROOT / "src" / "keyboard" / "common" / "linux_keysym.c",
3840
UPSTREAM_ROOT / "src" / "keyboard" / "sender" / "sender_uinput.c",
3941
UPSTREAM_ROOT / "src" / "keyboard" / "listener" / "listener_linux.c",
42+
UPSTREAM_ROOT / "src" / "mouse" / "mouse_linux.c",
4043
]
4144

4245

@@ -158,15 +161,17 @@ def make_extension() -> Extension:
158161
(UPSTREAM_ROOT / "src").as_posix(),
159162
(UPSTREAM_ROOT / "vendor").as_posix(),
160163
],
161-
define_macros=[],
164+
define_macros=[("Py_LIMITED_API", "0x030A0000")],
162165
extra_compile_args=[],
163166
extra_link_args=[],
164167
libraries=[],
165168
language="c",
169+
py_limited_api=True,
166170
)
167171

168172

169173
setup(
170174
ext_modules=[make_extension()],
171175
cmdclass={"build_ext": AxidevIoBuildExt},
176+
options={"bdist_wheel": {"py_limited_api": "cp310"}},
172177
)

src/axidev_io/__init__.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@
1515
Listener,
1616
LogLevel,
1717
Modifier,
18+
MouseListener,
19+
MouseListenerCallback,
20+
MouseListenerStatus,
21+
MouseState,
1822
PermissionHelpers,
1923
PermissionSetupResult,
2024
Status,
@@ -47,6 +51,10 @@
4751
listen,
4852
log_message,
4953
modifiers,
54+
mouse,
55+
mouse_listen,
56+
mouse_poll,
57+
mouse_stop_listener,
5058
parse_combo,
5159
parse_key,
5260
release_all_modifiers,
@@ -82,6 +90,10 @@
8290
"Listener",
8391
"LogLevel",
8492
"Modifier",
93+
"MouseListener",
94+
"MouseListenerCallback",
95+
"MouseListenerStatus",
96+
"MouseState",
8597
"PermissionHelpers",
8698
"PermissionSetupResult",
8799
"Status",
@@ -112,6 +124,10 @@
112124
"listen",
113125
"log_message",
114126
"modifiers",
127+
"mouse",
128+
"mouse_listen",
129+
"mouse_poll",
130+
"mouse_stop_listener",
115131
"parse_combo",
116132
"parse_key",
117133
"release_all_modifiers",

0 commit comments

Comments
 (0)