├── tests ├── dlls │ └── empty ├── __init__.py ├── lib │ ├── __init__.py │ ├── cmd.py │ ├── pythonversion.py │ ├── param.py │ ├── const.py │ ├── parser.py │ ├── run.py │ └── install.py ├── test_util.py ├── test_error_missing_routine.py ├── test_error_missing_dll.py ├── test_paths.py ├── test_float_types.py ├── test_error_memsync.py ├── test_no_arguments.py ├── test_callback_simple.py ├── test_pointers_and_array_malloc_by_dll.py ├── test_string_strsxp.py ├── test_pointers_and_struct_malloc_by_dll.py ├── test_memsync_minimal.py ├── test_fixedlength_struct.py ├── test_fixedlength_callback.py ├── test_pointers_and_struct.py ├── test_struct_pointer.py ├── test_callback_simple_struct.py ├── test_memsync_computed_length.py ├── test_memsync_in_struct.py ├── test_pointers_and_array_in_struct_malloc_by_dll.py ├── test_pointers_byref.py ├── test_memsync_struct_array.py └── test_callback_optional.py ├── CHANGES.rst ├── README.rst ├── benchmark ├── dlls │ └── empty ├── __init__.py ├── minimal.py └── memsync.py ├── docs ├── source │ ├── changes.md │ ├── _static │ │ └── logo.png │ ├── benchmarks_all.rst │ ├── benchmarks_sysinfo.rst │ ├── sessionclass.rst │ ├── configclass.rst │ ├── memsync.rst │ ├── session.rst │ ├── interoperability.rst │ ├── _templates │ │ └── layout.html │ ├── __init__.py │ ├── configuration.rst │ ├── security.rst │ ├── support.rst │ ├── wineenv.rst │ ├── benchmark_minimal.rst │ ├── benchmark_memsync.rst │ ├── benchmarks.rst │ ├── benchmark_maximal.rst │ ├── version.py │ ├── faq.rst │ ├── installation.rst │ ├── index.rst │ └── memsyncintro.rst ├── docutils.conf ├── __init__.py └── Makefile ├── .readthedocs.yml ├── AUTHORS.md ├── src └── zugbruecke │ ├── core │ ├── __init__.py │ ├── definitions │ │ ├── __init__.py │ │ ├── custom.py │ │ └── simple.py │ ├── typeguard.py │ ├── errors.py │ ├── lib.py │ ├── const.py │ ├── cache.py │ ├── abc.py │ ├── memory.py │ ├── path.py │ ├── dll_server.py │ └── dll_client.py │ ├── __init__.py │ ├── ctypes │ ├── util.py │ └── __init__.py │ └── _server_.py ├── HOWTORELEASE.md ├── .gitignore ├── .github └── workflows │ └── test.yaml ├── pyproject.toml └── makefile /tests/dlls/empty: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /CHANGES.rst: -------------------------------------------------------------------------------- 1 | CHANGES.md -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | README.md -------------------------------------------------------------------------------- /benchmark/dlls/empty: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/source/changes.md: -------------------------------------------------------------------------------- 1 | ../../CHANGES.md -------------------------------------------------------------------------------- /docs/docutils.conf: -------------------------------------------------------------------------------- 1 | [restructuredtext parser] 2 | tab_width: 4 3 | -------------------------------------------------------------------------------- /docs/source/_static/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pleiszenburg/zugbruecke/HEAD/docs/source/_static/logo.png -------------------------------------------------------------------------------- /docs/source/benchmarks_all.rst: -------------------------------------------------------------------------------- 1 | .. include:: benchmark_memsync.rst 2 | .. include:: benchmark_minimal.rst 3 | .. include:: benchmark_maximal.rst 4 | -------------------------------------------------------------------------------- /docs/source/benchmarks_sysinfo.rst: -------------------------------------------------------------------------------- 1 | Benchmarks were performed on an "AMD EPYC 7443P 24-Core Processor" CPU, Linux 5.15.0-56-generic 64bit, and Wine 7.17 (Staging). 2 | -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | python: 4 | version: 3.7 5 | install: 6 | - method: pip 7 | path: . 8 | extra_requirements: 9 | - dev 10 | -------------------------------------------------------------------------------- /docs/source/sessionclass.rst: -------------------------------------------------------------------------------- 1 | .. _sessionclass: 2 | 3 | The ``zugbruecke.CtypesSession`` class 4 | -------------------------------------- 5 | 6 | .. autoclass:: zugbruecke.CtypesSession 7 | :members: 8 | -------------------------------------------------------------------------------- /AUTHORS.md: -------------------------------------------------------------------------------- 1 | # *zugbruecke* authors 2 | 3 | Core developer: 4 | 5 | - Sebastian M. Ernst 6 | 7 | Contributors, in alphabetical order: 8 | 9 | - Jimmy M. Gong 10 | - Melvyn 11 | -------------------------------------------------------------------------------- /docs/source/configclass.rst: -------------------------------------------------------------------------------- 1 | The ``zugbruecke.Config`` class 2 | =============================== 3 | 4 | Internally, configuration is read and managed via a dedicated class, :class:`zugbruecke.Config`. 5 | 6 | .. autoclass:: zugbruecke.Config 7 | :members: 8 | -------------------------------------------------------------------------------- /docs/source/memsync.rst: -------------------------------------------------------------------------------- 1 | :github_url: 2 | 3 | .. _memsync: 4 | 5 | .. index:: 6 | single: pointer 7 | 8 | Pointers & Memory Synchronization 9 | ================================= 10 | 11 | *zugbruecke* needs to translate pointers and synchronize memory between two processes. This section explains how this is done and when special ``memsync`` directives, extending regular *ctypes* syntax, are required. 12 | 13 | .. toctree:: 14 | :maxdepth: 2 15 | :caption: Memory Synchronization in Detail 16 | 17 | memsyncintro 18 | memsyncprotocol 19 | memsyncattr 20 | -------------------------------------------------------------------------------- /docs/source/session.rst: -------------------------------------------------------------------------------- 1 | :github_url: 2 | 3 | .. _session: 4 | 5 | .. index:: 6 | single: zugbruecke.ctypes 7 | single: zugbruecke.CtypesSession 8 | 9 | Session Model 10 | ============= 11 | 12 | *zugbruecke* operates based on a session model. Each session represents a separate *Windows* *Python* interpreter process running on top of *Wine*. *zugbruecke* starts a default session during the import of ``zugbruecke.ctypes``, but the user can start more and distinctly configured sessions if required manually by creating instances of :class:`zugbruecke.CtypesSession`. 13 | 14 | .. toctree:: 15 | :maxdepth: 2 16 | :caption: The Session Model in Detail 17 | 18 | sessionoverview 19 | sessionclass 20 | -------------------------------------------------------------------------------- /docs/source/interoperability.rst: -------------------------------------------------------------------------------- 1 | :github_url: 2 | 3 | .. _interoperability: 4 | 5 | .. index:: 6 | single: winepath 7 | pair: platform; interoperability 8 | statement: zugbruecke.ctypes.zb_path_unix_to_wine 9 | statement: zugbruecke.ctypes.zb_path_wine_to_unix 10 | 11 | Platform Interoperability 12 | ========================= 13 | 14 | Leveraging the features of *Wine*, *zugbruecke* tries to make things as easy as possible for the user. Some issues remain though, such as converting between *Unix* and *Wine* paths, which must be handled manually by the user. *zugbruecke* offers special APIs for this purpose: 15 | 16 | - :meth:`zugbruecke.CtypesSession.zb_path_unix_to_wine` 17 | - :meth:`zugbruecke.CtypesSession.zb_path_wine_to_unix` 18 | -------------------------------------------------------------------------------- /docs/source/_templates/layout.html: -------------------------------------------------------------------------------- 1 | {% extends "!layout.html" %} 2 | 3 | {% block menu %} 4 | 5 | {{ super() }} 6 | 7 | {% if sidebar_external_links %} 8 | 9 |

10 | 11 | {% if sidebar_external_links_caption %} 12 | {{ sidebar_external_links_caption }} 13 | {% else %} 14 | External links 15 | {% endif %} 16 | 17 |

18 | 19 |
    20 | {% for text, link in sidebar_external_links %} 21 | {% if hasdoc(link) %} 22 |
  • {{ text }}
  • 23 | {% else %} 24 |
  • {{ text }}
  • 25 | {% endif %} 26 | {% endfor %} 27 |
28 | 29 | {% endif %} 30 | 31 | {% endblock %} 32 | -------------------------------------------------------------------------------- /docs/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | 5 | ZUGBRUECKE 6 | Calling routines in Windows DLLs from Python scripts running on unixlike systems 7 | https://github.com/pleiszenburg/zugbruecke 8 | 9 | docs/__init__.py: Docs root 10 | 11 | Required to run on platform / side: [UNIX] 12 | 13 | Copyright (C) 2017-2023 Sebastian M. Ernst 14 | 15 | 16 | The contents of this file are subject to the GNU Lesser General Public License 17 | Version 2.1 ("LGPL" or "License"). You may not use this file except in 18 | compliance with the License. You may obtain a copy of the License at 19 | https://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt 20 | https://github.com/pleiszenburg/zugbruecke/blob/master/LICENSE 21 | 22 | Software distributed under the License is distributed on an "AS IS" basis, 23 | WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the 24 | specific language governing rights and limitations under the License. 25 | 26 | 27 | """ 28 | -------------------------------------------------------------------------------- /docs/source/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | 5 | ZUGBRUECKE 6 | Calling routines in Windows DLLs from Python scripts running on unixlike systems 7 | https://github.com/pleiszenburg/zugbruecke 8 | 9 | docs/source/__init__.py: Docs source root 10 | 11 | Required to run on platform / side: [UNIX] 12 | 13 | Copyright (C) 2017-2023 Sebastian M. Ernst 14 | 15 | 16 | The contents of this file are subject to the GNU Lesser General Public License 17 | Version 2.1 ("LGPL" or "License"). You may not use this file except in 18 | compliance with the License. You may obtain a copy of the License at 19 | https://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt 20 | https://github.com/pleiszenburg/zugbruecke/blob/master/LICENSE 21 | 22 | Software distributed under the License is distributed on an "AS IS" basis, 23 | WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the 24 | specific language governing rights and limitations under the License. 25 | 26 | 27 | """ 28 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | 5 | ZUGBRUECKE 6 | Calling routines in Windows DLLs from Python scripts running on unixlike systems 7 | https://github.com/pleiszenburg/zugbruecke 8 | 9 | tests/__init__.py: Test package init file 10 | 11 | Required to run on platform / side: [UNIX, WINE] 12 | 13 | Copyright (C) 2017-2023 Sebastian M. Ernst 14 | 15 | 16 | The contents of this file are subject to the GNU Lesser General Public License 17 | Version 2.1 ("LGPL" or "License"). You may not use this file except in 18 | compliance with the License. You may obtain a copy of the License at 19 | https://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt 20 | https://github.com/pleiszenburg/zugbruecke/blob/master/LICENSE 21 | 22 | Software distributed under the License is distributed on an "AS IS" basis, 23 | WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the 24 | specific language governing rights and limitations under the License. 25 | 26 | 27 | """ 28 | -------------------------------------------------------------------------------- /benchmark/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | 5 | ZUGBRUECKE 6 | Calling routines in Windows DLLs from Python scripts running on unixlike systems 7 | https://github.com/pleiszenburg/zugbruecke 8 | 9 | benchmark/__init__.py: Benchmark package init file 10 | 11 | Required to run on platform / side: [UNIX] 12 | 13 | Copyright (C) 2017-2023 Sebastian M. Ernst 14 | 15 | 16 | The contents of this file are subject to the GNU Lesser General Public License 17 | Version 2.1 ("LGPL" or "License"). You may not use this file except in 18 | compliance with the License. You may obtain a copy of the License at 19 | https://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt 20 | https://github.com/pleiszenburg/zugbruecke/blob/master/LICENSE 21 | 22 | Software distributed under the License is distributed on an "AS IS" basis, 23 | WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the 24 | specific language governing rights and limitations under the License. 25 | 26 | 27 | """ 28 | 29 | 30 | -------------------------------------------------------------------------------- /src/zugbruecke/core/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | 5 | ZUGBRUECKE 6 | Calling routines in Windows DLLs from Python scripts running on unixlike systems 7 | https://github.com/pleiszenburg/zugbruecke 8 | 9 | src/zugbruecke/core/__init__.py: Core module 10 | 11 | Required to run on platform / side: [UNIX, WINE] 12 | 13 | Copyright (C) 2017-2023 Sebastian M. Ernst 14 | 15 | 16 | The contents of this file are subject to the GNU Lesser General Public License 17 | Version 2.1 ("LGPL" or "License"). You may not use this file except in 18 | compliance with the License. You may obtain a copy of the License at 19 | https://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt 20 | https://github.com/pleiszenburg/zugbruecke/blob/master/LICENSE 21 | 22 | Software distributed under the License is distributed on an "AS IS" basis, 23 | WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the 24 | specific language governing rights and limitations under the License. 25 | 26 | 27 | """ 28 | -------------------------------------------------------------------------------- /tests/lib/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | 5 | ZUGBRUECKE 6 | Calling routines in Windows DLLs from Python scripts running on unixlike systems 7 | https://github.com/pleiszenburg/zugbruecke 8 | 9 | tests/lib/__init__.py: Test library package init file 10 | 11 | Required to run on platform / side: [UNIX, WINE] 12 | 13 | Copyright (C) 2017-2023 Sebastian M. Ernst 14 | 15 | 16 | The contents of this file are subject to the GNU Lesser General Public License 17 | Version 2.1 ("LGPL" or "License"). You may not use this file except in 18 | compliance with the License. You may obtain a copy of the License at 19 | https://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt 20 | https://github.com/pleiszenburg/zugbruecke/blob/master/LICENSE 21 | 22 | Software distributed under the License is distributed on an "AS IS" basis, 23 | WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the 24 | specific language governing rights and limitations under the License. 25 | 26 | 27 | """ 28 | -------------------------------------------------------------------------------- /docs/source/configuration.rst: -------------------------------------------------------------------------------- 1 | :github_url: 2 | 3 | .. _configuration: 4 | 5 | .. index:: 6 | pair: python; version 7 | triple: python; arch; architecture 8 | triple: wine; arch; architecture 9 | triple: log; level; write 10 | statement: zugbruecke.ctypes.zb_set_parameter 11 | module: zugbruecke.Config 12 | 13 | Configuration 14 | ============= 15 | 16 | *zugbruecke* can automatically configure itself or can be configured manually. The configuration allows you to fine-tune its verbosity, architecture and other relevant parameters on a per-session basis. By importing ``zugbruecke.ctypes``, a default session is started and configured automatically. Alternatively, you can create and configure your own sessions manually by creating instances of :class:`zugbruecke.CtypesSession`. See the :ref:`chapter on the session model ` for details. Sessions can be configured :ref:`at the time of their creation ` as well as :ref:`at run-time `. 17 | 18 | .. toctree:: 19 | :maxdepth: 2 20 | :caption: Configuration in Detail 21 | 22 | configparameters 23 | configclass 24 | -------------------------------------------------------------------------------- /docs/source/security.rst: -------------------------------------------------------------------------------- 1 | :github_url: 2 | 3 | .. _security: 4 | 5 | .. index:: 6 | triple: root; super user; privileges 7 | 8 | Security 9 | ======== 10 | 11 | *zugbruecke* is **notoriously insecure by design**. 12 | 13 | - **DO NOT** run it on any system directly exposed to the internet! Have a firewall on at all times! 14 | - **DO NOT** run untrusted code (or DLLs)! 15 | - **DO NOT** use *zugbruecke* for any security related tasks such as encryption, decryption, 16 | authentication and handling of keys or passwords! 17 | - **DO NOT** drive any physical hardware with it unless you keep it under constant supervision! 18 | - **DO NOT** run it with root / super users privileges! 19 | 20 | The following problems also directly apply to *zugbruecke*: 21 | 22 | - *Wine* can in fact theoretically run (some) `Windows malware`_. 23 | - **NEVER run Wine as root!** See `FAQ at WineHQ`_ for details. 24 | 25 | .. _Windows malware: https://en.wikipedia.org/wiki/Wine_(software)#Security 26 | .. _FAQ at WineHQ: https://wiki.winehq.org/FAQ#Should_I_run_Wine_as_root.3F 27 | 28 | *zugbruecke* does not actively prohibit its use with root privileges. 29 | -------------------------------------------------------------------------------- /docs/source/support.rst: -------------------------------------------------------------------------------- 1 | .. _support: 2 | 3 | Getting Help 4 | ============ 5 | 6 | There are multiple ways of getting help. 7 | 8 | Mailing List 9 | ------------ 10 | 11 | A public mailing list for the ``zugbruecke`` project and closely related projects - including `wenv`_ and `wenv-kernel`_ - for both users and developers is available at `groups.io`_. 12 | 13 | .. _groups.io: https://groups.io/g/zugbruecke-dev 14 | .. _wenv: https://github.com/pleiszenburg/wenv 15 | .. _wenv-kernel: https://github.com/pleiszenburg/wenv-kernel 16 | 17 | Chat 18 | ---- 19 | 20 | There is a `dedicated public Matrix chat`_. 21 | 22 | .. _dedicated public Matrix chat: https://matrix.to/#/#zugbruecke:matrix.org 23 | 24 | Reporting Bugs / Issues 25 | ----------------------- 26 | 27 | Bugs and any kind of issues should be reported in the project's `Github issue tracker`_, also see :ref:`chapter on bugs `. 28 | 29 | .. _Github issue tracker: https://github.com/pleiszenburg/zugbruecke/issues 30 | 31 | Paid Support 32 | ------------ 33 | 34 | In addition to the previous options, paid support is available from `pleiszenburg.de - Independent Scientific Services`_. 35 | 36 | .. _pleiszenburg.de - Independent Scientific Services: https://www.pleiszenburg.de 37 | -------------------------------------------------------------------------------- /HOWTORELEASE.md: -------------------------------------------------------------------------------- 1 | # How to release zugbruecke 2 | 3 | 1. Merge all relevant changes into branch `develop` - this is where development and pre-release testing happens. 4 | 5 | 2. In branch `develop`, run tests and examples and check that the documentation builds without errors. 6 | 7 | ```bash 8 | make test 9 | ``` 10 | 11 | 3. In branch `develop`, add missing changes to `CHANGES.md` and commit. 12 | 13 | 4. Push branch `develop` to GitHub. 14 | 15 | 5. Wait for feedback from CI. 16 | 17 | 6. Change to branch `master`. 18 | 19 | 7. Merge branch `develop` into branch `master` (comment `f"{version:s} release"`). 20 | 21 | 8. Push branch `master` to GitHub. 22 | 23 | 9. Tag branch `master` with `f"v{version:s}"`. 24 | 25 | ```bash 26 | git tag "v0.0.1" 27 | ``` 28 | 29 | 10. Push the tag to Github. 30 | 31 | ```bash 32 | git push origin --tags 33 | ``` 34 | 35 | 11. Build and sign packages. 36 | 37 | ```bash 38 | make release 39 | ``` 40 | 41 | 12. Upload package to `pypi`. 42 | 43 | ```bash 44 | make upload 45 | ``` 46 | 47 | 13. Change to branch `develop`. 48 | 49 | 14. In branch `develop`, bump the package version in `src/zugbruecke/__init__.py` by changing the `__version__` string. 50 | 51 | 15. In `CHANGES.md`, indicate that a new development cycle has started. 52 | 53 | 16. Commit to branch `develop`. 54 | 55 | 17. Push branch `develop` to GitHub. 56 | -------------------------------------------------------------------------------- /docs/source/wineenv.rst: -------------------------------------------------------------------------------- 1 | :github_url: 2 | 3 | .. _wineenv: 4 | 5 | .. index:: 6 | single: wenv 7 | single: wenv python 8 | single: wenv pip 9 | single: wenv pytest 10 | single: wenv init 11 | triple: wine; python; environment 12 | module: zugbruecke.wenv 13 | 14 | Wine Python Environment 15 | ======================= 16 | 17 | The ``wenv`` package 18 | -------------------- 19 | 20 | *zugbruecke* runs on top of a *Windows* build of CPython on top of *Wine*. A dedicated *Wine Python Environment*, a special kind of Python virtual environment, is created underneath the currently activated *Unix Python Environment*. This entire mechanism is managed by a `dedicated Python package named wenv`_. 21 | 22 | .. _dedicated Python package named wenv: https://wenv.readthedocs.io/ 23 | 24 | .. note:: 25 | 26 | The functionality of ``wenv`` has its origin in *zugbruecke* and was eventually consolidated into its own separate Python package. ``wenv`` offers a sophisticated Command Line Interface (CLI) as well as an API for managing and working with *Windows Python* on top of *Wine*. For more details, see `wenv documentation`_. 27 | 28 | .. _wenv documentation: https://wenv.readthedocs.io/ 29 | 30 | The ``zugbruecke.Env`` class 31 | ---------------------------- 32 | 33 | The *zugbruecke* package offers its own version of the ``wenv.Env`` class, essentially inheriting from it and extending it. 34 | 35 | .. autoclass:: zugbruecke.Env 36 | :members: 37 | :show-inheritance: 38 | :inherited-members: 39 | -------------------------------------------------------------------------------- /docs/source/benchmark_minimal.rst: -------------------------------------------------------------------------------- 1 | .. csv-table:: "minimal" benchmark, CPython 3.10.6 on linux, versions of CPython on Wine 2 | :header: "version", "arch", "convention", "ctypes [µs]", "zugbruecke [µs]", "overhead [µs]" 3 | :delim: 0x0003B 4 | 5 | "3.7.9"; "win32"; "cdll"; 0.7; 151.8; 151.1 6 | "3.7.9"; "win32"; "windll"; 0.7; 143.5; 142.8 7 | "3.7.9"; "win64"; "cdll"; 0.6; 132.1; 131.5 8 | "3.7.9"; "win64"; "windll"; 0.6; 133.7; 133.1 9 | "3.8.10"; "win32"; "cdll"; 0.7; 143.1; 142.4 10 | "3.8.10"; "win32"; "windll"; 0.7; 139.2; 138.5 11 | "3.8.10"; "win64"; "cdll"; 0.5; 128.4; 127.9 12 | "3.8.10"; "win64"; "windll"; 0.5; 132.5; 132.0 13 | "3.9.13"; "win32"; "cdll"; 0.7; 146.8; 146.1 14 | "3.9.13"; "win32"; "windll"; 0.7; 147.3; 146.6 15 | "3.9.13"; "win64"; "cdll"; 0.5; 130.4; 129.9 16 | "3.9.13"; "win64"; "windll"; 0.5; 135.0; 134.5 17 | "3.10.9"; "win32"; "cdll"; 0.7; 146.2; 145.5 18 | "3.10.9"; "win32"; "windll"; 0.7; 146.1; 145.4 19 | "3.10.9"; "win64"; "cdll"; 0.5; 139.1; 138.6 20 | "3.10.9"; "win64"; "windll"; 0.5; 131.5; 131.0 21 | "3.11.1"; "win32"; "cdll"; 0.7; 142.5; 141.8 22 | "3.11.1"; "win32"; "windll"; 0.7; 142.5; 141.8 23 | "3.11.1"; "win64"; "cdll"; 0.5; 131.8; 131.3 24 | "3.11.1"; "win64"; "windll"; 0.5; 133.8; 133.3 25 | 26 | 27 | The "minimal" benchmark is a simple function call with 28 | two ``c_int`` parameters and a single ``c_int`` return value. 29 | The DLL function simply adds the two numbers and returns the result. 30 | 31 | -------------------------------------------------------------------------------- /src/zugbruecke/core/definitions/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | 5 | ZUGBRUECKE 6 | Calling routines in Windows DLLs from Python scripts running on unixlike systems 7 | https://github.com/pleiszenburg/zugbruecke 8 | 9 | src/zugbruecke/core/definitions/__init__.py: Type and sync definitions 10 | 11 | Required to run on platform / side: [UNIX, WINE] 12 | 13 | Copyright (C) 2017-2023 Sebastian M. Ernst 14 | 15 | 16 | The contents of this file are subject to the GNU Lesser General Public License 17 | Version 2.1 ("LGPL" or "License"). You may not use this file except in 18 | compliance with the License. You may obtain a copy of the License at 19 | https://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt 20 | https://github.com/pleiszenburg/zugbruecke/blob/master/LICENSE 21 | 22 | Software distributed under the License is distributed on an "AS IS" basis, 23 | WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the 24 | specific language governing rights and limitations under the License. 25 | 26 | 27 | """ 28 | 29 | 30 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 31 | # EXPORT 32 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 33 | 34 | from .base import Definition 35 | from .custom import DefinitionCustom 36 | from .func import DefinitionFunc 37 | from .simple import DefinitionSimple 38 | from .struct import DefinitionStruct 39 | 40 | from .memsync import DefinitionMemsync 41 | -------------------------------------------------------------------------------- /src/zugbruecke/core/typeguard.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | 5 | ZUGBRUECKE 6 | Calling routines in Windows DLLs from Python scripts running on unixlike systems 7 | https://github.com/pleiszenburg/zugbruecke 8 | 9 | src/zugbruecke/core/typeguard.py: Wrapper around typeguard (optional) 10 | 11 | Required to run on platform / side: [UNIX, WINE] 12 | 13 | Copyright (C) 2017-2023 Sebastian M. Ernst 14 | 15 | 16 | The contents of this file are subject to the GNU Lesser General Public License 17 | Version 2.1 ("LGPL" or "License"). You may not use this file except in 18 | compliance with the License. You may obtain a copy of the License at 19 | https://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt 20 | https://github.com/pleiszenburg/zugbruecke/blob/master/LICENSE 21 | 22 | Software distributed under the License is distributed on an "AS IS" basis, 23 | WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the 24 | specific language governing rights and limitations under the License. 25 | 26 | 27 | """ 28 | 29 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 30 | # WRAPPER 31 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 32 | 33 | import os 34 | import warnings 35 | 36 | if os.environ.get('ZUGBRUECKE_DEBUG', '0') == '1': 37 | from typeguard import typechecked 38 | warnings.warn("zugbruecke running in debug mode with activated run-time type checks", RuntimeWarning) 39 | else: 40 | typechecked = lambda x: x 41 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # ZUGBRUECKE 2 | # Calling routines in Windows DLLs from Python scripts running on unixlike systems 3 | # https://github.com/pleiszenburg/zugbruecke 4 | # 5 | # docs/makefile: GNU makefile for building the documentation 6 | # 7 | # Required to run on platform / side: [UNIX] 8 | # 9 | # Copyright (C) 2017-2023 Sebastian M. Ernst 10 | # 11 | # 12 | # The contents of this file are subject to the GNU Lesser General Public License 13 | # Version 2.1 ("LGPL" or "License"). You may not use this file except in 14 | # compliance with the License. You may obtain a copy of the License at 15 | # https://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt 16 | # https://github.com/pleiszenburg/zugbruecke/blob/master/LICENSE 17 | # 18 | # Software distributed under the License is distributed on an "AS IS" basis, 19 | # WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the 20 | # specific language governing rights and limitations under the License. 21 | # 22 | 23 | 24 | # You can set these variables from the command line. 25 | SPHINXOPTS = 26 | SPHINXBUILD = python -msphinx 27 | SPHINXPROJ = zugbruecke 28 | SOURCEDIR = source 29 | BUILDDIR = build 30 | 31 | # Put it first so that "make" without argument is like "make help". 32 | help: 33 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 34 | 35 | .PHONY: help Makefile 36 | 37 | # Catch-all target: route all unknown targets to Sphinx using the new 38 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 39 | %: Makefile 40 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 41 | -------------------------------------------------------------------------------- /src/zugbruecke/core/errors.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | 5 | ZUGBRUECKE 6 | Calling routines in Windows DLLs from Python scripts running on unixlike systems 7 | https://github.com/pleiszenburg/zugbruecke 8 | 9 | src/zugbruecke/core/errors.py: Exceptions 10 | 11 | Required to run on platform / side: [UNIX, WINE] 12 | 13 | Copyright (C) 2017-2023 Sebastian M. Ernst 14 | 15 | 16 | The contents of this file are subject to the GNU Lesser General Public License 17 | Version 2.1 ("LGPL" or "License"). You may not use this file except in 18 | compliance with the License. You may obtain a copy of the License at 19 | https://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt 20 | https://github.com/pleiszenburg/zugbruecke/blob/master/LICENSE 21 | 22 | Software distributed under the License is distributed on an "AS IS" basis, 23 | WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the 24 | specific language governing rights and limitations under the License. 25 | 26 | 27 | """ 28 | 29 | 30 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 31 | # TYPES 32 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 33 | 34 | 35 | class ConfigParserError(Exception): 36 | pass 37 | 38 | 39 | class WineError(Exception): 40 | pass 41 | 42 | 43 | class DataError(Exception): 44 | pass 45 | 46 | 47 | class DataFlagError(DataError): 48 | pass 49 | 50 | 51 | class DataTypeError(DataError): 52 | pass 53 | 54 | 55 | class DataGroupError(DataError): 56 | pass 57 | 58 | 59 | class DataMemsyncpathError(DataError): 60 | pass 61 | -------------------------------------------------------------------------------- /docs/source/benchmark_memsync.rst: -------------------------------------------------------------------------------- 1 | .. csv-table:: "memsync" benchmark, CPython 3.10.6 on linux, versions of CPython on Wine 2 | :header: "version", "arch", "convention", "ctypes [µs]", "zugbruecke [µs]", "overhead [µs]" 3 | :delim: 0x0003B 4 | 5 | "3.7.9"; "win32"; "cdll"; 11.6; 201.0; 189.4 6 | "3.7.9"; "win32"; "windll"; 11.6; 205.7; 194.1 7 | "3.7.9"; "win64"; "cdll"; 8.6; 185.4; 176.8 8 | "3.7.9"; "win64"; "windll"; 8.4; 186.7; 178.3 9 | "3.8.10"; "win32"; "cdll"; 10.1; 192.4; 182.3 10 | "3.8.10"; "win32"; "windll"; 10.0; 199.7; 189.7 11 | "3.8.10"; "win64"; "cdll"; 7.2; 180.1; 172.9 12 | "3.8.10"; "win64"; "windll"; 7.1; 179.7; 172.6 13 | "3.9.13"; "win32"; "cdll"; 9.4; 195.1; 185.7 14 | "3.9.13"; "win32"; "windll"; 9.4; 203.7; 194.3 15 | "3.9.13"; "win64"; "cdll"; 7.2; 174.4; 167.2 16 | "3.9.13"; "win64"; "windll"; 7.1; 180.3; 173.2 17 | "3.10.9"; "win32"; "cdll"; 9.8; 194.8; 185.0 18 | "3.10.9"; "win32"; "windll"; 9.7; 200.1; 190.4 19 | "3.10.9"; "win64"; "cdll"; 7.3; 183.3; 176.0 20 | "3.10.9"; "win64"; "windll"; 7.3; 180.3; 173.0 21 | "3.11.1"; "win32"; "cdll"; 9.4; 190.8; 181.4 22 | "3.11.1"; "win32"; "windll"; 9.3; 193.1; 183.8 23 | "3.11.1"; "win64"; "cdll"; 7.2; 179.7; 172.5 24 | "3.11.1"; "win64"; "windll"; 7.2; 176.7; 169.5 25 | 26 | 27 | The "memsync" benchmark is a basic test of bidirectional memory synchronization 28 | via a ``memsync`` directive for a pointer argument, 29 | an array of single-precision floating point numbers. 30 | The benchmark uses 10 numbers per array. 31 | It is passed to the DLL function, 32 | next to the array's length as an ``c_int``. 33 | The DLL function performs a classic bubblesort algorithm in-place 34 | on the passed / synchronized memory. 35 | 36 | -------------------------------------------------------------------------------- /docs/source/benchmarks.rst: -------------------------------------------------------------------------------- 1 | :github_url: 2 | 3 | .. _benchmarks: 4 | 5 | .. index:: 6 | single: speed 7 | single: benchmark 8 | single: optimization 9 | pair: overhead; call 10 | 11 | Benchmarks 12 | ========== 13 | 14 | *zugbruecke* performs reasonably well given its complexity with **0.15 ms overhead per simple function call** on average on modern hardware. Very complex function calls involving callback functions and memory synchronization can involve an overhead of several milliseconds. 15 | 16 | .. note:: 17 | 18 | *zugbruecke* is not yet optimized for speed. The inter-process communication via *multiprocessing connection* adds overhead to every function call. Because *zugbruecke* takes care of packing and unpacking of pointers and structures for arguments and return values, this adds another bit of overhead. Calls are slow in general, but the first call of an individual routine within a session is even slower due to necessary initialization happening beforehand. Depending on the use-case, instead of working with *zugbruecke*, it will be significantly faster to isolate functionality depending on DLL calls into a dedicated *Python* script and run it directly with a *Windows Python* interpreter under *Wine*. *zugbruecke* offers a :ref:`Wine Python Environment ` for this purpose. 19 | 20 | For comparison and overhead measurements, see the individual benchmarks. 21 | 22 | .. include:: benchmarks_all.rst 23 | 24 | .. include:: benchmarks_sysinfo.rst 25 | 26 | *zugbruecke* was :ref:`configured ` with ``log_level`` set to ``0`` (logs off) for minimal overhead. For the corresponding source code, both Python and C, check the `benchmark directory`_ of this project. 27 | 28 | .. _benchmark directory: https://github.com/pleiszenburg/zugbruecke/tree/master/benchmark 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | # lib/ 19 | # lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | .pytest_cache/ 45 | nosetests.xml 46 | coverage.xml 47 | *,cover 48 | .hypothesis/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | 58 | # Flask stuff: 59 | instance/ 60 | .webassets-cache 61 | 62 | # Scrapy stuff: 63 | .scrapy 64 | 65 | # Sphinx documentation 66 | docs/_build/ 67 | 68 | # PyBuilder 69 | target/ 70 | 71 | # IPython Notebook 72 | .ipynb_checkpoints 73 | 74 | # pyenv 75 | .python-version 76 | 77 | # celery beat schedule file 78 | celerybeat-schedule 79 | 80 | # vscode 81 | .vscode/ 82 | 83 | # dotenv 84 | .env 85 | 86 | # virtualenv 87 | venv/ 88 | ENV/ 89 | env*/ 90 | 91 | # Spyder project settings 92 | .spyderproject 93 | 94 | # Rope project settings 95 | .ropeproject 96 | 97 | # Wine stuff 98 | *.dll 99 | 100 | # Logging 101 | *_err.txt 102 | *_out.txt 103 | 104 | # CFG 105 | .zugbruecke.json 106 | 107 | # dev 108 | todo.md 109 | -------------------------------------------------------------------------------- /src/zugbruecke/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | 5 | ZUGBRUECKE 6 | Calling routines in Windows DLLs from Python scripts running on unixlike systems 7 | https://github.com/pleiszenburg/zugbruecke 8 | 9 | src/zugbruecke/__init__.py: Package init file 10 | 11 | Required to run on platform / side: [UNIX, WINE] 12 | 13 | Copyright (C) 2017-2023 Sebastian M. Ernst 14 | 15 | 16 | The contents of this file are subject to the GNU Lesser General Public License 17 | Version 2.1 ("LGPL" or "License"). You may not use this file except in 18 | compliance with the License. You may obtain a copy of the License at 19 | https://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt 20 | https://github.com/pleiszenburg/zugbruecke/blob/master/LICENSE 21 | 22 | Software distributed under the License is distributed on an "AS IS" basis, 23 | WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the 24 | specific language governing rights and limitations under the License. 25 | 26 | 27 | """ 28 | 29 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 30 | # CONST 31 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 32 | 33 | __version__ = "0.2.1" 34 | 35 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 36 | # EXPORT: zugbruecke core 37 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 38 | 39 | import sys as _sys 40 | 41 | if not _sys.platform.startswith("win"): 42 | from .core.session import CtypesSession 43 | del _sys 44 | 45 | from .core.config import Config 46 | from .core.errors import * 47 | from .core.wenv import Env 48 | -------------------------------------------------------------------------------- /src/zugbruecke/ctypes/util.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | 5 | ZUGBRUECKE 6 | Calling routines in Windows DLLs from Python scripts running on unixlike systems 7 | https://github.com/pleiszenburg/zugbruecke 8 | 9 | src/zugbruecke/ctypes/util.py: ctypes.util for POSIX, hacked for DLL compatibility 10 | 11 | Required to run on platform / side: [UNIX] 12 | 13 | Copyright (C) 2017-2023 Sebastian M. Ernst 14 | 15 | 16 | The contents of this file are subject to the GNU Lesser General Public License 17 | Version 2.1 ("LGPL" or "License"). You may not use this file except in 18 | compliance with the License. You may obtain a copy of the License at 19 | https://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt 20 | https://github.com/pleiszenburg/zugbruecke/blob/master/LICENSE 21 | 22 | Software distributed under the License is distributed on an "AS IS" basis, 23 | WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the 24 | specific language governing rights and limitations under the License. 25 | 26 | 27 | """ 28 | 29 | 30 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 31 | # IMPORT: zugbruecke core 32 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 33 | 34 | from . import util as _util 35 | 36 | 37 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 38 | # Setup module 39 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 40 | 41 | _globals = globals() 42 | for _util_item in dir(_util): 43 | if _util_item.startswith("__"): 44 | continue 45 | _globals[_util_item] = getattr(_util, _util_item) 46 | del _util, _util_item, _globals 47 | -------------------------------------------------------------------------------- /src/zugbruecke/ctypes/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | 5 | ZUGBRUECKE 6 | Calling routines in Windows DLLs from Python scripts running on unixlike systems 7 | https://github.com/pleiszenburg/zugbruecke 8 | 9 | src/zugbruecke/ctypes/__init__.py: ctypes drop-in replacement 10 | 11 | Required to run on platform / side: [UNIX] 12 | 13 | Copyright (C) 2017-2023 Sebastian M. Ernst 14 | 15 | 16 | The contents of this file are subject to the GNU Lesser General Public License 17 | Version 2.1 ("LGPL" or "License"). You may not use this file except in 18 | compliance with the License. You may obtain a copy of the License at 19 | https://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt 20 | https://github.com/pleiszenburg/zugbruecke/blob/master/LICENSE 21 | 22 | Software distributed under the License is distributed on an "AS IS" basis, 23 | WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the 24 | specific language governing rights and limitations under the License. 25 | 26 | 27 | """ 28 | 29 | 30 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 31 | # IMPORT: zugbruecke core 32 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 33 | 34 | from ..core.session import ( 35 | CtypesSession as _CtypesSession, 36 | _ctypes_veryprivate, 37 | ) 38 | 39 | 40 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 41 | # Setup module 42 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 43 | 44 | _session = _CtypesSession() 45 | _globals = globals() 46 | for _ctypes_item in dir(_session): 47 | if _ctypes_item.startswith("__") and not _ctypes_item in _ctypes_veryprivate: 48 | continue 49 | _globals[_ctypes_item] = getattr(_session, _ctypes_item) 50 | del _globals, _session, _CtypesSession, _ctypes_item, _ctypes_veryprivate 51 | -------------------------------------------------------------------------------- /tests/test_util.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | 5 | ZUGBRUECKE 6 | Calling routines in Windows DLLs from Python scripts running on unixlike systems 7 | https://github.com/pleiszenburg/zugbruecke 8 | 9 | tests/test_util.py: Testing methods crom ctypes.util 10 | 11 | Required to run on platform / side: [UNIX, WINE] 12 | 13 | Copyright (C) 2017-2023 Sebastian M. Ernst 14 | 15 | 16 | The contents of this file are subject to the GNU Lesser General Public License 17 | Version 2.1 ("LGPL" or "License"). You may not use this file except in 18 | compliance with the License. You may obtain a copy of the License at 19 | https://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt 20 | https://github.com/pleiszenburg/zugbruecke/blob/master/LICENSE 21 | 22 | Software distributed under the License is distributed on an "AS IS" basis, 23 | WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the 24 | specific language governing rights and limitations under the License. 25 | 26 | 27 | """ 28 | 29 | 30 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 31 | # IMPORT 32 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 33 | 34 | from .lib.ctypes import get_context 35 | 36 | import pytest 37 | 38 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 39 | # TEST(s) 40 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 41 | 42 | 43 | @pytest.mark.parametrize( 44 | "arch,conv,ctypes,dll_path", get_context(__file__, handle=False) 45 | ) 46 | def test_find_library(arch, conv, ctypes, dll_path): 47 | 48 | assert ( 49 | ctypes.util.find_library("kernel32") == "C:\\windows\\system32\\kernel32.dll" 50 | ) 51 | 52 | 53 | @pytest.mark.parametrize( 54 | "arch,conv,ctypes,dll_path", get_context(__file__, handle=False) 55 | ) 56 | def test_find_msvcrt(arch, conv, ctypes, dll_path): 57 | 58 | assert ctypes.util.find_msvcrt() == None 59 | -------------------------------------------------------------------------------- /docs/source/benchmark_maximal.rst: -------------------------------------------------------------------------------- 1 | .. csv-table:: "maximal" benchmark, CPython 3.10.6 on linux, versions of CPython on Wine 2 | :header: "version", "arch", "convention", "ctypes [µs]", "zugbruecke [µs]", "overhead [µs]" 3 | :delim: 0x0003B 4 | 5 | "3.7.9"; "win32"; "cdll"; 67.3; 2,573.5; 2,506.2 6 | "3.7.9"; "win32"; "windll"; 66.8; 2,578.1; 2,511.3 7 | "3.7.9"; "win64"; "cdll"; 49.9; 2,301.9; 2,252.0 8 | "3.7.9"; "win64"; "windll"; 51.4; 2,313.5; 2,262.1 9 | "3.8.10"; "win32"; "cdll"; 59.9; 2,406.5; 2,346.6 10 | "3.8.10"; "win32"; "windll"; 60.5; 2,402.5; 2,342.0 11 | "3.8.10"; "win64"; "cdll"; 45.2; 2,166.9; 2,121.7 12 | "3.8.10"; "win64"; "windll"; 44.7; 2,181.1; 2,136.4 13 | "3.9.13"; "win32"; "cdll"; 59.6; 2,434.6; 2,375.0 14 | "3.9.13"; "win32"; "windll"; 59.8; 2,415.7; 2,355.9 15 | "3.9.13"; "win64"; "cdll"; 45.9; 2,187.5; 2,141.6 16 | "3.9.13"; "win64"; "windll"; 45.6; 2,188.5; 2,142.9 17 | "3.10.9"; "win32"; "cdll"; 62.6; 2,361.9; 2,299.3 18 | "3.10.9"; "win32"; "windll"; 62.5; 2,439.2; 2,376.7 19 | "3.10.9"; "win64"; "cdll"; 47.0; 2,177.9; 2,130.9 20 | "3.10.9"; "win64"; "windll"; 46.5; 2,173.7; 2,127.2 21 | "3.11.1"; "win32"; "cdll"; 58.5; 2,490.1; 2,431.6 22 | "3.11.1"; "win32"; "windll"; 58.6; 2,347.9; 2,289.3 23 | "3.11.1"; "win64"; "cdll"; 42.7; 2,113.6; 2,070.9 24 | "3.11.1"; "win64"; "windll"; 42.7; 2,123.4; 2,080.7 25 | 26 | 27 | The "maximal" benchmark runs through everything that *zugbuecke* has to offer. 28 | The DLL function takes three arguments: Two pointers to structs and a function pointer. 29 | The structs themselves contain pointers to memory of arbitrary length which is handled by ``memsync``. 30 | The function pointer allows to pass a reference to a callback function, written in pure Python. 31 | It takes a single pointer to a struct, again containing a pointer to memory of arbitrary length, 32 | yet again handled by ``memsync``, and returns a single integer. 33 | The callback is invoked 9 times per DLL function call. 34 | The test is based on a simple monochrom image filter where the DLL function iterates over every pixel 35 | in a 3x3 pixel monochrom image while the filter's kernel is provided by the callback function. 36 | 37 | -------------------------------------------------------------------------------- /src/zugbruecke/core/lib.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | 5 | ZUGBRUECKE 6 | Calling routines in Windows DLLs from Python scripts running on unixlike systems 7 | https://github.com/pleiszenburg/zugbruecke 8 | 9 | src/zugbruecke/core/lib.py: General purpose routines 10 | 11 | Required to run on platform / side: [UNIX, WINE] 12 | 13 | Copyright (C) 2017-2023 Sebastian M. Ernst 14 | 15 | 16 | The contents of this file are subject to the GNU Lesser General Public License 17 | Version 2.1 ("LGPL" or "License"). You may not use this file except in 18 | compliance with the License. You may obtain a copy of the License at 19 | https://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt 20 | https://github.com/pleiszenburg/zugbruecke/blob/master/LICENSE 21 | 22 | Software distributed under the License is distributed on an "AS IS" basis, 23 | WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the 24 | specific language governing rights and limitations under the License. 25 | 26 | 27 | """ 28 | 29 | 30 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 31 | # IMPORT 32 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 33 | 34 | import hashlib 35 | import random 36 | import socket 37 | 38 | from .typeguard import typechecked 39 | 40 | 41 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 42 | # LIBRARY ROUTINES 43 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 44 | 45 | 46 | @typechecked 47 | def get_free_port() -> int: 48 | 49 | s = socket.socket() 50 | s.bind(("", 0)) 51 | port = s.getsockname()[1] 52 | s.close() 53 | 54 | return port 55 | 56 | 57 | @typechecked 58 | def get_hash_of_string(in_str: str) -> str: 59 | 60 | return hashlib.sha256(in_str.encode("utf-8")).hexdigest() 61 | 62 | 63 | @typechecked 64 | def get_randhashstr(digits: int) -> str: 65 | 66 | return "{{NUMBER:0{DIGITS:d}x}}".format(DIGITS=digits).format( 67 | NUMBER=random.randrange(16 ** digits) 68 | ) 69 | 70 | 71 | @typechecked 72 | def generate_session_id() -> str: 73 | 74 | return get_randhashstr(8) # 8 digit hash string by default 75 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: zugbruecke test suite 2 | 3 | on: 4 | push: {} 5 | schedule: 6 | - cron: '30 9 7 * *' 7 | 8 | jobs: 9 | build: 10 | 11 | strategy: 12 | matrix: 13 | os: ["ubuntu-20.04"] 14 | python-version: [ 15 | 3.7, 16 | 3.8, 17 | 3.9, 18 | "3.10", 19 | "3.11" 20 | ] 21 | 22 | runs-on: ${{ matrix.os }} 23 | 24 | steps: 25 | - uses: actions/checkout@v2 26 | - name: Install Wine 27 | run: | 28 | sudo dpkg --add-architecture i386 29 | sudo sh -c "curl https://dl.winehq.org/wine-builds/winehq.key | gpg --dearmor > /etc/apt/trusted.gpg.d/winehq.gpg" 30 | sudo sh -c "apt-add-repository \"https://dl.winehq.org/wine-builds/ubuntu\"" 31 | sudo apt update 32 | sudo apt-get install -yqq --allow-downgrades libgd3/focal libpcre2-8-0/focal libpcre2-16-0/focal libpcre2-32-0/focal libpcre2-posix2/focal 33 | sudo apt-get purge -yqq libmono* moby* mono* php* libgdiplus libpcre2-posix3 libzip4 34 | echo "/opt/wine-staging/bin" >> $GITHUB_PATH 35 | sudo apt install -y --install-recommends wine-staging="7.17~focal-1" wine-staging-i386="7.17~focal-1" wine-staging-amd64="7.17~focal-1" 36 | - name: Install compiler 37 | run: | 38 | sudo apt-get install -y gcc-mingw-w64-i686 39 | sudo apt-get install -y gcc-mingw-w64-x86-64 40 | - name: Install Python ${{ matrix.python-version }} 41 | uses: actions/setup-python@v2 42 | with: 43 | python-version: ${{ matrix.python-version }} 44 | - name: Update Python infrastructure 45 | run: | 46 | python -m pip install --upgrade pip 47 | pip install -U setuptools 48 | - name: Install zugbruecke package 49 | run: | 50 | make install 51 | - name: Pre-check 52 | run: | 53 | wine --version 54 | wine64 --version 55 | i686-w64-mingw32-gcc --version 56 | x86_64-w64-mingw32-gcc --version 57 | python --version 58 | pytest --version 59 | uname -a 60 | lsb_release -a 61 | wenv --version 62 | - name: Build docs 63 | run: | 64 | make docs 65 | - name: Run tests 66 | run: | 67 | ZUGBRUECKE_LOG_LEVEL=100 make test 68 | -------------------------------------------------------------------------------- /tests/lib/cmd.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | 5 | ZUGBRUECKE 6 | Calling routines in Windows DLLs from Python scripts running on unixlike systems 7 | https://github.com/pleiszenburg/zugbruecke 8 | 9 | tests/lib/cmd.py: Thin wrapper around subprocess.Popen 10 | 11 | Required to run on platform / side: [UNIX, WINE] 12 | 13 | Copyright (C) 2017-2023 Sebastian M. Ernst 14 | 15 | 16 | The contents of this file are subject to the GNU Lesser General Public License 17 | Version 2.1 ("LGPL" or "License"). You may not use this file except in 18 | compliance with the License. You may obtain a copy of the License at 19 | https://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt 20 | https://github.com/pleiszenburg/zugbruecke/blob/master/LICENSE 21 | 22 | Software distributed under the License is distributed on an "AS IS" basis, 23 | WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the 24 | specific language governing rights and limitations under the License. 25 | 26 | 27 | """ 28 | 29 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 30 | # IMPORT 31 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 32 | 33 | import os 34 | from subprocess import Popen 35 | from typing import Dict, List, Optional 36 | 37 | from typeguard import typechecked 38 | 39 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 40 | # ROUTINES 41 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 42 | 43 | 44 | @typechecked 45 | def run_cmd(cmd: List[str], env: Optional[Dict[str, str]] = None): 46 | """ 47 | Minimal subprocess.Popen wrapper 48 | 49 | Args: 50 | - cmd: List of command fragments for Popen 51 | - env: Environment variables 52 | """ 53 | 54 | envvars = os.environ.copy() 55 | if env is not None: 56 | envvars.update(env) 57 | 58 | # for name in ('PWD', 'OLDPWD', 'VIRTUAL_ENV', 'PYTHONPATH'): 59 | # try: 60 | # envvars.pop(name) 61 | # except KeyError: 62 | # continue 63 | 64 | # print('???', envvars) 65 | 66 | proc = Popen(cmd, env = envvars) 67 | proc.wait() 68 | if proc.returncode != 0: 69 | raise SystemError('command failed', cmd, env) 70 | -------------------------------------------------------------------------------- /tests/test_error_missing_routine.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | 5 | ZUGBRUECKE 6 | Calling routines in Windows DLLs from Python scripts running on unixlike systems 7 | https://github.com/pleiszenburg/zugbruecke 8 | 9 | tests/test_error_missing_routine.py: Checks for proper error handling if routine does not exist 10 | 11 | Required to run on platform / side: [UNIX, WINE] 12 | 13 | Copyright (C) 2017-2023 Sebastian M. Ernst 14 | 15 | 16 | The contents of this file are subject to the GNU Lesser General Public License 17 | Version 2.1 ("LGPL" or "License"). You may not use this file except in 18 | compliance with the License. You may obtain a copy of the License at 19 | https://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt 20 | https://github.com/pleiszenburg/zugbruecke/blob/master/LICENSE 21 | 22 | Software distributed under the License is distributed on an "AS IS" basis, 23 | WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the 24 | specific language governing rights and limitations under the License. 25 | 26 | 27 | """ 28 | 29 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 30 | # C 31 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 32 | 33 | HEADER = """ 34 | {{ PREFIX }} int16_t {{ SUFFIX }} sqrt_int( 35 | int16_t a 36 | ); 37 | """ 38 | 39 | SOURCE = """ 40 | {{ PREFIX }} int16_t {{ SUFFIX }} sqrt_int( 41 | int16_t a 42 | ) 43 | { 44 | return sqrt(a); 45 | } 46 | """ 47 | 48 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 49 | # IMPORT 50 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 51 | 52 | from .lib.ctypes import get_context 53 | 54 | import pytest 55 | 56 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 57 | # TEST(s) 58 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 59 | 60 | 61 | @pytest.mark.parametrize("arch,conv,ctypes,dll_handle", get_context(__file__)) 62 | def test_missingroutine(arch, conv, ctypes, dll_handle): 63 | """ 64 | Routine in DLL does not exist 65 | """ 66 | 67 | with pytest.raises(AttributeError): 68 | _ = dll_handle.missing_routine 69 | -------------------------------------------------------------------------------- /src/zugbruecke/core/const.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | 5 | ZUGBRUECKE 6 | Calling routines in Windows DLLs from Python scripts running on unixlike systems 7 | https://github.com/pleiszenburg/zugbruecke 8 | 9 | src/zugbruecke/core/const.py: Holds constant values, flags, types 10 | 11 | Required to run on platform / side: [UNIX, WINE] 12 | 13 | Copyright (C) 2017-2023 Sebastian M. Ernst 14 | 15 | 16 | The contents of this file are subject to the GNU Lesser General Public License 17 | Version 2.1 ("LGPL" or "License"). You may not use this file except in 18 | compliance with the License. You may obtain a copy of the License at 19 | https://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt 20 | https://github.com/pleiszenburg/zugbruecke/blob/master/LICENSE 21 | 22 | Software distributed under the License is distributed on an "AS IS" basis, 23 | WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the 24 | specific language governing rights and limitations under the License. 25 | 26 | 27 | """ 28 | 29 | 30 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 31 | # TYPES 32 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 33 | 34 | FLAG_POINTER = -1 35 | 36 | SIMPLE_GROUP = "PyCSimpleType" 37 | STRUCT_GROUP = "PyCStructType" 38 | FUNC_GROUP = "PyCFuncPtrType" 39 | CUSTOM_GROUP = "Custom" 40 | 41 | 42 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 43 | # STATES 44 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 45 | 46 | PLATFORMS = ("UNIX", "WINE") 47 | CONVENTIONS = ("cdll", "windll", "oledll") 48 | 49 | 50 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 51 | # CTYPES FLAGS 52 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 53 | 54 | # Required for WINFUNCTYPE 55 | _FUNCFLAG_STDCALL = 0 # EXPORT 56 | 57 | 58 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 59 | # FOLDER- AND FILENAMES 60 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 61 | 62 | CONFIG_FLD = ".zugbruecke" 63 | CONFIG_FN = ".zugbruecke.json" 64 | -------------------------------------------------------------------------------- /docs/source/version.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | 5 | ZUGBRUECKE 6 | Calling routines in Windows DLLs from Python scripts running on unixlike systems 7 | https://github.com/pleiszenburg/zugbruecke 8 | 9 | docs/source/version.py: Package version parser 10 | 11 | Required to run on platform / side: [UNIX] 12 | 13 | Copyright (C) 2017-2023 Sebastian M. Ernst 14 | 15 | 16 | The contents of this file are subject to the GNU Lesser General Public License 17 | Version 2.1 ("LGPL" or "License"). You may not use this file except in 18 | compliance with the License. You may obtain a copy of the License at 19 | https://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt 20 | https://github.com/pleiszenburg/zugbruecke/blob/master/LICENSE 21 | 22 | Software distributed under the License is distributed on an "AS IS" basis, 23 | WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the 24 | specific language governing rights and limitations under the License. 25 | 26 | 27 | """ 28 | 29 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 30 | # IMPORT 31 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 32 | 33 | import ast 34 | import os 35 | 36 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 37 | # CONFIG 38 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 39 | 40 | SRC_DIR = "src" 41 | 42 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 43 | # ROUTINES 44 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 45 | 46 | def parse_version(code: str) -> str: 47 | 48 | tree = ast.parse(code) 49 | 50 | for item in tree.body: 51 | if not isinstance(item, ast.Assign): 52 | continue 53 | if len(item.targets) != 1: 54 | continue 55 | if item.targets[0].id != "__version__": 56 | continue 57 | return item.value.s 58 | 59 | def get_version() -> str: 60 | 61 | path = os.path.join( 62 | os.path.dirname(__file__), '..', '..', SRC_DIR, "zugbruecke", "__init__.py", 63 | ) 64 | 65 | with open(path, "r", encoding="utf-8") as f: 66 | version = parse_version(f.read()) 67 | 68 | return version 69 | -------------------------------------------------------------------------------- /tests/test_error_missing_dll.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | 5 | ZUGBRUECKE 6 | Calling routines in Windows DLLs from Python scripts running on unixlike systems 7 | https://github.com/pleiszenburg/zugbruecke 8 | 9 | tests/test_error_missing_dll.py: Checks for proper error handling if DLL does not exist 10 | 11 | Required to run on platform / side: [UNIX, WINE] 12 | 13 | Copyright (C) 2017-2023 Sebastian M. Ernst 14 | 15 | 16 | The contents of this file are subject to the GNU Lesser General Public License 17 | Version 2.1 ("LGPL" or "License"). You may not use this file except in 18 | compliance with the License. You may obtain a copy of the License at 19 | https://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt 20 | https://github.com/pleiszenburg/zugbruecke/blob/master/LICENSE 21 | 22 | Software distributed under the License is distributed on an "AS IS" basis, 23 | WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the 24 | specific language governing rights and limitations under the License. 25 | 26 | 27 | """ 28 | 29 | 30 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 31 | # IMPORT 32 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 33 | 34 | import os 35 | 36 | from .lib.ctypes import get_context 37 | 38 | import pytest 39 | 40 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 41 | # TEST(s) 42 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 43 | 44 | 45 | @pytest.mark.parametrize( 46 | "arch,conv,ctypes,dll_path", get_context(__file__, handle=False) 47 | ) 48 | def test_missingdll(arch, conv, ctypes, dll_path): 49 | """ 50 | DLL file does not exist, via LoadLibrary 51 | """ 52 | 53 | missing = f"tests/nonexistent_{conv:s}_{arch:s}.dll" 54 | 55 | assert not os.path.exists(missing) 56 | 57 | with pytest.raises(OSError): 58 | _ = getattr(ctypes, conv).LoadLibrary(missing) 59 | 60 | 61 | @pytest.mark.parametrize( 62 | "arch,conv,ctypes,dll_path", get_context(__file__, handle=False) 63 | ) 64 | def test_missingdll_attr(arch, conv, ctypes, dll_path): 65 | """ 66 | DLL file does not exist, via attribute 67 | """ 68 | 69 | missing = f"nonexistent_{conv:s}_{arch:s}_attr" 70 | 71 | with pytest.raises(OSError): 72 | _ = getattr(ctypes.cdll, missing) 73 | -------------------------------------------------------------------------------- /benchmark/minimal.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | 5 | ZUGBRUECKE 6 | Calling routines in Windows DLLs from Python scripts running on unixlike systems 7 | https://github.com/pleiszenburg/zugbruecke 8 | 9 | benchmark/minimal.py: Minimal call, two parameters, no pointers or memsync 10 | 11 | Required to run on platform / side: [UNIX, WINE] 12 | 13 | Copyright (C) 2017-2023 Sebastian M. Ernst 14 | 15 | 16 | The contents of this file are subject to the GNU Lesser General Public License 17 | Version 2.1 ("LGPL" or "License"). You may not use this file except in 18 | compliance with the License. You may obtain a copy of the License at 19 | https://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt 20 | https://github.com/pleiszenburg/zugbruecke/blob/master/LICENSE 21 | 22 | Software distributed under the License is distributed on an "AS IS" basis, 23 | WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the 24 | specific language governing rights and limitations under the License. 25 | 26 | 27 | """ 28 | 29 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 30 | # C 31 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 32 | 33 | HEADER = """ 34 | {{ PREFIX }} int {{ SUFFIX }} add_int( 35 | int a, 36 | int b 37 | ); 38 | """ 39 | 40 | SOURCE = """ 41 | {{ PREFIX }} int {{ SUFFIX }} add_int( 42 | int a, 43 | int b 44 | ) 45 | { 46 | return a + b; 47 | } 48 | """ 49 | 50 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 51 | # IMPORT 52 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 53 | 54 | from tests.lib.benchmark import benchmark 55 | 56 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 57 | # BENCHMARK(s) 58 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 59 | 60 | 61 | def init(ctypes, dll_handle, conv): 62 | 63 | add_int = dll_handle.add_int 64 | add_int.argtypes = (ctypes.c_int, ctypes.c_int) 65 | add_int.restype = ctypes.c_int 66 | 67 | return add_int 68 | 69 | 70 | @benchmark(fn = __file__, initializer = init) 71 | def minimal(ctypes, func): 72 | """ 73 | The "minimal" benchmark is a simple function call with 74 | two ``c_int`` parameters and a single ``c_int`` return value. 75 | The DLL function simply adds the two numbers and returns the result. 76 | """ 77 | 78 | x, y = 3, 4 79 | z = func(x, y) 80 | assert z == 7 81 | -------------------------------------------------------------------------------- /src/zugbruecke/core/cache.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | 5 | ZUGBRUECKE 6 | Calling routines in Windows DLLs from Python scripts running on unixlike systems 7 | https://github.com/pleiszenburg/zugbruecke 8 | 9 | src/zugbruecke/core/cache.py: Cached types and handles 10 | 11 | Required to run on platform / side: [UNIX, WINE] 12 | 13 | Copyright (C) 2017-2023 Sebastian M. Ernst 14 | 15 | 16 | The contents of this file are subject to the GNU Lesser General Public License 17 | Version 2.1 ("LGPL" or "License"). You may not use this file except in 18 | compliance with the License. You may obtain a copy of the License at 19 | https://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt 20 | https://github.com/pleiszenburg/zugbruecke/blob/master/LICENSE 21 | 22 | Software distributed under the License is distributed on an "AS IS" basis, 23 | WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the 24 | specific language governing rights and limitations under the License. 25 | 26 | 27 | """ 28 | 29 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 30 | # IMPORT 31 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 32 | 33 | from ctypes import _FUNCFLAG_CDECL 34 | from typing import Dict 35 | 36 | from .abc import CacheABC 37 | from .const import _FUNCFLAG_STDCALL 38 | from .typeguard import typechecked 39 | 40 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 41 | # CLASS 42 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 43 | 44 | @typechecked 45 | class Cache(CacheABC): 46 | """ 47 | Holding struct types, function types and function handles 48 | """ 49 | 50 | def __init__(self): 51 | 52 | self._cdecl = {} 53 | self._stdcall = {} 54 | self._struct = {} 55 | self._handle = {} 56 | 57 | @property 58 | def cdecl(self) -> Dict: 59 | 60 | return self._cdecl 61 | 62 | @property 63 | def stdcall(self) -> Dict: 64 | 65 | return self._stdcall 66 | 67 | @property 68 | def struct(self) -> Dict: 69 | 70 | return self._struct 71 | 72 | @property 73 | def handle(self) -> Dict: 74 | 75 | return self._handle 76 | 77 | def by_conv(self, flag: int) -> Dict: 78 | """ 79 | By calling convention flag 80 | """ 81 | 82 | if flag == _FUNCFLAG_CDECL: 83 | return self._cdecl 84 | 85 | if flag == _FUNCFLAG_STDCALL: 86 | return self._stdcall 87 | 88 | raise ValueError(f'unknown flag "{flag}"') 89 | -------------------------------------------------------------------------------- /src/zugbruecke/core/abc.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | 5 | ZUGBRUECKE 6 | Calling routines in Windows DLLs from Python scripts running on unixlike systems 7 | https://github.com/pleiszenburg/zugbruecke 8 | 9 | src/zugbruecke/core/abc.py: Abstract base classes 10 | 11 | Required to run on platform / side: [UNIX, WINE] 12 | 13 | Copyright (C) 2017-2023 Sebastian M. Ernst 14 | 15 | 16 | The contents of this file are subject to the GNU Lesser General Public License 17 | Version 2.1 ("LGPL" or "License"). You may not use this file except in 18 | compliance with the License. You may obtain a copy of the License at 19 | https://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt 20 | https://github.com/pleiszenburg/zugbruecke/blob/master/LICENSE 21 | 22 | Software distributed under the License is distributed on an "AS IS" basis, 23 | WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the 24 | specific language governing rights and limitations under the License. 25 | 26 | 27 | """ 28 | 29 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 30 | # IMPORT 31 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 32 | 33 | from abc import ABC 34 | 35 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 36 | # CLASSES 37 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 38 | 39 | class CacheABC(ABC): 40 | pass 41 | 42 | 43 | class CallbackClientABC(ABC): 44 | pass 45 | 46 | 47 | class CallbackServerABC(ABC): 48 | pass 49 | 50 | 51 | class ConfigABC(ABC): 52 | pass 53 | 54 | 55 | class CtypesSessionABC(ABC): 56 | pass 57 | 58 | 59 | class DataABC(ABC): 60 | pass 61 | 62 | 63 | class DefinitionABC(ABC): 64 | pass 65 | 66 | 67 | class DefinitionMemsyncABC(ABC): 68 | pass 69 | 70 | 71 | class DllClientABC(ABC): 72 | pass 73 | 74 | 75 | class DllServerABC(ABC): 76 | pass 77 | 78 | 79 | class InterpreterABC(ABC): 80 | pass 81 | 82 | 83 | class LogABC(ABC): 84 | pass 85 | 86 | 87 | class MempkgABC(ABC): 88 | pass 89 | 90 | 91 | class MessageABC(ABC): 92 | pass 93 | 94 | 95 | class RoutineClientABC(ABC): 96 | pass 97 | 98 | 99 | class RoutineServerABC(ABC): 100 | pass 101 | 102 | 103 | class RpcClientABC(ABC): 104 | pass 105 | 106 | 107 | class RpcServerABC(ABC): 108 | pass 109 | 110 | 111 | class SessionClientABC(ABC): 112 | pass 113 | 114 | 115 | class SessionServerABC(ABC): 116 | pass 117 | -------------------------------------------------------------------------------- /tests/lib/pythonversion.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | 5 | ZUGBRUECKE 6 | Calling routines in Windows DLLs from Python scripts running on unixlike systems 7 | https://github.com/pleiszenburg/zugbruecke 8 | 9 | tests/lib/pythonversion.py: IO for wenv's PythonVersion class 10 | 11 | Required to run on platform / side: [UNIX, WINE] 12 | 13 | Copyright (C) 2017-2023 Sebastian M. Ernst 14 | 15 | 16 | The contents of this file are subject to the GNU Lesser General Public License 17 | Version 2.1 ("LGPL" or "License"). You may not use this file except in 18 | compliance with the License. You may obtain a copy of the License at 19 | https://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt 20 | https://github.com/pleiszenburg/zugbruecke/blob/master/LICENSE 21 | 22 | Software distributed under the License is distributed on an "AS IS" basis, 23 | WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the 24 | specific language governing rights and limitations under the License. 25 | 26 | 27 | """ 28 | 29 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 30 | # IMPORT 31 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 32 | 33 | from json import dumps, loads 34 | from typing import Dict, List 35 | 36 | from typeguard import typechecked 37 | from wenv import PythonVersion 38 | 39 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 40 | # ROUTINES 41 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 42 | 43 | 44 | @typechecked 45 | def read_python_builds(fn: str) -> Dict[str, List[PythonVersion]]: 46 | """ 47 | Read JSON file containing list of installed Windows Python builds 48 | 49 | Args: 50 | - fn: Location of JSON file 51 | Returns: 52 | Available Windows Python builds in Wine Python environments 53 | """ 54 | 55 | with open(fn, mode = "r", encoding="utf-8") as f: 56 | raw = f.read() 57 | 58 | return { 59 | arch: [PythonVersion.from_config(arch, build) for build in builds] 60 | for arch, builds in loads(raw).items() 61 | } 62 | 63 | 64 | @typechecked 65 | def write_python_builds(fn: str, builds: Dict[str, List[PythonVersion]]): 66 | """ 67 | Read JSON file containing list of installed Windows Python builds 68 | 69 | Args: 70 | - Available Windows Python builds in Wine Python environments 71 | """ 72 | 73 | with open(fn, mode = "w", encoding="utf-8") as f: 74 | f.write(dumps({ 75 | arch: [ 76 | build.as_config() for build in _builds 77 | ] for arch, _builds in builds.items() 78 | })) 79 | -------------------------------------------------------------------------------- /src/zugbruecke/core/memory.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | 5 | ZUGBRUECKE 6 | Calling routines in Windows DLLs from Python scripts running on unixlike systems 7 | https://github.com/pleiszenburg/zugbruecke 8 | 9 | src/zugbruecke/core/memory.py: Handles memory transfers between both sides 10 | 11 | Required to run on platform / side: [UNIX, WINE] 12 | 13 | Copyright (C) 2017-2023 Sebastian M. Ernst 14 | 15 | 16 | The contents of this file are subject to the GNU Lesser General Public License 17 | Version 2.1 ("LGPL" or "License"). You may not use this file except in 18 | compliance with the License. You may obtain a copy of the License at 19 | https://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt 20 | https://github.com/pleiszenburg/zugbruecke/blob/master/LICENSE 21 | 22 | Software distributed under the License is distributed on an "AS IS" basis, 23 | WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the 24 | specific language governing rights and limitations under the License. 25 | 26 | 27 | """ 28 | 29 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 30 | # IMPORT 31 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 32 | 33 | from typing import Any 34 | import ctypes 35 | 36 | from .typeguard import typechecked 37 | 38 | 39 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 40 | # ROUTINES 41 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 42 | 43 | 44 | @typechecked 45 | def is_null_pointer(ptr: Any) -> bool: 46 | """ 47 | Args: 48 | - ptr: ctypes pointer object 49 | Returns: 50 | Is it null? 51 | """ 52 | 53 | try: 54 | return ctypes.cast(ptr, ctypes.c_void_p).value is None 55 | except ctypes.ArgumentError: # catch non-pointer arguments 56 | return False 57 | 58 | 59 | @typechecked 60 | def strip_pointer(ptr: Any) -> Any: 61 | """ 62 | Args: 63 | - ptr: ctypes pointer object 64 | Returns: 65 | ctypes object, extracted from pointer 66 | """ 67 | 68 | # Handle pointer object 69 | if hasattr(ptr, "contents"): 70 | return ptr.contents 71 | 72 | # Handle reference (byref) 'light pointer' 73 | if hasattr(ptr, "_obj"): 74 | return ptr._obj 75 | 76 | # Object was likely not provided as a pointer 77 | return ptr 78 | 79 | 80 | @typechecked 81 | def strip_simplecdata(item: Any) -> Any: 82 | """ 83 | Args: 84 | - item: potentially some ctypes SimpleCData object 85 | Returns: 86 | Raw value, extracted from ctypes SimpleCData object 87 | """ 88 | 89 | return getattr(item, "value", item) 90 | -------------------------------------------------------------------------------- /docs/source/faq.rst: -------------------------------------------------------------------------------- 1 | :github_url: 2 | 3 | .. _FAQ: 4 | 5 | FAQ 6 | === 7 | 8 | Why? Seriously, why? 9 | -------------------- 10 | 11 | Good question. Have a look at the :ref:`motivation ` section in the introduction. 12 | 13 | What are actual use cases for this project? 14 | ------------------------------------------- 15 | 16 | Read the section on :ref:`use cases ` in the introduction. 17 | 18 | How does it work? 19 | ----------------- 20 | 21 | Have a closer look at the section describing the :ref:`implementation ` in the introduction. 22 | 23 | Is it secure? 24 | ------------- 25 | 26 | No. See :ref:`chapter on security `. 27 | 28 | How fast/slow is it? 29 | -------------------- 30 | 31 | It performs reasonably well. See :ref:`benchmark section `. 32 | 33 | Can it handle structures? 34 | ------------------------- 35 | 36 | Yes, in principle. Though, limitations apply. See next question for details. 37 | 38 | Can it handle pointers? 39 | ----------------------- 40 | 41 | Yes and no. 42 | 43 | Pointers to simple C data types (int, float, etc.) used as function parameters or within structures can be handled just fine. 44 | 45 | Pointers to arbitrary data structures can be handled if another parameter of the call contains the length of the memory section the pointer is pointing to or if it is a zero/null-terminated string buffer. *zugbruecke* uses a special ``memsync`` protocol for indicating which memory sections must be kept in sync between the *Unix* and the *Wine* side of the code. If executed on *Windows*, the regular *ctypes* will just ignore any ``memsync`` directive in the code. See :ref:`chapter on memsync ` for details. 46 | 47 | How are integer widths handled for long integer types? 48 | ------------------------------------------------------ 49 | 50 | *zugbruecke* sticks to the Unix-specific width of long integer types. For details, see the :ref:`introduction to integer width handling `. 51 | 52 | What about long doubles and half precision? 53 | ------------------------------------------- 54 | 55 | They are basically not supported, see :ref:`introduction to floating point handling `. 56 | 57 | Is it thread-safe? 58 | ------------------ 59 | 60 | Probably (yes). More extensive tests are required. 61 | 62 | If you want to be on the safe side, start one *zugbruecke* session per thread in your code manually. You can do this as follows: 63 | 64 | .. code:: python 65 | 66 | from zugbruecke import CtypesSession 67 | # start new thread or process (multiprocessing) - then, inside, do: 68 | a = CtypesSession() 69 | # now you can do stuff like 70 | kernel32 = a.cdll.kernel32 71 | # do not forget to terminate the session (i.e. the Windows Python interpreter) 72 | a.zb_terminate() 73 | 74 | Something is off by 8 bytes. What happened? 75 | ------------------------------------------- 76 | 77 | You (probably) used the wrong :ref:`calling convention `. 78 | -------------------------------------------------------------------------------- /tests/lib/param.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | 5 | ZUGBRUECKE 6 | Calling routines in Windows DLLs from Python scripts running on unixlike systems 7 | https://github.com/pleiszenburg/zugbruecke 8 | 9 | tests/lib/param.py: Providing test parameters and helpers 10 | 11 | Required to run on platform / side: [UNIX, WINE] 12 | 13 | Copyright (C) 2017-2023 Sebastian M. Ernst 14 | 15 | 16 | The contents of this file are subject to the GNU Lesser General Public License 17 | Version 2.1 ("LGPL" or "License"). You may not use this file except in 18 | compliance with the License. You may obtain a copy of the License at 19 | https://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt 20 | https://github.com/pleiszenburg/zugbruecke/blob/master/LICENSE 21 | 22 | Software distributed under the License is distributed on an "AS IS" basis, 23 | WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the 24 | specific language governing rights and limitations under the License. 25 | 26 | 27 | """ 28 | 29 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 30 | # IMPORT 31 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 32 | 33 | from typing import Dict 34 | 35 | from typeguard import typechecked 36 | 37 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 38 | # CONST / CONFIG 39 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 40 | 41 | MAX_EXAMPLES = 300 42 | 43 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 44 | # ROUTINES 45 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 46 | 47 | 48 | @typechecked 49 | def get_int_limits(bits: int, sign: bool = True) -> Dict[str, int]: 50 | """ 51 | computes upper and lower limits of integers of a given number of bits 52 | 53 | Args: 54 | - bits: Number of bits 55 | - sign: Indicate signed integer 56 | Returns 57 | Dict of min and max values 58 | """ 59 | 60 | assert bits in (8, 16, 32, 64) 61 | 62 | if sign: 63 | return {"min_value": -1 * 2 ** (bits - 1), "max_value": 2 ** (bits - 1) - 1} 64 | return {"min_value": 0, "max_value": 2 ** bits - 1} 65 | 66 | 67 | @typechecked 68 | def force_int_overflow(value: int, bits: int, sign: bool) -> int: 69 | """ 70 | Emulate C-like integer overflow in Python 71 | 72 | Args: 73 | - value: Cut off overflow bits from this integer 74 | - bits: Emulate an integer of this number of bits 75 | - sign: Indicate the emulation of a signed integer 76 | """ 77 | 78 | assert bits in (8, 16, 32, 64) 79 | 80 | int_limits = get_int_limits(bits, sign) 81 | while value > int_limits["max_value"]: 82 | value -= 2 ** bits 83 | while value < int_limits["min_value"]: 84 | value += 2 ** bits 85 | return value 86 | -------------------------------------------------------------------------------- /src/zugbruecke/_server_.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env wine-python 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | 6 | ZUGBRUECKE 7 | Calling routines in Windows DLLs from Python scripts running on unixlike systems 8 | https://github.com/pleiszenburg/zugbruecke 9 | 10 | src/zugbruecke/_server_.py: Started with Python on Wine, executing DLL calls 11 | 12 | Required to run on platform / side: [WINE] 13 | 14 | Copyright (C) 2017-2023 Sebastian M. Ernst 15 | 16 | 17 | The contents of this file are subject to the GNU Lesser General Public License 18 | Version 2.1 ("LGPL" or "License"). You may not use this file except in 19 | compliance with the License. You may obtain a copy of the License at 20 | https://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt 21 | https://github.com/pleiszenburg/zugbruecke/blob/master/LICENSE 22 | 23 | Software distributed under the License is distributed on an "AS IS" basis, 24 | WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the 25 | specific language governing rights and limitations under the License. 26 | 27 | 28 | """ 29 | 30 | 31 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 32 | # IMPORT 33 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 34 | 35 | import argparse 36 | 37 | from .core.config import Config 38 | from .core.session_server import SessionServer 39 | 40 | 41 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 42 | # ROUTINES 43 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 44 | 45 | 46 | def run(): 47 | 48 | # Parse arguments comming from unix side 49 | parser = argparse.ArgumentParser() 50 | parser.add_argument("--id", type=str, nargs=1) 51 | parser.add_argument("--port_socket_unix", type=int, nargs=1) 52 | parser.add_argument("--port_socket_wine", type=int, nargs=1) 53 | parser.add_argument("--log_level", type=int, nargs=1) 54 | parser.add_argument("--log_write", type=int, nargs=1) 55 | parser.add_argument("--timeout_start", type=float, nargs=1) 56 | args = parser.parse_args() 57 | 58 | # Generate parameter dict 59 | parameter = { 60 | "id": args.id[0], 61 | "platform": "WINE", 62 | "stdout": False, 63 | "stderr": False, 64 | "log_write": bool(args.log_write[0]), 65 | "log_level": args.log_level[0], 66 | "port_socket_wine": args.port_socket_wine[0], 67 | "port_socket_unix": args.port_socket_unix[0], 68 | "timeout_start": args.timeout_start[0], 69 | } 70 | 71 | # Fire up wine server session with parsed parameters 72 | _ = SessionServer(Config(**parameter)) 73 | 74 | 75 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 76 | # MAIN 77 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 78 | 79 | if __name__ == "__main__": 80 | 81 | run() 82 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["flit_core >=3.2,<4"] 3 | build-backend = "flit_core.buildapi" 4 | 5 | [project] 6 | name = "zugbruecke" 7 | description = "Calling routines in Windows DLLs from Python scripts running under Linux, MacOS or BSD" 8 | authors = [{name = "Sebastian M. Ernst", email = "ernst@pleiszenburg.de"}] 9 | maintainers = [{name = "Sebastian M. Ernst", email = "ernst@pleiszenburg.de"}] 10 | keywords = ["ctypes", "wine"] 11 | readme = "README.md" 12 | license = {file = "LICENSE"} 13 | requires-python = ">=3.7" 14 | dependencies = ["wenv >=0.5.1,<0.6.0"] 15 | classifiers = [ 16 | "Development Status :: 3 - Alpha", 17 | "Intended Audience :: Developers", 18 | "Intended Audience :: Information Technology", 19 | "Intended Audience :: Science/Research", 20 | "Intended Audience :: System Administrators", 21 | "License :: OSI Approved :: GNU Lesser General Public License v2 (LGPLv2)", 22 | "Operating System :: MacOS", 23 | "Operating System :: POSIX :: BSD", 24 | "Operating System :: POSIX :: Linux", 25 | "Programming Language :: Python :: 3", 26 | "Programming Language :: Python :: 3.7", 27 | "Programming Language :: Python :: 3.8", 28 | "Programming Language :: Python :: 3.9", 29 | "Programming Language :: Python :: 3.10", 30 | "Programming Language :: Python :: 3.11", 31 | "Programming Language :: Python :: 3 :: Only", 32 | "Programming Language :: Python :: Implementation :: CPython", 33 | "Topic :: Scientific/Engineering", 34 | "Topic :: Software Development", 35 | "Topic :: System :: Operating System", 36 | "Topic :: System :: Operating System Kernels", 37 | "Topic :: Utilities", 38 | ] 39 | dynamic = ["version"] 40 | 41 | [project.optional-dependencies] 42 | dev = [ 43 | "flit", 44 | "Jinja2", 45 | "python-lsp-server[all]", 46 | "Sphinx", 47 | "sphinx_rtd_theme", 48 | "sphinx-autodoc-typehints", 49 | "myst-parser", 50 | "toml", 51 | "twine", 52 | ] 53 | test = [ 54 | "hypothesis", 55 | "pytest<7.0", 56 | "coverage[toml]", 57 | "pytest-cov", 58 | "typeguard", 59 | "numpy", 60 | ] 61 | 62 | [project.urls] 63 | Home = "https://github.com/pleiszenburg/zugbruecke" 64 | Documentation = "https://zugbruecke.readthedocs.io/en/latest/" 65 | Source = "https://github.com/pleiszenburg/zugbruecke" 66 | 67 | [tool.black] 68 | target-version = ['py37'] 69 | include = '\.pyi?$' 70 | exclude = ''' 71 | /( 72 | \.eggs 73 | | \.git 74 | | \.venv 75 | | \.cache 76 | | \.hypothesis 77 | | \.ipynb_checkpoints 78 | | \.pytest_cache 79 | | .ropenproject 80 | | _build 81 | | build 82 | | buck-out 83 | | demo_dll 84 | | dist 85 | | env[0-9]{2} 86 | | env 87 | )/ 88 | ''' 89 | 90 | [tool.coverage.run] 91 | branch = true 92 | parallel = true 93 | relative_files = true 94 | source_pkgs = ["zugbruecke"] 95 | 96 | [tool.coverage.paths] 97 | source = [ 98 | "src/", 99 | "C:\\*\\Lib\\site-packages\\", 100 | ] 101 | 102 | [tool.pytest.ini_options] 103 | testpaths = ["tests"] 104 | -------------------------------------------------------------------------------- /makefile: -------------------------------------------------------------------------------- 1 | # ZUGBRUECKE 2 | # Calling routines in Windows DLLs from Python scripts running on unixlike systems 3 | # https://github.com/pleiszenburg/zugbruecke 4 | # 5 | # makefile: GNU makefile for project management 6 | # 7 | # Required to run on platform / side: [UNIX] 8 | # 9 | # Copyright (C) 2017-2023 Sebastian M. Ernst 10 | # 11 | # 12 | # The contents of this file are subject to the GNU Lesser General Public License 13 | # Version 2.1 ("LGPL" or "License"). You may not use this file except in 14 | # compliance with the License. You may obtain a copy of the License at 15 | # https://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt 16 | # https://github.com/pleiszenburg/zugbruecke/blob/master/LICENSE 17 | # 18 | # Software distributed under the License is distributed on an "AS IS" basis, 19 | # WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the 20 | # specific language governing rights and limitations under the License. 21 | # 22 | 23 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 24 | # LIB 25 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 26 | 27 | _clean_coverage: 28 | coverage erase 29 | 30 | _clean_dll: 31 | find src/ tests/ benchmark/ -name '*.dll' -exec rm -f {} + 32 | 33 | _clean_py: 34 | find src/ tests/ benchmark/ -name '*.pyc' -exec rm -f {} + 35 | find src/ tests/ benchmark/ -name '*.pyo' -exec rm -f {} + 36 | find src/ tests/ benchmark/ -name '*~' -exec rm -f {} + 37 | find src/ tests/ benchmark/ -name '__pycache__' -exec rm -fr {} + 38 | 39 | _clean_release: 40 | -rm -r build/* 41 | -rm -r dist/* 42 | 43 | # examples: 44 | # @(cd examples; make clean; make; make install) 45 | 46 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 47 | # ENTRY POINTS 48 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 49 | 50 | black: 51 | black . 52 | 53 | benchmark: 54 | make clean 55 | -rm benchmark/data.raw 56 | -rm docs/source/benchmark_*.rst 57 | python -m tests.lib.build benchmark 58 | python -m tests.lib.benchmark wine 59 | python -m tests.lib.benchmark unix 60 | python -m tests.lib.benchmark table 61 | make docs 62 | 63 | clean: 64 | make _clean_release 65 | make _clean_coverage 66 | make _clean_py 67 | make _clean_dll 68 | 69 | docs: 70 | @(cd docs; make clean; make html) 71 | 72 | install: 73 | pip install -U -e .[dev,test] 74 | python -m tests.lib.install 75 | 76 | release: 77 | make clean 78 | flit build 79 | gpg --detach-sign -a dist/zugbruecke*.whl 80 | gpg --detach-sign -a dist/zugbruecke*.tar.gz 81 | 82 | upload: 83 | for filename in $$(ls dist/*.tar.gz dist/*.whl) ; do \ 84 | twine upload $$filename $$filename.asc ; \ 85 | done 86 | 87 | test: 88 | make clean 89 | python -m tests.lib.build tests 90 | python -m tests.lib.run wine 91 | python -m tests.lib.run unix 92 | mv .coverage .coverage.00 ; coverage combine ; coverage html -i 93 | 94 | .PHONY: docs 95 | .PHONY: benchmark 96 | -------------------------------------------------------------------------------- /tests/lib/const.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | 5 | ZUGBRUECKE 6 | Calling routines in Windows DLLs from Python scripts running on unixlike systems 7 | https://github.com/pleiszenburg/zugbruecke 8 | 9 | tests/lib/const.py: Holds constant values, flags, types 10 | 11 | Required to run on platform / side: [UNIX, WINE] 12 | 13 | Copyright (C) 2017-2023 Sebastian M. Ernst 14 | 15 | 16 | The contents of this file are subject to the GNU Lesser General Public License 17 | Version 2.1 ("LGPL" or "License"). You may not use this file except in 18 | compliance with the License. You may obtain a copy of the License at 19 | https://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt 20 | https://github.com/pleiszenburg/zugbruecke/blob/master/LICENSE 21 | 22 | Software distributed under the License is distributed on an "AS IS" basis, 23 | WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the 24 | specific language governing rights and limitations under the License. 25 | 26 | 27 | """ 28 | 29 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 30 | # IMPORT / PLATFORM 31 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 32 | 33 | from platform import architecture 34 | from sys import platform 35 | 36 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 37 | # CONST 38 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 39 | 40 | ARCHITECTURE = architecture()[0][:2] 41 | 42 | if any([platform.startswith(os_name) for os_name in ["linux", "darwin", "freebsd"]]): 43 | PLATFORM = "unix" 44 | elif platform.startswith("win"): 45 | PLATFORM = "wine" 46 | else: 47 | raise SystemError("unsopported platform") 48 | 49 | HEADER_FN = "tmp_header.h" 50 | SOURCE_FN = "tmp_source.c" 51 | 52 | DLL_FLD = "dlls" 53 | 54 | DLL_HEADER = """ 55 | 56 | #ifndef TESTDLL_H 57 | #define TESTDLL_H 58 | 59 | #include 60 | #include 61 | #include 62 | #include 63 | #include 64 | #include 65 | 66 | typedef int32_t bool; 67 | #define TRUE 1 68 | #define FALSE 0 69 | 70 | {{ HEADER }} 71 | 72 | #endif 73 | 74 | """ 75 | 76 | DLL_SOURCE = """ 77 | 78 | #include "{{ HEADER_FN }}" 79 | 80 | {{ SOURCE }} 81 | 82 | """ 83 | 84 | ARCHS = ["win32", "win64"] 85 | CONVENTIONS = ["cdll", "windll"] 86 | 87 | PREFIX = { 88 | "cdll": "__declspec(dllexport)", 89 | "windll": "__declspec(dllexport)", 90 | } 91 | SUFFIX = { 92 | "cdll": "__cdecl", 93 | "windll": "__stdcall", 94 | } 95 | 96 | CC = { 97 | "win32": "i686-w64-mingw32-gcc", 98 | "win64": "x86_64-w64-mingw32-gcc", 99 | } 100 | _CFLAGS = ["-Wall", "-shared", "-std=c99"] 101 | CFLAGS = { 102 | "cdll": _CFLAGS + ["-Wl,--subsystem,windows"], 103 | "windll": _CFLAGS + ["-Wl,-add-stdcall-alias"], 104 | } 105 | LDFLAGS = [ 106 | "-lm", 107 | ] 108 | 109 | PYTHONBUILDS_FN = 'pythonbuilds.json' 110 | 111 | PYTHON_MINOR_MIN = 7 112 | PYTHON_MINOR_MAX = 11 113 | -------------------------------------------------------------------------------- /src/zugbruecke/core/path.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | 5 | ZUGBRUECKE 6 | Calling routines in Windows DLLs from Python scripts running on unixlike systems 7 | https://github.com/pleiszenburg/zugbruecke 8 | 9 | src/zugbruecke/core/path.py: Coverts paths fron Unix to Wine format and vv 10 | 11 | Required to run on platform / side: [WINE] 12 | 13 | Copyright (C) 2017-2023 Sebastian M. Ernst 14 | 15 | 16 | The contents of this file are subject to the GNU Lesser General Public License 17 | Version 2.1 ("LGPL" or "License"). You may not use this file except in 18 | compliance with the License. You may obtain a copy of the License at 19 | https://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt 20 | https://github.com/pleiszenburg/zugbruecke/blob/master/LICENSE 21 | 22 | Software distributed under the License is distributed on an "AS IS" basis, 23 | WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the 24 | specific language governing rights and limitations under the License. 25 | 26 | 27 | """ 28 | 29 | 30 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 31 | # IMPORT 32 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 33 | 34 | import ctypes 35 | from ctypes import wintypes 36 | 37 | from .errors import WineError 38 | from .typeguard import typechecked 39 | 40 | 41 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 42 | # PATH CLASS 43 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 44 | 45 | 46 | @typechecked 47 | class PathStyles: 48 | """ 49 | Coverts paths fron Unix to Wine format and vice versa 50 | """ 51 | 52 | def __init__(self): 53 | 54 | self._unix_to_wine = ctypes.cdll.kernel32.wine_get_dos_file_name 55 | self._unix_to_wine.argtypes = (wintypes.LPCSTR,) 56 | self._unix_to_wine.restype = wintypes.LPCWSTR 57 | 58 | self._wine_to_unix = ctypes.cdll.kernel32.wine_get_unix_file_name 59 | self._wine_to_unix.argtypes = (wintypes.LPCWSTR,) 60 | self._wine_to_unix.restype = wintypes.LPCSTR 61 | 62 | def unix_to_wine(self, in_path: str) -> str: 63 | """ 64 | In: Unix path 65 | Out: Wine path 66 | """ 67 | 68 | if not isinstance(in_path, str): 69 | raise TypeError("in_path must by of type str") 70 | 71 | out_path = self._unix_to_wine( 72 | ctypes.cast( 73 | ctypes.create_string_buffer(in_path.encode("utf-8")), wintypes.LPCSTR 74 | ) 75 | ) 76 | 77 | if out_path is None: 78 | raise WineError() 79 | 80 | return out_path 81 | 82 | def wine_to_unix(self, in_path: str) -> str: 83 | """ 84 | In: Wine path 85 | Out: Unix path 86 | """ 87 | 88 | if not isinstance(in_path, str): 89 | raise TypeError("in_path must by of type str") 90 | 91 | out_path = self._wine_to_unix( 92 | ctypes.cast(ctypes.create_unicode_buffer(in_path), wintypes.LPCWSTR) 93 | ) 94 | 95 | if out_path is None: 96 | raise WineError() 97 | 98 | return out_path.decode("utf-8") 99 | -------------------------------------------------------------------------------- /docs/source/installation.rst: -------------------------------------------------------------------------------- 1 | :github_url: 2 | 3 | .. _installation: 4 | 5 | .. index:: 6 | pair: pip; install 7 | triple: wine; linux; installation 8 | triple: wine; macos; installation 9 | triple: wine; bsd; installation 10 | 11 | Installation 12 | ============ 13 | 14 | Getting *Wine* 15 | -------------- 16 | 17 | For using *zugbruecke*, you need to install **Wine** first. Depending on your platform, there are different ways of doing that. 18 | 19 | * `Installation instructions for various Linux distributions`_ 20 | * `Installation instructions for MacOS`_ 21 | * `Installation instructions for FreeBSD`_ 22 | 23 | .. _Installation instructions for various Linux distributions: https://www.winehq.org/download 24 | .. _Installation instructions for MacOS: https://wiki.winehq.org/MacOS 25 | .. _Installation instructions for FreeBSD: https://wiki.winehq.org/FreeBSD 26 | 27 | .. note:: 28 | 29 | Currently, Wine >= 6.x is supported (tested). 30 | 31 | .. warning:: 32 | 33 | Calling into 32 bit DLLs requires a 32 bit version of *Wine*. As most Linux distributions have started to drop 32 bit support, 32 bit versions of *Wine* are now rarely shipped by default. On some Linux distributions, e.g. Ubuntu and derivatives, the installation of 32 bit packages must be explicitly activated before one can even attempt to install *Wine* for 32 bit. Please read related instructions for your Linux distribution carefully before proceeding. 34 | 35 | .. warning:: 36 | 37 | Support for MacOS and FreeBSD is provided on a best-effort basis. *zugbruecke* currently does not receive regular testing on those platforms. 38 | 39 | Getting *zugbruecke* 40 | -------------------- 41 | 42 | The latest (more or less) **stable release version** can be installed with *pip*: 43 | 44 | .. code:: bash 45 | 46 | pip install zugbruecke 47 | 48 | If you are interested in testing the latest work from the **development branch**, you can try it like this: 49 | 50 | .. code:: bash 51 | 52 | pip install git+https://github.com/pleiszenburg/zugbruecke.git@develop 53 | 54 | After installing the package with ``pip``, you may choose to manually :ref:`initialize the Wine Python environment ` by running ``wenv init``. If you choose not to do this, ``zugbruecke`` will take care of it during its first use. 55 | 56 | .. note:: 57 | 58 | If you are relying on *zugbruecke*, please notice that it uses **semantic versioning**. Breaking changes are indicated by increasing the second version number, the minor version. Going for example from 0.0.x to 0.1.y or going from 0.1.x to 0.2.y therefore indicates a breaking change. For as long as *zugbruecke* has development status "alpha", please expect more breaking changes to come. 59 | 60 | If you are encountering any problems, see :ref:`section on bugs and known issues `. 61 | 62 | .. warning:: 63 | 64 | CentOS is known to be problematic. It packages both CPython and Wine in rather unusual and partially incompatible ways. See section on :ref:`CentOS `. 65 | 66 | Installing *zugbruecke* in Development Mode 67 | ------------------------------------------- 68 | 69 | If you are interested in contributing to *zugbruecke*, you might want to install it in development mode. You can find the latest instructions on how to do this in the `CONTRIBUTING file`_ of this project on *Github*. 70 | 71 | .. _`CONTRIBUTING file`: https://github.com/pleiszenburg/zugbruecke/blob/develop/CONTRIBUTING.md 72 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | :github_url: 2 | 3 | .. image:: _static/logo.png 4 | :alt: zugbruecke 5 | 6 | zugbruecke 7 | ========== 8 | 9 | **Calling routines in Windows DLLs from Python scripts running under Linux, MacOS or BSD** 10 | 11 | */ˈt͡suːkˌbʁʏkə/* - German, noun, feminine: `drawbridge`_ 12 | 13 | .. _drawbridge: https://dict.leo.org/englisch-deutsch/zugbrücke 14 | 15 | .. |build_master| image:: https://github.com/pleiszenburg/zugbruecke/actions/workflows/test.yaml/badge.svg?branch=master 16 | :target: https://github.com/pleiszenburg/zugbruecke/actions/workflows/test.yaml 17 | :alt: Test Status: master / release 18 | .. |docs_master| image:: https://readthedocs.org/projects/zugbruecke/badge/?version=latest&style=flat-square 19 | :target: http://zugbruecke.readthedocs.io/en/latest/?badge=latest 20 | :alt: Documentation Status: master / release 21 | .. |license| image:: https://img.shields.io/pypi/l/zugbruecke.svg?style=flat-square 22 | :target: https://github.com/pleiszenburg/zugbruecke/blob/master/LICENSE 23 | :alt: Project License: LGPLv2 24 | .. |status| image:: https://img.shields.io/pypi/status/zugbruecke.svg?style=flat-square 25 | :target: https://github.com/pleiszenburg/zugbruecke/milestone/1 26 | :alt: Project Development Status 27 | .. |pypi_version| image:: https://img.shields.io/pypi/v/zugbruecke.svg?style=flat-square 28 | :target: https://pypi.python.org/pypi/zugbruecke 29 | :alt: Available on PyPi - the Python Package Index 30 | .. |pypi_versions| image:: https://img.shields.io/pypi/pyversions/zugbruecke.svg?style=flat-square 31 | :target: https://pypi.python.org/pypi/zugbruecke 32 | :alt: Available on PyPi - the Python Package Index 33 | .. |chat| image:: https://img.shields.io/matrix/zugbruecke:matrix.org.svg?style=flat-square 34 | :target: https://matrix.to/#/#zugbruecke:matrix.org 35 | :alt: Matrix Chat Room 36 | .. |mailing_list| image:: https://img.shields.io/badge/mailing%20list-groups.io-8cbcd1.svg?style=flat-square 37 | :target: https://groups.io/g/zugbruecke-dev 38 | :alt: Mailing List 39 | 40 | |build_master| |docs_master| |license| |status| |pypi_version| |pypi_versions| |chat| |mailing_list| 41 | 42 | User's guide 43 | ------------ 44 | 45 | *zugbruecke* is a drop-in replacement for *ctypes* with minimal, *ctypes*-compatible syntax extensions. 46 | 47 | .. warning:: 48 | 49 | This manual describes what makes *zugbruecke* special and how it differs from *ctypes*. It does **NOT** substitute the `ctypes documentation`_. Please read the latter first if you have never called foreign functions with *ctypes* from *Python* scripts. 50 | 51 | .. _ctypes documentation: https://docs.python.org/3/library/ctypes.html?highlight=ctypes#module-ctypes 52 | 53 | .. toctree:: 54 | :maxdepth: 2 55 | :caption: Introduction 56 | 57 | introduction 58 | installation 59 | examples 60 | 61 | .. toctree:: 62 | :maxdepth: 2 63 | :caption: Reference 64 | 65 | session 66 | configuration 67 | memsync 68 | interoperability 69 | wineenv 70 | 71 | .. toctree:: 72 | :maxdepth: 2 73 | :caption: Advanced 74 | 75 | benchmarks 76 | security 77 | bugs 78 | changes 79 | faq 80 | support 81 | 82 | `Interested in contributing?`_ 83 | 84 | .. _Interested in contributing?: https://github.com/pleiszenburg/zugbruecke/blob/develop/CONTRIBUTING.md 85 | 86 | Indices and tables 87 | ------------------ 88 | 89 | * :ref:`genindex` 90 | * :ref:`search` 91 | -------------------------------------------------------------------------------- /tests/lib/parser.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | 5 | ZUGBRUECKE 6 | Calling routines in Windows DLLs from Python scripts running on unixlike systems 7 | https://github.com/pleiszenburg/zugbruecke 8 | 9 | tests/lib/parser.py: Extracting information from Python code without importing it 10 | 11 | Required to run on platform / side: [UNIX, WINE] 12 | 13 | Copyright (C) 2017-2023 Sebastian M. Ernst 14 | 15 | 16 | The contents of this file are subject to the GNU Lesser General Public License 17 | Version 2.1 ("LGPL" or "License"). You may not use this file except in 18 | compliance with the License. You may obtain a copy of the License at 19 | https://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt 20 | https://github.com/pleiszenburg/zugbruecke/blob/master/LICENSE 21 | 22 | Software distributed under the License is distributed on an "AS IS" basis, 23 | WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the 24 | specific language governing rights and limitations under the License. 25 | 26 | 27 | """ 28 | 29 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 30 | # IMPORT 31 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 32 | 33 | import ast 34 | from typing import Any, Dict, Optional 35 | 36 | from typeguard import typechecked 37 | 38 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 39 | # ROUTINES 40 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 41 | 42 | 43 | @typechecked 44 | def get_vars_from_source( 45 | src: str, 46 | *names: str, 47 | default: Optional[Any] = None, 48 | ) -> Dict[str, Optional[Any]]: 49 | """ 50 | Extract variables from code by variable name 51 | 52 | Args: 53 | - src: Raw source code 54 | - names: Variable names 55 | - default: Default value of variable is not present in code 56 | """ 57 | 58 | tree = ast.parse(src) 59 | out = {name: default for name in names} 60 | for item in tree.body: 61 | if not isinstance(item, ast.Assign): 62 | continue 63 | for target in item.targets: 64 | if target.id not in names: 65 | continue 66 | out[target.id] = _parse_tree(item.value) 67 | return out 68 | 69 | 70 | @typechecked 71 | def _parse_tree(leaf: Any) -> Any: 72 | """ 73 | Recursively walk leafs of abstract syntax tree 74 | """ 75 | 76 | if isinstance(leaf, ast.Str) or isinstance(leaf, ast.Bytes): 77 | return leaf.s 78 | if isinstance(leaf, ast.Num): 79 | return leaf.n 80 | if isinstance(leaf, ast.NameConstant): 81 | return leaf.value 82 | if isinstance(leaf, ast.Dict): 83 | return { 84 | _parse_tree(leaf_key): _parse_tree(leaf_value) 85 | for leaf_key, leaf_value in zip(leaf.keys, leaf.values) 86 | } 87 | if isinstance(leaf, ast.List): 88 | return [_parse_tree(leaf_item) for leaf_item in leaf.elts] 89 | if isinstance(leaf, ast.Tuple): 90 | return tuple([_parse_tree(leaf_item) for leaf_item in leaf.elts]) 91 | if isinstance(leaf, ast.Set): 92 | return {_parse_tree(leaf_item) for leaf_item in leaf.elts} 93 | 94 | raise SyntaxError(f"unhandled type: {str(leaf):s} ({str(dir(leaf)):s})") 95 | -------------------------------------------------------------------------------- /tests/test_paths.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | 5 | ZUGBRUECKE 6 | Calling routines in Windows DLLs from Python scripts running on unixlike systems 7 | https://github.com/pleiszenburg/zugbruecke 8 | 9 | tests/test_paths.py: Converting paths between Unix and Wine/Windows formats 10 | 11 | Required to run on platform / side: [UNIX, WINE] 12 | 13 | Copyright (C) 2017-2023 Sebastian M. Ernst 14 | 15 | 16 | The contents of this file are subject to the GNU Lesser General Public License 17 | Version 2.1 ("LGPL" or "License"). You may not use this file except in 18 | compliance with the License. You may obtain a copy of the License at 19 | https://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt 20 | https://github.com/pleiszenburg/zugbruecke/blob/master/LICENSE 21 | 22 | Software distributed under the License is distributed on an "AS IS" basis, 23 | WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the 24 | specific language governing rights and limitations under the License. 25 | 26 | 27 | """ 28 | 29 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 30 | # IMPORT 31 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 32 | 33 | import pytest 34 | 35 | from .lib.ctypes import get_context, PLATFORM 36 | 37 | if PLATFORM == "unix": 38 | from zugbruecke.core.errors import WineError 39 | 40 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 41 | # TEST(s) 42 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 43 | 44 | 45 | @pytest.mark.skipif(PLATFORM != "unix", reason="only relevant for unix side") 46 | @pytest.mark.parametrize( 47 | "arch,conv,ctypes,dll_path", get_context(__file__, handle=False) 48 | ) 49 | def test_path_unix_to_wine_abs(arch, conv, ctypes, dll_path): 50 | 51 | PATH_A = ("/foo/bar", "Z:\\foo\\bar") 52 | assert ctypes.zb_path_unix_to_wine(PATH_A[0]) == PATH_A[1] 53 | 54 | 55 | @pytest.mark.skipif(PLATFORM != "unix", reason="only relevant for unix side") 56 | @pytest.mark.parametrize( 57 | "arch,conv,ctypes,dll_path", get_context(__file__, handle=False) 58 | ) 59 | def test_path_unix_to_wine_rel(arch, conv, ctypes, dll_path): 60 | 61 | PATH_A = ("foo/bar", "foo\\bar") 62 | path_out = ctypes.zb_path_unix_to_wine(PATH_A[0]) 63 | assert path_out[:3] == "Z:\\" 64 | assert path_out.endswith(PATH_A[1]) 65 | assert len(path_out) > len(PATH_A[1]) + len("Z:\\") 66 | 67 | 68 | @pytest.mark.skipif(PLATFORM != "unix", reason="only relevant for unix side") 69 | @pytest.mark.parametrize( 70 | "arch,conv,ctypes,dll_path", get_context(__file__, handle=False) 71 | ) 72 | def test_path_wine_to_unix_abs(arch, conv, ctypes, dll_path): 73 | 74 | PATH_A = "C:\\" 75 | path_out = ctypes.zb_path_wine_to_unix(PATH_A) 76 | assert path_out.startswith("/") 77 | assert path_out.endswith("/c:/") 78 | assert len(path_out) > len("/") + len("/c:/") 79 | 80 | 81 | @pytest.mark.skipif(PLATFORM != "unix", reason="only relevant for unix side") 82 | @pytest.mark.parametrize( 83 | "arch,conv,ctypes,dll_path", get_context(__file__, handle=False) 84 | ) 85 | def test_path_wine_to_unix_fail(arch, conv, ctypes, dll_path): 86 | 87 | PATH_A = "a" * 270 88 | with pytest.raises(WineError): 89 | _ = ctypes.zb_path_wine_to_unix(PATH_A) 90 | -------------------------------------------------------------------------------- /tests/test_float_types.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | 5 | ZUGBRUECKE 6 | Calling routines in Windows DLLs from Python scripts running on unixlike systems 7 | https://github.com/pleiszenburg/zugbruecke 8 | 9 | tests/test_float_types.py: Tests by value argument passing and return value (float) 10 | 11 | Required to run on platform / side: [UNIX, WINE] 12 | 13 | Copyright (C) 2017-2023 Sebastian M. Ernst 14 | 15 | 16 | The contents of this file are subject to the GNU Lesser General Public License 17 | Version 2.1 ("LGPL" or "License"). You may not use this file except in 18 | compliance with the License. You may obtain a copy of the License at 19 | https://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt 20 | https://github.com/pleiszenburg/zugbruecke/blob/master/LICENSE 21 | 22 | Software distributed under the License is distributed on an "AS IS" basis, 23 | WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the 24 | specific language governing rights and limitations under the License. 25 | 26 | 27 | """ 28 | 29 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 30 | # C 31 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 32 | 33 | HEADER = """ 34 | {% for DTYPE, _, _ in DTYPES %} 35 | {{ PREFIX }} int {{ SUFFIX }} in_mandel_{{ DTYPE }}( 36 | {{ DTYPE }} x0, 37 | {{ DTYPE }} y0, 38 | int n 39 | ); 40 | {% endfor %} 41 | """ 42 | 43 | SOURCE = """ 44 | {% for DTYPE, _, _ in DTYPES %} 45 | {{ PREFIX }} int {{ SUFFIX }} in_mandel_{{ DTYPE }}( 46 | {{ DTYPE }} x0, 47 | {{ DTYPE }} y0, 48 | int n 49 | ) 50 | { 51 | /* Test if (x0,y0) is in the Mandelbrot set or not */ 52 | {{ DTYPE }} x = 0, y = 0, xtemp; 53 | while (n > 0) 54 | { 55 | xtemp = x * x - y * y + x0; 56 | y = 2 * x * y + y0; 57 | x = xtemp; 58 | n -= 1; 59 | if (x * x + y * y > 4) return 0; 60 | } 61 | return 1; 62 | } 63 | {% endfor %} 64 | """ 65 | 66 | EXTRA = { 67 | "DTYPES": [ 68 | ("float", "FLT_MIN", "FLT_MAX"), 69 | ("double", "DBL_MIN", "DBL_MAX"), 70 | ] 71 | } 72 | 73 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 74 | # IMPORT 75 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 76 | 77 | from .lib.ctypes import get_context 78 | 79 | import pytest 80 | 81 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 82 | # TEST(s) 83 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 84 | 85 | 86 | @pytest.mark.parametrize("arch,conv,ctypes,dll_handle", get_context(__file__)) 87 | @pytest.mark.parametrize("dtype", ['float', 'double']) 88 | def test_float_types(dtype, arch, conv, ctypes, dll_handle): 89 | """ 90 | Testing float arguments for multiple float types 91 | """ 92 | 93 | c_type = getattr(ctypes, f'c_{dtype:s}') 94 | 95 | in_mandel_dll = getattr(dll_handle, f'in_mandel_{dtype:s}') 96 | in_mandel_dll.argtypes = (c_type, c_type, ctypes.c_int) 97 | in_mandel_dll.restype = ctypes.c_int 98 | 99 | assert 1 == in_mandel_dll(0.0, 0.0, 500) 100 | assert 0 == in_mandel_dll(2.0, 1.0, 500) 101 | -------------------------------------------------------------------------------- /docs/source/memsyncintro.rst: -------------------------------------------------------------------------------- 1 | :github_url: 2 | 3 | .. _memsyncintro: 4 | 5 | Introduction to Memory Synchronization 6 | ====================================== 7 | 8 | Because *zugbruecke* runs code in a separate *Windows Python* interpreter on *Wine*, pointers pose a special kind of problem. They can, unfortunately, not be passed from the code running on the *Unix* side to the code running on the *Wine* side or vice versa "as is" thanks to `memory protection`_. This is why pointers must be translated and data in memory kept in sync on both sides. In many cases, *zugbruecke* can do this fully automatically. There are cases however where memory synchronization must be controlled by the user through the ``memsync`` protocol. 9 | 10 | .. note:: 11 | 12 | ``memsync`` implements special directives, which extend regular *ctypes* syntax but do not interfere with *ctypes* should the code be required to run on *Windows* as well. 13 | 14 | Where is memory synchronized automatically? 15 | ------------------------------------------- 16 | 17 | *zugbruecke* can handle some types of pointers on its own, without additional ``memsync`` directives. Pointers to variables containing a single element - e.g. a floating point number or a structure - and pointers to fixed-length arrays are handled transparently without additional directives. 18 | 19 | In more technical terms, whenever ``ctypes.sizeof(your_argument_type)`` would give a meaningful result right out of the gate, i.e. the size of the memory to be synchronized can be determined from data types provided via ``argtypes`` and/or ``restype``, ``memsync`` is not required. This also applies to more complex types such as structures where ``ctypes.POINTER(your_struct_type)`` is just fine on its own as long as only a single structure is passed back and forth. 20 | 21 | Arrays can be handled automatically as well if they have a fixed, specified length. Type definitions given via ``argtypes`` and/or ``restype`` such as ``ctypes.POINTER(ctypes.c_int * 10)`` or even ``ctypes.POINTER(your_struct_type * 10)`` are fine because their size can clearly be deduced from the type itself. 22 | 23 | Last but not least, every bit of data that is passed truly "by value" instead of as a pointer does not require ``memsync``. 24 | 25 | .. warning:: 26 | 27 | Data passed "by value" can be confusing because original *ctypes* does accept data passed "by value" in certain cases but actually passes a pointer into the DLL function internally. Zero/null-terminated string buffers are a good example for this problem. Here, *zugbruecke* is a bit more strict: It forces the user to specify pointer types in ``argtypes`` where applicable. Passing data "implicitly by reference" is not supported by *zugbruecke* in most scenarios. Such cases can require ``memsync`` as a consequence. Implicitly passing data by reference only really works with *zugbruecke* for "simply data types", i.e. individual integers and floating point numbers. 28 | 29 | Where is ``memsync`` required? 30 | ------------------------------ 31 | 32 | If the size of a chunk of memory a pointer is pointing to is arbitrary and dynamically determined at runtime, i.e. once per function call, *zugbruecke* must be provided with a hint on where it can find information on the size of the memory section within the arguments or return value of a routine call. Arrays of arbitrary length are classic examples. Besides, if null/zero-terminated string buffers are passed via ``ctypes.POINTER(ctypes.c_char)`` or similar, *zugbruecke** must be made aware of this fact. Those hints can be provided through ``memsync``. 33 | 34 | .. _memory protection: https://en.wikipedia.org/wiki/Memory_protection 35 | -------------------------------------------------------------------------------- /tests/test_error_memsync.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | 5 | ZUGBRUECKE 6 | Calling routines in Windows DLLs from Python scripts running on unixlike systems 7 | https://github.com/pleiszenburg/zugbruecke 8 | 9 | tests/test_error_memsync.py: Checks for proper error handling of memsync definitions 10 | 11 | Required to run on platform / side: [UNIX, WINE] 12 | 13 | Copyright (C) 2017-2023 Sebastian M. Ernst 14 | 15 | 16 | The contents of this file are subject to the GNU Lesser General Public License 17 | Version 2.1 ("LGPL" or "License"). You may not use this file except in 18 | compliance with the License. You may obtain a copy of the License at 19 | https://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt 20 | https://github.com/pleiszenburg/zugbruecke/blob/master/LICENSE 21 | 22 | Software distributed under the License is distributed on an "AS IS" basis, 23 | WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the 24 | specific language governing rights and limitations under the License. 25 | 26 | 27 | """ 28 | 29 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 30 | # C 31 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 32 | 33 | HEADER = """ 34 | {{ PREFIX }} int16_t {{ SUFFIX }} sub_ints( 35 | int16_t a, 36 | int16_t b 37 | ); 38 | """ 39 | 40 | SOURCE = """ 41 | {{ PREFIX }} int16_t {{ SUFFIX }} sub_ints( 42 | int16_t a, 43 | int16_t b 44 | ) 45 | { 46 | return a - b; 47 | } 48 | """ 49 | 50 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 51 | # IMPORT 52 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 53 | 54 | from .lib.ctypes import get_context 55 | 56 | import pytest 57 | 58 | from sys import platform 59 | 60 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 61 | # TEST(s) 62 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 63 | 64 | 65 | @pytest.mark.parametrize("arch,conv,ctypes,dll_handle", get_context(__file__)) 66 | def test_memsync_on_routine_not_list(arch, conv, ctypes, dll_handle): 67 | """ 68 | Memsync on DLL function has wrong type 69 | """ 70 | 71 | sub_ints = dll_handle.sub_ints 72 | 73 | if any(platform.startswith(os_name) for os_name in ["linux", "darwin", "freebsd"]): 74 | with pytest.raises(TypeError): 75 | sub_ints.memsync = {} 76 | elif platform.startswith("win"): 77 | sub_ints.memsync = {} 78 | 79 | 80 | @pytest.mark.parametrize("arch,conv,ctypes,dll_handle", get_context(__file__)) 81 | def test_memsync_on_callback_not_list(arch, conv, ctypes, dll_handle): 82 | """ 83 | Memsync on callback function type has wrong type 84 | """ 85 | 86 | if conv == "cdll": 87 | func_type = ctypes.CFUNCTYPE 88 | elif conv == "windll": 89 | func_type = ctypes.WINFUNCTYPE 90 | else: 91 | raise ValueError("unknown calling convention", conv) 92 | 93 | conveyor_belt = func_type(ctypes.c_int16, ctypes.c_int16) 94 | 95 | if any(platform.startswith(os_name) for os_name in ['linux', 'darwin', 'freebsd']): 96 | with pytest.raises(TypeError): 97 | conveyor_belt.memsync = {} 98 | elif platform.startswith('win'): 99 | conveyor_belt.memsync = {} 100 | -------------------------------------------------------------------------------- /tests/test_no_arguments.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | 5 | ZUGBRUECKE 6 | Calling routines in Windows DLLs from Python scripts running on unixlike systems 7 | https://github.com/pleiszenburg/zugbruecke 8 | 9 | tests/test_no_arguments.py: Test function call without parameters 10 | 11 | Required to run on platform / side: [UNIX, WINE] 12 | 13 | Copyright (C) 2017-2023 Sebastian M. Ernst 14 | 15 | 16 | The contents of this file are subject to the GNU Lesser General Public License 17 | Version 2.1 ("LGPL" or "License"). You may not use this file except in 18 | compliance with the License. You may obtain a copy of the License at 19 | https://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt 20 | https://github.com/pleiszenburg/zugbruecke/blob/master/LICENSE 21 | 22 | Software distributed under the License is distributed on an "AS IS" basis, 23 | WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the 24 | specific language governing rights and limitations under the License. 25 | 26 | 27 | """ 28 | 29 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 30 | # C 31 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 32 | 33 | HEADER = """ 34 | {{ PREFIX }} int16_t {{ SUFFIX }} get_const_int_a(void); 35 | 36 | {{ PREFIX }} int16_t {{ SUFFIX }} get_const_int_b(void); 37 | 38 | {{ PREFIX }} int16_t {{ SUFFIX }} get_const_int_c(void); 39 | """ 40 | 41 | SOURCE = """ 42 | {{ PREFIX }} int16_t {{ SUFFIX }} get_const_int_a(void) 43 | { 44 | return (int16_t)sqrt(49); 45 | } 46 | 47 | {{ PREFIX }} int16_t {{ SUFFIX }} get_const_int_b(void) 48 | { 49 | return (int16_t)sqrt(36); 50 | } 51 | 52 | {{ PREFIX }} int16_t {{ SUFFIX }} get_const_int_c(void) 53 | { 54 | return (int16_t)sqrt(25); 55 | } 56 | """ 57 | 58 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 59 | # IMPORT 60 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 61 | 62 | from .lib.ctypes import get_context 63 | 64 | import pytest 65 | 66 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 67 | # TEST(s) 68 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 69 | 70 | 71 | @pytest.mark.parametrize("arch,conv,ctypes,dll_handle", get_context(__file__)) 72 | def test_no_configuration(arch, conv, ctypes, dll_handle): 73 | """ 74 | Argtypes is not configured 75 | """ 76 | 77 | get_const_int = dll_handle.get_const_int_a 78 | get_const_int.restype = ctypes.c_int16 79 | 80 | assert 7 == get_const_int() 81 | 82 | 83 | @pytest.mark.parametrize("arch,conv,ctypes,dll_handle", get_context(__file__)) 84 | def test_empty_tuple(arch, conv, ctypes, dll_handle): 85 | """ 86 | Argtypes is set to empty tuple 87 | """ 88 | 89 | get_const_int = dll_handle.get_const_int_b 90 | get_const_int.argtypes = tuple() 91 | get_const_int.restype = ctypes.c_int16 92 | 93 | assert 6 == get_const_int() 94 | 95 | 96 | @pytest.mark.parametrize("arch,conv,ctypes,dll_handle", get_context(__file__)) 97 | def test_empty_list(arch, conv, ctypes, dll_handle): 98 | """ 99 | Argtypes is set to empty list 100 | """ 101 | 102 | get_const_int = dll_handle.get_const_int_c 103 | get_const_int.argtypes = [] 104 | get_const_int.restype = ctypes.c_int16 105 | 106 | assert 5 == get_const_int() 107 | -------------------------------------------------------------------------------- /tests/test_callback_simple.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | 5 | ZUGBRUECKE 6 | Calling routines in Windows DLLs from Python scripts running on unixlike systems 7 | https://github.com/pleiszenburg/zugbruecke 8 | 9 | tests/test_callback_simple.py: Demonstrates callback routines as arguments 10 | 11 | Required to run on platform / side: [UNIX, WINE] 12 | 13 | Copyright (C) 2017-2023 Sebastian M. Ernst 14 | 15 | 16 | The contents of this file are subject to the GNU Lesser General Public License 17 | Version 2.1 ("LGPL" or "License"). You may not use this file except in 18 | compliance with the License. You may obtain a copy of the License at 19 | https://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt 20 | https://github.com/pleiszenburg/zugbruecke/blob/master/LICENSE 21 | 22 | Software distributed under the License is distributed on an "AS IS" basis, 23 | WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the 24 | specific language governing rights and limitations under the License. 25 | 26 | 27 | """ 28 | 29 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 30 | # C 31 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 32 | 33 | HEADER = """ 34 | typedef int16_t {{ SUFFIX }} (*conveyor_belt)(int16_t index); 35 | 36 | {{ PREFIX }} int16_t {{ SUFFIX }} sum_elements_from_callback( 37 | int16_t len, 38 | conveyor_belt get_data 39 | ); 40 | """ 41 | 42 | SOURCE = """ 43 | {{ PREFIX }} int16_t {{ SUFFIX }} sum_elements_from_callback( 44 | int16_t len, 45 | conveyor_belt get_data 46 | ) 47 | { 48 | 49 | int16_t sum = 0; 50 | int16_t i; 51 | 52 | for(i = 0; i < len; i++) 53 | { 54 | sum += get_data(i); 55 | } 56 | 57 | return sum; 58 | 59 | } 60 | """ 61 | 62 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 63 | # IMPORT 64 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 65 | 66 | from typing import List 67 | 68 | from .lib.ctypes import get_context 69 | 70 | import pytest 71 | 72 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 73 | # TEST(s) 74 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 75 | 76 | 77 | @pytest.mark.parametrize("arch,conv,ctypes,dll_handle", get_context(__file__)) 78 | def test_callback_simple(arch, conv, ctypes, dll_handle): 79 | """ 80 | Demonstrates callback routines as arguments 81 | """ 82 | 83 | if conv == "cdll": 84 | func_type = ctypes.CFUNCTYPE 85 | elif conv == "windll": 86 | func_type = ctypes.WINFUNCTYPE 87 | else: 88 | raise ValueError("unknown calling convention", conv) 89 | 90 | ConveyorBeltFunc = func_type(ctypes.c_int16, ctypes.c_int16) 91 | 92 | sum_elements_from_callback_dll = dll_handle.sum_elements_from_callback 93 | sum_elements_from_callback_dll.argtypes = (ctypes.c_int16, ConveyorBeltFunc) 94 | sum_elements_from_callback_dll.restype = ctypes.c_int16 95 | 96 | DATA = [1, 6, 8, 4, 9, 7, 4, 2, 5, 2] 97 | 98 | @ConveyorBeltFunc 99 | def get_data(index): 100 | """ 101 | Callback function, called by DLL function 102 | """ 103 | 104 | print(index, DATA[index]) 105 | return DATA[index] 106 | 107 | def sum_elements_from_callback(data: List[int]) -> int: 108 | """ 109 | User-facing wrapper around DLL function 110 | """ 111 | 112 | return sum_elements_from_callback_dll(len(data), get_data) 113 | 114 | assert 48 == sum_elements_from_callback(DATA) 115 | -------------------------------------------------------------------------------- /tests/test_pointers_and_array_malloc_by_dll.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | 5 | ZUGBRUECKE 6 | Calling routines in Windows DLLs from Python scripts running on unixlike systems 7 | https://github.com/pleiszenburg/zugbruecke 8 | 9 | tests/test_pointers_and_array_malloc_by_dll.py: Test array pointer and allocation of memory by DLL 10 | 11 | Required to run on platform / side: [UNIX, WINE] 12 | 13 | Copyright (C) 2017-2023 Sebastian M. Ernst 14 | 15 | 16 | The contents of this file are subject to the GNU Lesser General Public License 17 | Version 2.1 ("LGPL" or "License"). You may not use this file except in 18 | compliance with the License. You may obtain a copy of the License at 19 | https://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt 20 | https://github.com/pleiszenburg/zugbruecke/blob/master/LICENSE 21 | 22 | Software distributed under the License is distributed on an "AS IS" basis, 23 | WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the 24 | specific language governing rights and limitations under the License. 25 | 26 | 27 | """ 28 | 29 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 30 | # C 31 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 32 | 33 | HEADER = """ 34 | {{ PREFIX }} void {{ SUFFIX }} square_int_array( 35 | int16_t *in_array, 36 | void *out_array, 37 | int16_t len 38 | ); 39 | """ 40 | 41 | SOURCE = """ 42 | {{ PREFIX }} void {{ SUFFIX }} square_int_array( 43 | int16_t *in_array, 44 | void *out_array, 45 | int16_t len 46 | ) 47 | { 48 | int i; 49 | int16_t **out_array_p = out_array; 50 | *out_array_p = malloc(sizeof(int16_t) * len); 51 | for(i = 0; i < len; i++) 52 | { 53 | (*out_array_p)[i] = in_array[i] * in_array[i]; 54 | } 55 | } 56 | """ 57 | 58 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 59 | # IMPORT 60 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 61 | 62 | from typing import List 63 | 64 | from .lib.ctypes import get_context 65 | 66 | import pytest 67 | 68 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 69 | # TEST(s) 70 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 71 | 72 | 73 | @pytest.mark.parametrize("arch,conv,ctypes,dll_handle", get_context(__file__)) 74 | def test_pointers_and_array_malloc_by_dll(arch, conv, ctypes, dll_handle): 75 | 76 | square_int_array_dll = dll_handle.square_int_array 77 | square_int_array_dll.argtypes = ( 78 | ctypes.POINTER(ctypes.c_int16), 79 | ctypes.c_void_p, 80 | ctypes.c_int16, 81 | ) 82 | square_int_array_dll.memsync = [ 83 | dict( 84 | pointer = [0], 85 | length = [2], 86 | type = ctypes.c_int16, 87 | ), 88 | dict( 89 | pointer = [1, -1], 90 | length = [2], 91 | type = ctypes.c_int16, 92 | ), 93 | ] 94 | 95 | def square_int_array(data: List[int]) -> List[int]: 96 | """ 97 | User-facing wrapper around DLL function 98 | """ 99 | 100 | in_ptr = ctypes.cast( 101 | ctypes.pointer((ctypes.c_int16 * len(data))(*data)), 102 | ctypes.POINTER(ctypes.c_int16), 103 | ) 104 | out_ptr = ctypes.pointer(ctypes.c_void_p()) 105 | 106 | square_int_array_dll(in_ptr, out_ptr, ctypes.c_int16(len(data))) 107 | 108 | return ctypes.cast( 109 | out_ptr.contents, ctypes.POINTER(ctypes.c_int16 * len(data)) 110 | ).contents[:] 111 | 112 | assert [4, 16, 9, 25] == square_int_array([2, 4, 3, 5]) 113 | -------------------------------------------------------------------------------- /tests/test_string_strsxp.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | 5 | ZUGBRUECKE 6 | Calling routines in Windows DLLs from Python scripts running on unixlike systems 7 | https://github.com/pleiszenburg/zugbruecke 8 | 9 | tests/test_string_strsxp.py: R-style strings 10 | 11 | Required to run on platform / side: [UNIX, WINE] 12 | 13 | Copyright (C) 2017-2023 Sebastian M. Ernst 14 | 15 | 16 | The contents of this file are subject to the GNU Lesser General Public License 17 | Version 2.1 ("LGPL" or "License"). You may not use this file except in 18 | compliance with the License. You may obtain a copy of the License at 19 | https://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt 20 | https://github.com/pleiszenburg/zugbruecke/blob/master/LICENSE 21 | 22 | Software distributed under the License is distributed on an "AS IS" basis, 23 | WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the 24 | specific language governing rights and limitations under the License. 25 | 26 | 27 | """ 28 | 29 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 30 | # C 31 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 32 | 33 | HEADER = """ 34 | {{ PREFIX }} void {{ SUFFIX }} replace_char( 35 | char **in_string, 36 | char old_letter, 37 | char new_letter 38 | ); 39 | """ 40 | 41 | SOURCE = """ 42 | {{ PREFIX }} void {{ SUFFIX }} replace_char( 43 | char **in_string, 44 | char old_letter, 45 | char new_letter 46 | ) 47 | { 48 | int i; 49 | for (i = 0; i < strlen((*in_string)); i++) { 50 | if((*in_string)[i] == old_letter) { 51 | (*in_string)[i] = new_letter; 52 | } 53 | } 54 | } 55 | """ 56 | 57 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 58 | # IMPORT 59 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 60 | 61 | from .lib.ctypes import get_context, PLATFORM 62 | 63 | import pytest 64 | 65 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 66 | # TEST(s) 67 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 68 | 69 | 70 | @pytest.mark.xfail(PLATFORM == "unix", strict=True, reason="not yet implemented") 71 | @pytest.mark.parametrize("arch,conv,ctypes,dll_handle", get_context(__file__)) 72 | def test_r_strsxp(arch, conv, ctypes, dll_handle): 73 | """ 74 | Passing R-style strings, a pointer to an array of pointers to string fragments 75 | """ 76 | 77 | replace_char_dll = dll_handle.replace_char 78 | replace_char_dll.argtypes = ( 79 | ctypes.POINTER( 80 | ctypes.POINTER(ctypes.c_char) 81 | ), # Generate pointer to char manually 82 | ctypes.c_char, 83 | ctypes.c_char, 84 | ) 85 | replace_char_dll.memsync = [ 86 | dict( 87 | pointer = [0, -1], 88 | null = True, 89 | ), 90 | dict( 91 | pointer = [0, -1], 92 | null = True, 93 | ), 94 | ] 95 | 96 | def replace_char(string: str, old_char: str, new_char: str): 97 | """ 98 | User-facing wrapper around DLL function 99 | """ 100 | 101 | assert len(old_char) == 1 and len(new_char) == 1 102 | 103 | buffer = (ctypes.c_char_p * 1)(string.encode("utf-8")) 104 | ptr = ctypes.cast( 105 | buffer, ctypes.POINTER(ctypes.POINTER(ctypes.c_char)) 106 | ) 107 | 108 | replace_char_dll( 109 | ptr, old_char.encode("utf-8"), new_char.encode("utf-8") 110 | ) 111 | 112 | return buffer[:][0].decode("utf-8") 113 | 114 | assert "Hellu wurld!" == replace_char("Hello world!", "o", "u") 115 | -------------------------------------------------------------------------------- /tests/test_pointers_and_struct_malloc_by_dll.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | 5 | ZUGBRUECKE 6 | Calling routines in Windows DLLs from Python scripts running on unixlike systems 7 | https://github.com/pleiszenburg/zugbruecke 8 | 9 | tests/test_pointers_and_struct_malloc_by_dll.py: Test allocation of memory by DLL, returned as struct 10 | 11 | Required to run on platform / side: [UNIX, WINE] 12 | 13 | Copyright (C) 2017-2023 Sebastian M. Ernst 14 | 15 | 16 | The contents of this file are subject to the GNU Lesser General Public License 17 | Version 2.1 ("LGPL" or "License"). You may not use this file except in 18 | compliance with the License. You may obtain a copy of the License at 19 | https://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt 20 | https://github.com/pleiszenburg/zugbruecke/blob/master/LICENSE 21 | 22 | Software distributed under the License is distributed on an "AS IS" basis, 23 | WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the 24 | specific language governing rights and limitations under the License. 25 | 26 | 27 | """ 28 | 29 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 30 | # C 31 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 32 | 33 | HEADER = """ 34 | typedef struct int_array_data { 35 | int16_t *data; 36 | int16_t len; 37 | } int_array_data; 38 | 39 | {{ PREFIX }} int_array_data {{ SUFFIX }} *fibonacci_sequence( 40 | int16_t len 41 | ); 42 | """ 43 | 44 | SOURCE = """ 45 | {{ PREFIX }} int_array_data {{ SUFFIX }} *fibonacci_sequence( 46 | int16_t len 47 | ) 48 | { 49 | int16_t i; 50 | int_array_data *out_data = malloc(sizeof(int_array_data)); 51 | out_data->len = len; 52 | out_data->data = malloc(sizeof(int16_t) * out_data->len); 53 | for(i = 0; i < len; i++){ 54 | if(i == 0 || i == 1) { out_data->data[i] = 1; continue; } 55 | out_data->data[i] = out_data->data[i - 1] + out_data->data[i - 2]; 56 | } 57 | return out_data; 58 | } 59 | """ 60 | 61 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 62 | # IMPORT 63 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 64 | 65 | from typing import List 66 | 67 | from .lib.ctypes import get_context 68 | 69 | import pytest 70 | 71 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 72 | # TEST(s) 73 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 74 | 75 | 76 | @pytest.mark.parametrize("arch,conv,ctypes,dll_handle", get_context(__file__)) 77 | def test_pointers_and_struct_malloc_by_dll(arch, conv, ctypes, dll_handle): 78 | """ 79 | Retrieve data from memory (within returned struct) that has been allocated by DLL 80 | """ 81 | 82 | class ArrayData(ctypes.Structure): 83 | _fields_ = [ 84 | ("data", ctypes.POINTER(ctypes.c_int16)), 85 | ("len", ctypes.c_int16), 86 | ] 87 | 88 | fibonacci_sequence_dll = dll_handle.fibonacci_sequence 89 | fibonacci_sequence_dll.argtypes = (ctypes.c_int16,) 90 | fibonacci_sequence_dll.restype = ctypes.POINTER(ArrayData) 91 | fibonacci_sequence_dll.memsync = [ 92 | dict( 93 | pointer = ["r", "data"], 94 | length = ["r", "len"], 95 | type = ctypes.c_int16, 96 | ) 97 | ] 98 | 99 | def fibonacci_sequence(length: int) -> List[int]: 100 | """ 101 | User-facing wrapper around DLL function 102 | """ 103 | 104 | ptr = fibonacci_sequence_dll(length) 105 | 106 | return ctypes.cast( 107 | ptr.contents.data, ctypes.POINTER(ctypes.c_int16 * length) 108 | ).contents[:] 109 | 110 | assert [1, 1, 2, 3, 5, 8, 13, 21, 34, 55] == fibonacci_sequence(10) 111 | -------------------------------------------------------------------------------- /tests/test_memsync_minimal.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | 5 | ZUGBRUECKE 6 | Calling routines in Windows DLLs from Python scripts running on unixlike systems 7 | https://github.com/pleiszenburg/zugbruecke 8 | 9 | tests/test_memsync_minimal.py: Test bidirectional memory sync for pointers 10 | 11 | Required to run on platform / side: [UNIX, WINE] 12 | 13 | Copyright (C) 2017-2023 Sebastian M. Ernst 14 | 15 | 16 | The contents of this file are subject to the GNU Lesser General Public License 17 | Version 2.1 ("LGPL" or "License"). You may not use this file except in 18 | compliance with the License. You may obtain a copy of the License at 19 | https://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt 20 | https://github.com/pleiszenburg/zugbruecke/blob/master/LICENSE 21 | 22 | Software distributed under the License is distributed on an "AS IS" basis, 23 | WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the 24 | specific language governing rights and limitations under the License. 25 | 26 | 27 | """ 28 | 29 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 30 | # C 31 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 32 | 33 | HEADER = """ 34 | {{ PREFIX }} void {{ SUFFIX }} bubblesort( 35 | float *a, 36 | int n 37 | ); 38 | """ 39 | 40 | SOURCE = """ 41 | {{ PREFIX }} void {{ SUFFIX }} bubblesort( 42 | float *a, 43 | int n 44 | ) 45 | { 46 | int i, j; 47 | for (i = 0; i < n - 1; ++i) 48 | { 49 | for (j = 0; j < n - i - 1; ++j) 50 | { 51 | if (a[j] > a[j + 1]) 52 | { 53 | float tmp = a[j]; 54 | a[j] = a[j + 1]; 55 | a[j + 1] = tmp; 56 | } 57 | } 58 | } 59 | } 60 | """ 61 | 62 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 63 | # IMPORT 64 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 65 | 66 | from typing import List 67 | 68 | from .lib.ctypes import get_context 69 | 70 | import pytest 71 | 72 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 73 | # TEST(s) 74 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 75 | 76 | 77 | @pytest.mark.parametrize("arch,conv,ctypes,dll_handle", get_context(__file__)) 78 | def test_memsync_minimal(arch, conv, ctypes, dll_handle): 79 | """ 80 | Minimal memsync test with one pointer and one length argument 81 | """ 82 | 83 | bubblesort_dll = dll_handle.bubblesort 84 | bubblesort_dll.memsync = [ # Regular ctypes on Windows should ignore this statement 85 | dict( 86 | pointer = [0], # "path" to argument containing the pointer 87 | length = [1], # "path" to argument containing the length 88 | type = ctypes.c_float, # type of argument (optional, default char/byte): sizeof(type) * length == bytes 89 | ) 90 | ] 91 | bubblesort_dll.argtypes = (ctypes.POINTER(ctypes.c_float), ctypes.c_int) 92 | 93 | def bubblesort(values: List[float]): 94 | """ 95 | User-facing wrapper around DLL function 96 | """ 97 | 98 | ct_values = ((ctypes.c_float) * len(values))(*values) 99 | ct_ptr = ctypes.cast(ctypes.pointer(ct_values), ctypes.POINTER(ctypes.c_float)) 100 | bubblesort_dll(ct_ptr, len(values)) 101 | values[:] = ct_values[:] 102 | 103 | data = [5.74, 3.72, 6.28, 8.6, 9.34, 6.47, 2.05, 9.09, 4.39, 4.75] 104 | bubblesort(data) 105 | 106 | data = [round(number, 2) for number in data] 107 | expected = [2.05, 3.72, 4.39, 4.75, 5.74, 6.28, 6.47, 8.6, 9.09, 9.34] 108 | diff = sum(abs(a - b) for a, b in zip(data, expected)) 109 | 110 | assert pytest.approx(0.0, 0.0000001) == diff 111 | -------------------------------------------------------------------------------- /tests/test_fixedlength_struct.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | 5 | ZUGBRUECKE 6 | Calling routines in Windows DLLs from Python scripts running on unixlike systems 7 | https://github.com/pleiszenburg/zugbruecke 8 | 9 | tests/test_fixedlength_struct.py: Tests fixed length arrays of structs 10 | 11 | Required to run on platform / side: [UNIX, WINE] 12 | 13 | Copyright (C) 2017-2023 Sebastian M. Ernst 14 | 15 | 16 | The contents of this file are subject to the GNU Lesser General Public License 17 | Version 2.1 ("LGPL" or "License"). You may not use this file except in 18 | compliance with the License. You may obtain a copy of the License at 19 | https://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt 20 | https://github.com/pleiszenburg/zugbruecke/blob/master/LICENSE 21 | 22 | Software distributed under the License is distributed on an "AS IS" basis, 23 | WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the 24 | specific language governing rights and limitations under the License. 25 | 26 | 27 | """ 28 | 29 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 30 | # C 31 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 32 | 33 | HEADER = """ 34 | typedef struct manynumbers { 35 | float a, b, c, d, e, f; 36 | } manynumbers; 37 | 38 | {{ PREFIX }} void {{ SUFFIX }} sum_members( 39 | manynumbers numbers[10], 40 | manynumbers *sum 41 | ); 42 | """ 43 | 44 | SOURCE = """ 45 | {{ PREFIX }} void {{ SUFFIX }} sum_members( 46 | manynumbers numbers[10], 47 | manynumbers *sum 48 | ) 49 | { 50 | sum->a = 0.0; 51 | sum->b = 0.0; 52 | sum->c = 0.0; 53 | sum->d = 0.0; 54 | sum->e = 0.0; 55 | sum->f = 0.0; 56 | for (int i = 0; i < 10; i++) 57 | { 58 | sum->a += numbers[i].a; 59 | sum->b += numbers[i].b; 60 | sum->c += numbers[i].c; 61 | sum->d += numbers[i].d; 62 | sum->e += numbers[i].e; 63 | sum->f += numbers[i].f; 64 | } 65 | } 66 | """ 67 | 68 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 69 | # IMPORT 70 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 71 | 72 | from .lib.ctypes import get_context 73 | 74 | import pytest 75 | 76 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 77 | # TEST(s) 78 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 79 | 80 | 81 | @pytest.mark.parametrize("arch,conv,ctypes,dll_handle", get_context(__file__)) 82 | def test_fixedlength_struct(arch, conv, ctypes, dll_handle): 83 | """ 84 | Tests fixed length arrays of structs 85 | """ 86 | 87 | class ManyNumbers(ctypes.Structure): 88 | _fields_ = [ 89 | ('a', ctypes.c_float), 90 | ('b', ctypes.c_float), 91 | ('c', ctypes.c_float), 92 | ('d', ctypes.c_float), 93 | ('e', ctypes.c_float), 94 | ('f', ctypes.c_float), 95 | ] 96 | 97 | sum_members_dll = dll_handle.sum_members 98 | sum_members_dll.argtypes = ( 99 | ManyNumbers * 10, 100 | ctypes.POINTER(ManyNumbers), 101 | ) 102 | 103 | numbers = (ManyNumbers * 10)() 104 | for idx in range(10): 105 | numbers[idx].a = 1 + idx * 10 106 | numbers[idx].b = 2 + idx * 10 107 | numbers[idx].c = 3 + idx * 10 108 | numbers[idx].d = 4 + idx * 10 109 | numbers[idx].e = 5 + idx * 10 110 | numbers[idx].f = 6 + idx * 10 111 | sum_ = ManyNumbers() 112 | 113 | sum_members_dll(numbers, ctypes.pointer(sum_)) 114 | 115 | assert all(( 116 | sum_.a == 460, 117 | sum_.b == 470, 118 | sum_.c == 480, 119 | sum_.d == 490, 120 | sum_.e == 500, 121 | sum_.f == 510, 122 | )) 123 | -------------------------------------------------------------------------------- /tests/test_fixedlength_callback.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | 5 | ZUGBRUECKE 6 | Calling routines in Windows DLLs from Python scripts running on unixlike systems 7 | https://github.com/pleiszenburg/zugbruecke 8 | 9 | tests/test_fixedlength_callback.py: Tests fixed length arrays of function pointers 10 | 11 | Required to run on platform / side: [UNIX, WINE] 12 | 13 | Copyright (C) 2017-2023 Sebastian M. Ernst 14 | 15 | 16 | The contents of this file are subject to the GNU Lesser General Public License 17 | Version 2.1 ("LGPL" or "License"). You may not use this file except in 18 | compliance with the License. You may obtain a copy of the License at 19 | https://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt 20 | https://github.com/pleiszenburg/zugbruecke/blob/master/LICENSE 21 | 22 | Software distributed under the License is distributed on an "AS IS" basis, 23 | WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the 24 | specific language governing rights and limitations under the License. 25 | 26 | 27 | """ 28 | 29 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 30 | # C 31 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 32 | 33 | HEADER = """ 34 | typedef double {{ SUFFIX }} (*mathop)(double a, double b); 35 | 36 | {{ PREFIX }} double {{ SUFFIX }} apply_op( 37 | mathop ops[2], 38 | double a, 39 | double b, 40 | int opcode 41 | ); 42 | """ 43 | 44 | SOURCE = """ 45 | {{ PREFIX }} double {{ SUFFIX }} apply_op( 46 | mathop ops[2], 47 | double a, 48 | double b, 49 | int opcode 50 | ) 51 | { 52 | if (opcode >= 0 && opcode <= 1) { 53 | return ops[opcode](a, b); 54 | } 55 | return 0.0; 56 | } 57 | """ 58 | 59 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 60 | # IMPORT 61 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 62 | 63 | from .lib.ctypes import get_context 64 | 65 | import pytest 66 | 67 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 68 | # TEST(s) 69 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 70 | 71 | 72 | @pytest.mark.parametrize("arch,conv,ctypes,dll_handle", get_context(__file__)) 73 | def test_fixedlength_callback(arch, conv, ctypes, dll_handle): 74 | """ 75 | Tests fixed length arrays of callback functions 76 | """ 77 | 78 | if conv == "cdll": 79 | FuncType = ctypes.CFUNCTYPE 80 | elif conv == "windll": 81 | FuncType = ctypes.WINFUNCTYPE 82 | else: 83 | raise ValueError("unknown calling convention", conv) 84 | 85 | MathOp = FuncType(ctypes.c_double, ctypes.c_double, ctypes.c_double) 86 | 87 | apply_op_dll = dll_handle.apply_op 88 | apply_op_dll.argtypes = ( 89 | MathOp * 2, 90 | ctypes.c_double, 91 | ctypes.c_double, 92 | ctypes.c_int, 93 | ) 94 | apply_op_dll.restype = ctypes.c_double 95 | 96 | @MathOp 97 | def add_op(a, b): 98 | """ 99 | Callback function, called by DLL function 100 | """ 101 | 102 | return a + b 103 | 104 | @MathOp 105 | def sub_op(a, b): 106 | """ 107 | Callback function, called by DLL function 108 | """ 109 | 110 | return a - b 111 | 112 | def apply_op(a: float, b: float, opcode: int) -> float: 113 | """ 114 | User-facing wrapper around DLL function 115 | """ 116 | 117 | ops = (MathOp * 2)() 118 | ops[0] = add_op 119 | ops[1] = sub_op 120 | 121 | return apply_op_dll(ops, a, b, opcode) 122 | 123 | assert pytest.approx(10.0) == apply_op(7.0, 3.0, 0) 124 | assert pytest.approx(4.0) == apply_op(7.0, 3.0, 1) 125 | assert pytest.approx(0.0) == apply_op(7.0, 3.0, 2) 126 | -------------------------------------------------------------------------------- /tests/test_pointers_and_struct.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | 5 | ZUGBRUECKE 6 | Calling routines in Windows DLLs from Python scripts running on unixlike systems 7 | https://github.com/pleiszenburg/zugbruecke 8 | 9 | tests/test_pointers_and_struct.py: Tests by value struct type passing and return value 10 | 11 | Required to run on platform / side: [UNIX, WINE] 12 | 13 | Copyright (C) 2017-2023 Sebastian M. Ernst 14 | 15 | 16 | The contents of this file are subject to the GNU Lesser General Public License 17 | Version 2.1 ("LGPL" or "License"). You may not use this file except in 18 | compliance with the License. You may obtain a copy of the License at 19 | https://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt 20 | https://github.com/pleiszenburg/zugbruecke/blob/master/LICENSE 21 | 22 | Software distributed under the License is distributed on an "AS IS" basis, 23 | WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the 24 | specific language governing rights and limitations under the License. 25 | 26 | 27 | """ 28 | 29 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 30 | # C 31 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 32 | 33 | HEADER = """ 34 | typedef struct point { 35 | double x, y; 36 | } point; 37 | 38 | {{ PREFIX }} double {{ SUFFIX }} distance( 39 | point *p1, 40 | point *p2 41 | ); 42 | 43 | {{ PREFIX }} double {{ SUFFIX }} *distance_pointer( 44 | point *p1, 45 | point *p2 46 | ); 47 | """ 48 | 49 | SOURCE = """ 50 | {{ PREFIX }} double {{ SUFFIX }} distance( 51 | point *p1, 52 | point *p2 53 | ) 54 | { 55 | return hypot(p1->x - p2->x, p1->y - p2->y); 56 | } 57 | 58 | {{ PREFIX }} double {{ SUFFIX }} *distance_pointer( 59 | point *p1, 60 | point *p2 61 | ) 62 | { 63 | double *distance_p = (double *)malloc(sizeof(double)); 64 | *distance_p = hypot(p1->x - p2->x, p1->y - p2->y); 65 | return distance_p; 66 | } 67 | """ 68 | 69 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 70 | # IMPORT 71 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 72 | 73 | from .lib.ctypes import get_context 74 | 75 | import pytest 76 | 77 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 78 | # TEST(s) 79 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 80 | 81 | 82 | @pytest.mark.parametrize("arch,conv,ctypes,dll_handle", get_context(__file__)) 83 | def test_pointer_args(arch, conv, ctypes, dll_handle): 84 | """ 85 | Pointer arguments, pass structs directly though, get result by value 86 | """ 87 | 88 | class Point(ctypes.Structure): 89 | _fields_ = [("x", ctypes.c_double), ("y", ctypes.c_double)] 90 | 91 | distance_dll = dll_handle.distance 92 | distance_dll.argtypes = (ctypes.POINTER(Point), ctypes.POINTER(Point)) 93 | distance_dll.restype = ctypes.c_double 94 | 95 | assert pytest.approx(4.242640687119285, 0.0000001) == distance_dll( 96 | Point(1, 2), Point(4, 5) 97 | ) 98 | 99 | 100 | @pytest.mark.parametrize("arch,conv,ctypes,dll_handle", get_context(__file__)) 101 | def test_pointer_args_and_retval(arch, conv, ctypes, dll_handle): 102 | """ 103 | Pointer arguments, pass structs directly though, get result as pointer (allocated by DLL) 104 | """ 105 | 106 | class Point(ctypes.Structure): 107 | _fields_ = [("x", ctypes.c_double), ("y", ctypes.c_double)] 108 | 109 | distance_pointer_dll = dll_handle.distance_pointer 110 | distance_pointer_dll.argtypes = (ctypes.POINTER(Point), ctypes.POINTER(Point)) 111 | distance_pointer_dll.restype = ctypes.POINTER(ctypes.c_double) 112 | 113 | assert pytest.approx(4.242640687119285, 0.0000001) == distance_pointer_dll( 114 | Point(1, 2), Point(4, 5) 115 | ).contents.value 116 | -------------------------------------------------------------------------------- /tests/test_struct_pointer.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | 5 | ZUGBRUECKE 6 | Calling routines in Windows DLLs from Python scripts running on unixlike systems 7 | https://github.com/pleiszenburg/zugbruecke 8 | 9 | tests/test_struct_pointer.py: Tests pointer to struct of fixed size 10 | 11 | Required to run on platform / side: [UNIX, WINE] 12 | 13 | Copyright (C) 2017-2023 Sebastian M. Ernst 14 | 15 | 16 | The contents of this file are subject to the GNU Lesser General Public License 17 | Version 2.1 ("LGPL" or "License"). You may not use this file except in 18 | compliance with the License. You may obtain a copy of the License at 19 | https://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt 20 | https://github.com/pleiszenburg/zugbruecke/blob/master/LICENSE 21 | 22 | Software distributed under the License is distributed on an "AS IS" basis, 23 | WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the 24 | specific language governing rights and limitations under the License. 25 | 26 | 27 | """ 28 | 29 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 30 | # C 31 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 32 | 33 | HEADER = """ 34 | typedef struct Vector4d { 35 | int16_t x, y, z, u; 36 | } Vector4d; 37 | 38 | {{ PREFIX }} Vector4d {{ SUFFIX }} *vector4d_add( 39 | Vector4d *v1, 40 | Vector4d *v2 41 | ); 42 | """ 43 | 44 | SOURCE = """ 45 | {{ PREFIX }} Vector4d {{ SUFFIX }} *vector4d_add( 46 | Vector4d *v1, 47 | Vector4d *v2 48 | ) 49 | { 50 | 51 | Vector4d *v3 = malloc(sizeof(Vector4d)); 52 | 53 | v3->x = v1->x + v2->x; 54 | v3->y = v1->y + v2->y; 55 | v3->z = v1->z + v2->z; 56 | v3->u = v1->u + v2->u; 57 | 58 | return v3; 59 | 60 | } 61 | """ 62 | 63 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 64 | # IMPORT 65 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 66 | 67 | from typing import Dict 68 | 69 | from .lib.ctypes import get_context 70 | 71 | import pytest 72 | 73 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 74 | # TEST(s) 75 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 76 | 77 | 78 | @pytest.mark.parametrize("arch,conv,ctypes,dll_handle", get_context(__file__)) 79 | def test_struct_pointer(arch, conv, ctypes, dll_handle): 80 | """ 81 | Testing pointer to individual struct object of fixed size (no memsync required) 82 | """ 83 | 84 | class Vector4d(ctypes.Structure): 85 | _fields_ = [ 86 | ("x", ctypes.c_int16), 87 | ("y", ctypes.c_int16), 88 | ("z", ctypes.c_int16), 89 | ("u", ctypes.c_int16), 90 | ] 91 | 92 | vector4d_add_dll = dll_handle.vector4d_add 93 | vector4d_add_dll.argtypes = ( 94 | ctypes.POINTER(Vector4d), 95 | ctypes.POINTER(Vector4d), 96 | ) 97 | vector4d_add_dll.restype = ctypes.POINTER(Vector4d) 98 | 99 | def vector4d_add(v1: Dict[str, int], v2: Dict[str, int]) -> Dict[str, int]: 100 | """ 101 | User-facing wrapper around DLL function 102 | """ 103 | 104 | return dict_from_struct( 105 | vector4d_add_dll(struct_from_dict(v1), struct_from_dict(v2)) 106 | ) 107 | 108 | def struct_from_dict(vector: Dict[str, int]): 109 | """ 110 | Helper 111 | """ 112 | 113 | return Vector4d(**vector) 114 | 115 | def dict_from_struct(vector: Vector4d): 116 | """ 117 | Helper 118 | """ 119 | 120 | return {name: getattr(vector.contents, name) for name, _ in Vector4d._fields_} 121 | 122 | v1 = {"x": 5, "y": 7, "z": 2, "u": 4} 123 | v2 = {"x": 1, "y": 9, "z": 8, "u": 3} 124 | added = {"x": 6, "y": 16, "z": 10, "u": 7} 125 | 126 | assert added == vector4d_add(v1, v2) 127 | -------------------------------------------------------------------------------- /src/zugbruecke/core/dll_server.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | 5 | ZUGBRUECKE 6 | Calling routines in Windows DLLs from Python scripts running on unixlike systems 7 | https://github.com/pleiszenburg/zugbruecke 8 | 9 | src/zugbruecke/core/dll_server.py: Classes for managing the access to DLLs 10 | 11 | Required to run on platform / side: [WINE] 12 | 13 | Copyright (C) 2017-2023 Sebastian M. Ernst 14 | 15 | 16 | The contents of this file are subject to the GNU Lesser General Public License 17 | Version 2.1 ("LGPL" or "License"). You may not use this file except in 18 | compliance with the License. You may obtain a copy of the License at 19 | https://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt 20 | https://github.com/pleiszenburg/zugbruecke/blob/master/LICENSE 21 | 22 | Software distributed under the License is distributed on an "AS IS" basis, 23 | WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the 24 | specific language governing rights and limitations under the License. 25 | 26 | 27 | """ 28 | 29 | 30 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 31 | # IMPORT 32 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 33 | 34 | from ctypes import CDLL 35 | import traceback 36 | from typing import Union 37 | 38 | from .abc import DataABC, DllServerABC, LogABC, RpcServerABC 39 | from .routine_server import RoutineServer 40 | from .typeguard import typechecked 41 | 42 | 43 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 44 | # DLL SERVER CLASS 45 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 46 | 47 | 48 | @typechecked 49 | class DllServer(DllServerABC): 50 | """ 51 | Representing one idividual dll to be called into 52 | """ 53 | 54 | def __init__( 55 | self, 56 | name: str, 57 | hash_id: str, 58 | convention: str, 59 | handler: CDLL, 60 | log: LogABC, 61 | rpc_server: RpcServerABC, 62 | data: DataABC, 63 | ): 64 | 65 | self._name = name 66 | self._convention = convention 67 | self._hash_id = hash_id 68 | self._handler = handler 69 | 70 | self._log = log 71 | self._rpc_server = rpc_server 72 | self._data = data 73 | 74 | self._routines = {} 75 | 76 | for name in ( 77 | "get_repr", 78 | "register_routine", 79 | ): 80 | self._rpc_server.register_function( 81 | getattr(self, name), 82 | f"{self._hash_id:s}_{name:s}", 83 | ) 84 | 85 | def get_repr(self) -> str: 86 | """ 87 | Called by DLL client 88 | """ 89 | 90 | return repr(self._handler) 91 | 92 | def register_routine(self, name: Union[str, int]): 93 | """ 94 | Called by DLL client 95 | """ 96 | 97 | if name in self._routines.keys(): 98 | return 99 | 100 | self._log.info(f'[dll-server] Trying to access "{str(name):s}" in DLL file "{self._name:s}" ...') 101 | 102 | try: 103 | if isinstance(name, str): 104 | routine_handler = getattr(self._handler, name) 105 | else: 106 | routine_handler = self._handler[name] 107 | except AttributeError as e: 108 | self._log.info("[dll-server] ... failed!") 109 | raise e 110 | except Exception as e: 111 | self._log.info("[dll-server] ... failed!") 112 | self._log.error(traceback.format_exc()) 113 | raise e 114 | 115 | # Generate new instance of routine class 116 | self._routines[name] = RoutineServer( 117 | name, 118 | routine_handler, 119 | self._hash_id, 120 | self._convention, 121 | self._name, 122 | self._log, 123 | self._rpc_server, 124 | self._data, 125 | ) 126 | 127 | self._log.info("[dll-server] ... done.") 128 | -------------------------------------------------------------------------------- /tests/test_callback_simple_struct.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | 5 | ZUGBRUECKE 6 | Calling routines in Windows DLLs from Python scripts running on unixlike systems 7 | https://github.com/pleiszenburg/zugbruecke 8 | 9 | tests/test_callback_simple_struct.py: Demonstrates callback in struct 10 | 11 | Required to run on platform / side: [UNIX, WINE] 12 | 13 | Copyright (C) 2017-2023 Sebastian M. Ernst 14 | 15 | 16 | The contents of this file are subject to the GNU Lesser General Public License 17 | Version 2.1 ("LGPL" or "License"). You may not use this file except in 18 | compliance with the License. You may obtain a copy of the License at 19 | https://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt 20 | https://github.com/pleiszenburg/zugbruecke/blob/master/LICENSE 21 | 22 | Software distributed under the License is distributed on an "AS IS" basis, 23 | WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the 24 | specific language governing rights and limitations under the License. 25 | 26 | 27 | """ 28 | 29 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 30 | # C 31 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 32 | 33 | HEADER = """ 34 | typedef int16_t {{ SUFFIX }} (*conveyor_belt)(int16_t index); 35 | 36 | typedef struct conveyor_belt_data { 37 | int16_t len; 38 | conveyor_belt get_data; 39 | } conveyor_belt_data; 40 | 41 | {{ PREFIX }} int16_t {{ SUFFIX }} sum_elements_from_callback_in_struct( 42 | struct conveyor_belt_data *data 43 | ); 44 | """ 45 | 46 | SOURCE = """ 47 | {{ PREFIX }} int16_t {{ SUFFIX }} sum_elements_from_callback_in_struct( 48 | struct conveyor_belt_data *data 49 | ) 50 | { 51 | 52 | int16_t sum = 0; 53 | int16_t i; 54 | 55 | for(i = 0; i < data->len; i++) 56 | { 57 | sum += data->get_data(i); 58 | } 59 | 60 | return sum; 61 | 62 | } 63 | """ 64 | 65 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 66 | # IMPORT 67 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 68 | 69 | from typing import List 70 | 71 | from .lib.ctypes import get_context 72 | 73 | import pytest 74 | 75 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 76 | # TEST(s) 77 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 78 | 79 | 80 | @pytest.mark.parametrize("arch,conv,ctypes,dll_handle", get_context(__file__)) 81 | def test_callback_simple(arch, conv, ctypes, dll_handle): 82 | """ 83 | Demonstrates callback in struct 84 | """ 85 | 86 | if conv == "cdll": 87 | func_type = ctypes.CFUNCTYPE 88 | elif conv == "windll": 89 | func_type = ctypes.WINFUNCTYPE 90 | else: 91 | raise ValueError("unknown calling convention", conv) 92 | 93 | ConveyorBeltFunc = func_type(ctypes.c_int16, ctypes.c_int16) 94 | 95 | class ConveyorBeltData(ctypes.Structure): 96 | _fields_ = [("len", ctypes.c_int16), ("get_data", ConveyorBeltFunc)] 97 | 98 | sum_elements_from_callback_in_struct_dll = ( 99 | dll_handle.sum_elements_from_callback_in_struct 100 | ) 101 | sum_elements_from_callback_in_struct_dll.argtypes = ( 102 | ctypes.POINTER(ConveyorBeltData), 103 | ) 104 | sum_elements_from_callback_in_struct_dll.restype = ctypes.c_int16 105 | 106 | DATA = [1, 6, 8, 4, 9, 7, 4, 2, 5, 2] 107 | 108 | @ConveyorBeltFunc 109 | def get_data(index): 110 | """ 111 | Callback function, called by DLL function 112 | """ 113 | 114 | print(index, DATA[index]) 115 | return DATA[index] 116 | 117 | def sum_elements_from_callback_in_struct(data: List[int]) -> int: 118 | """ 119 | User-facing wrapper around DLL function 120 | """ 121 | 122 | return sum_elements_from_callback_in_struct_dll(ConveyorBeltData(len(data), get_data)) 123 | 124 | assert 48 == sum_elements_from_callback_in_struct(DATA) 125 | -------------------------------------------------------------------------------- /src/zugbruecke/core/definitions/custom.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | 5 | ZUGBRUECKE 6 | Calling routines in Windows DLLs from Python scripts running on unixlike systems 7 | https://github.com/pleiszenburg/zugbruecke 8 | 9 | src/zugbruecke/core/definitions/simple.py: Simple argument definition 10 | 11 | Required to run on platform / side: [UNIX, WINE] 12 | 13 | Copyright (C) 2017-2023 Sebastian M. Ernst 14 | 15 | 16 | The contents of this file are subject to the GNU Lesser General Public License 17 | Version 2.1 ("LGPL" or "License"). You may not use this file except in 18 | compliance with the License. You may obtain a copy of the License at 19 | https://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt 20 | https://github.com/pleiszenburg/zugbruecke/blob/master/LICENSE 21 | 22 | Software distributed under the License is distributed on an "AS IS" basis, 23 | WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the 24 | specific language governing rights and limitations under the License. 25 | 26 | 27 | """ 28 | 29 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 30 | # IMPORT 31 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 32 | 33 | import ctypes 34 | from typing import Any, Dict, List, Optional, Tuple, Union 35 | 36 | from ..abc import CacheABC, DefinitionABC 37 | from ..const import CUSTOM_GROUP 38 | from ..typeguard import typechecked 39 | 40 | from . import base 41 | 42 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 43 | # CLASS 44 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 45 | 46 | @typechecked 47 | class DefinitionCustom(base.Definition): 48 | 49 | GROUP = CUSTOM_GROUP 50 | 51 | def __init__(self, *args: Any, **kwargs: Any): 52 | 53 | super().__init__(*args, **kwargs) 54 | 55 | def as_packed(self) -> Dict: 56 | """ 57 | Pack as dict so it can be sent to other side 58 | 59 | Counterpart to `from_packed` 60 | """ 61 | 62 | return { 63 | "group": self.GROUP, 64 | "flags": self._flags, 65 | "field_name": self._field_name, 66 | "type_name": self._type_name, 67 | } 68 | 69 | @classmethod 70 | def from_packed(cls, 71 | flags: List[int], # f 72 | field_name: Union[str, int, None], # n 73 | type_name: Optional[str], # t 74 | ) -> DefinitionABC: 75 | """ 76 | Unpack from dict received from other side 77 | 78 | Counterpart to `as_packed` 79 | """ 80 | 81 | base_type, data_type = cls._assemble_datatype(type_name, flags) 82 | 83 | return cls( 84 | flags = flags, 85 | field_name = field_name, 86 | type_name = type_name, 87 | data_type = data_type, 88 | base_type = base_type, 89 | ) 90 | 91 | @classmethod 92 | def _assemble_datatype(cls, type_name: Optional[str], flags: List[int]) -> Tuple[Any, Any]: 93 | """ 94 | Assemble ctypes data type 95 | 96 | Counterpart to `_disassemble_datatype` 97 | """ 98 | 99 | base_type = ctypes.c_void_p # use void pointer as place holder type 100 | if len(flags) > 0: 101 | raise ValueError('flags unsupported for custom data types') 102 | data_type = base_type 103 | 104 | return base_type, data_type 105 | 106 | @classmethod 107 | def _from_data_type( 108 | cls, 109 | flags: List[int], # f 110 | field_name: Union[str, int, None], # n 111 | type_name: Optional[str], # t 112 | data_type: Any, 113 | base_type: Any, 114 | cache: CacheABC, 115 | ): 116 | """ 117 | Simple group-specific helper for from ctypes data type 118 | """ 119 | 120 | return cls( 121 | flags = flags, 122 | field_name = field_name, 123 | type_name = type_name, 124 | data_type = data_type, 125 | base_type = base_type, 126 | ) 127 | -------------------------------------------------------------------------------- /src/zugbruecke/core/dll_client.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | 5 | ZUGBRUECKE 6 | Calling routines in Windows DLLs from Python scripts running on unixlike systems 7 | https://github.com/pleiszenburg/zugbruecke 8 | 9 | src/zugbruecke/core/dll_client.py: Classes for managing the access to DLLs 10 | 11 | Required to run on platform / side: [UNIX] 12 | 13 | Copyright (C) 2017-2023 Sebastian M. Ernst 14 | 15 | 16 | The contents of this file are subject to the GNU Lesser General Public License 17 | Version 2.1 ("LGPL" or "License"). You may not use this file except in 18 | compliance with the License. You may obtain a copy of the License at 19 | https://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt 20 | https://github.com/pleiszenburg/zugbruecke/blob/master/LICENSE 21 | 22 | Software distributed under the License is distributed on an "AS IS" basis, 23 | WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the 24 | specific language governing rights and limitations under the License. 25 | 26 | 27 | """ 28 | 29 | 30 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 31 | # IMPORT 32 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 33 | 34 | from typing import Union 35 | 36 | from .abc import DataABC, DllClientABC, LogABC, RoutineClientABC, RpcClientABC 37 | from .routine_client import RoutineClient 38 | from .typeguard import typechecked 39 | 40 | 41 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 42 | # DLL CLIENT CLASS 43 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 44 | 45 | 46 | @typechecked 47 | class DllClient(DllClientABC): 48 | """ 49 | Representing one idividual dll to be called into, returned by LoadLibrary 50 | """ 51 | 52 | def __init__( 53 | self, 54 | name: str, 55 | hash_id: str, 56 | convention: str, 57 | log: LogABC, 58 | rpc_client: RpcClientABC, 59 | data: DataABC, 60 | ): 61 | 62 | self._name = name 63 | self._hash_id = hash_id 64 | self._convention = convention 65 | 66 | self._log = log 67 | self._rpc_client = rpc_client 68 | self._data = data 69 | 70 | self._routines = {} 71 | 72 | for name in ( 73 | "get_repr", 74 | "register_routine", 75 | ): 76 | setattr( 77 | self, 78 | f"_{name:s}_on_server", 79 | getattr( 80 | self._rpc_client, 81 | f"{self._hash_id:s}_{name:s}", 82 | ), 83 | ) 84 | 85 | def __getattr__(self, name: str) -> RoutineClientABC: 86 | 87 | if name in ("__objclass__", "__name__"): 88 | raise AttributeError(name) 89 | 90 | if name not in self._routines.keys(): 91 | self._register_routine(name) 92 | 93 | return self._routines[name] 94 | 95 | def __getitem__(self, name_or_ordinal: Union[str, int]) -> RoutineClientABC: 96 | 97 | if name_or_ordinal not in self._routines.keys(): 98 | self._register_routine(name_or_ordinal) 99 | 100 | return self._routines[name_or_ordinal] 101 | 102 | def __repr__(self) -> str: 103 | 104 | return self._get_repr_on_server() 105 | 106 | def _register_routine(self, name: Union[str, int]): 107 | 108 | self._log.info(f'[dll-client] Trying to register routine "{str(name):s}" in DLL file "{self._name:s}" ...') 109 | 110 | try: 111 | self._register_routine_on_server(name) 112 | except AttributeError as e: 113 | self._log.info("[dll-client] ... failed!") 114 | raise e 115 | 116 | self._routines[name] = RoutineClient( 117 | name, 118 | self._hash_id, 119 | self._convention, 120 | self._name, 121 | self._log, 122 | self._rpc_client, 123 | self._data, 124 | ) 125 | 126 | self._log.info("[dll-client] ... registered (unconfigured).") 127 | -------------------------------------------------------------------------------- /tests/test_memsync_computed_length.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | 5 | ZUGBRUECKE 6 | Calling routines in Windows DLLs from Python scripts running on unixlike systems 7 | https://github.com/pleiszenburg/zugbruecke 8 | 9 | tests/test_memsync_computed_length.py: Memory sync with lambda function 10 | 11 | Required to run on platform / side: [UNIX, WINE] 12 | 13 | Copyright (C) 2017-2023 Sebastian M. Ernst 14 | 15 | 16 | The contents of this file are subject to the GNU Lesser General Public License 17 | Version 2.1 ("LGPL" or "License"). You may not use this file except in 18 | compliance with the License. You may obtain a copy of the License at 19 | https://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt 20 | https://github.com/pleiszenburg/zugbruecke/blob/master/LICENSE 21 | 22 | Software distributed under the License is distributed on an "AS IS" basis, 23 | WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the 24 | specific language governing rights and limitations under the License. 25 | 26 | 27 | """ 28 | 29 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 30 | # C 31 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 32 | 33 | HEADER = """ 34 | {{ PREFIX }} void {{ SUFFIX }} bubblesort( 35 | float *a, 36 | int u, 37 | int v 38 | ); 39 | """ 40 | 41 | SOURCE = """ 42 | {{ PREFIX }} void {{ SUFFIX }} bubblesort( 43 | float *a, 44 | int u, 45 | int v 46 | ) 47 | { 48 | int i, j; 49 | int n = u * v; 50 | for (i = 0; i < n - 1; ++i) 51 | { 52 | for (j = 0; j < n - i - 1; ++j) 53 | { 54 | if (a[j] > a[j + 1]) 55 | { 56 | float tmp = a[j]; 57 | a[j] = a[j + 1]; 58 | a[j + 1] = tmp; 59 | } 60 | } 61 | } 62 | } 63 | """ 64 | 65 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 66 | # IMPORT 67 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 68 | 69 | from typing import List 70 | 71 | from .lib.ctypes import get_context 72 | 73 | import pytest 74 | 75 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 76 | # TEST(s) 77 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 78 | 79 | 80 | @pytest.mark.parametrize("arch,conv,ctypes,dll_handle", get_context(__file__)) 81 | def test_memsync_computed_length(arch, conv, ctypes, dll_handle): 82 | """ 83 | Memory sync with length computed from multiple arguments via lambda function 84 | """ 85 | 86 | bubblesort_dll = dll_handle.bubblesort 87 | bubblesort_dll.memsync = [ # Regular ctypes on Windows should ignore this statement 88 | dict( 89 | pointer = [0], # "path" to argument containing the pointer 90 | length = ([1], [2]), # "path" to arguments containing information on length 91 | func = "lambda x, y: x * y", # function computing length from relevant arguments 92 | type = ctypes.c_float, # type of argument 93 | ) 94 | ] 95 | bubblesort_dll.argtypes = ( 96 | ctypes.POINTER(ctypes.c_float), 97 | ctypes.c_int, 98 | ctypes.c_int, 99 | ) 100 | 101 | def bubblesort(values: List[float], u: int, v: int): 102 | """ 103 | User-facing wrapper around DLL function 104 | """ 105 | 106 | ct_values = ((ctypes.c_float) * len(values))(*values) 107 | ct_ptr = ctypes.cast(ctypes.pointer(ct_values), ctypes.POINTER(ctypes.c_float)) 108 | bubblesort_dll(ct_ptr, u, v) 109 | values[:] = ct_values[:] 110 | 111 | data = [5.74, 3.72, 6.28, 8.6, 9.34, 6.47, 2.05, 9.09, 4.39, 4.75] 112 | u = 2 113 | bubblesort(data, u = u, v = len(data) // u) 114 | 115 | data = [round(number, 2) for number in data] 116 | expected = [2.05, 3.72, 4.39, 4.75, 5.74, 6.28, 6.47, 8.6, 9.09, 9.34] 117 | diff = sum(abs(a - b) for a, b in zip(data, expected)) 118 | assert pytest.approx(0.0, 0.0000001) == diff 119 | -------------------------------------------------------------------------------- /src/zugbruecke/core/definitions/simple.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | 5 | ZUGBRUECKE 6 | Calling routines in Windows DLLs from Python scripts running on unixlike systems 7 | https://github.com/pleiszenburg/zugbruecke 8 | 9 | src/zugbruecke/core/definitions/simple.py: Simple argument definition 10 | 11 | Required to run on platform / side: [UNIX, WINE] 12 | 13 | Copyright (C) 2017-2023 Sebastian M. Ernst 14 | 15 | 16 | The contents of this file are subject to the GNU Lesser General Public License 17 | Version 2.1 ("LGPL" or "License"). You may not use this file except in 18 | compliance with the License. You may obtain a copy of the License at 19 | https://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt 20 | https://github.com/pleiszenburg/zugbruecke/blob/master/LICENSE 21 | 22 | Software distributed under the License is distributed on an "AS IS" basis, 23 | WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the 24 | specific language governing rights and limitations under the License. 25 | 26 | 27 | """ 28 | 29 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 30 | # IMPORT 31 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 32 | 33 | import ctypes 34 | from typing import Any, Dict, List, Tuple, Union 35 | 36 | from ..abc import CacheABC, DefinitionABC 37 | from ..const import SIMPLE_GROUP 38 | from ..typeguard import typechecked 39 | 40 | from . import base 41 | 42 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 43 | # CLASS 44 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 45 | 46 | @typechecked 47 | class DefinitionSimple(base.Definition): 48 | 49 | GROUP = SIMPLE_GROUP 50 | 51 | def __init__(self, *args: Any, **kwargs: Any): 52 | 53 | super().__init__(*args, **kwargs) 54 | 55 | def as_packed(self) -> Dict: 56 | """ 57 | Pack as dict so it can be sent to other side 58 | 59 | Counterpart to `from_packed` 60 | """ 61 | 62 | return { 63 | "group": self.GROUP, 64 | "flags": self._flags, 65 | "field_name": self._field_name, 66 | "type_name": self._type_name, 67 | } 68 | 69 | @classmethod 70 | def from_packed(cls, 71 | flags: List[int], # f 72 | field_name: Union[str, int, None], # n 73 | type_name: str, # t 74 | ) -> DefinitionABC: 75 | """ 76 | Unpack from dict received from other side 77 | 78 | Counterpart to `as_packed` 79 | """ 80 | 81 | base_type, data_type = cls._assemble_datatype(type_name, flags) 82 | 83 | return cls( 84 | flags = flags, 85 | field_name = field_name, 86 | type_name = type_name, 87 | data_type = data_type, 88 | base_type = base_type, 89 | ) 90 | 91 | @classmethod 92 | def _assemble_datatype(cls, type_name: str, flags: List[int]) -> Tuple[Any, Any]: 93 | """ 94 | Assemble ctypes data type 95 | 96 | Counterpart to `_disassemble_datatype` 97 | """ 98 | 99 | base_type = getattr(ctypes, type_name) 100 | data_type = cls._apply_flags(base_type, flags) 101 | 102 | return base_type, data_type 103 | 104 | @classmethod 105 | def _from_data_type( 106 | cls, 107 | flags: List[int], # f 108 | field_name: Union[str, int, None], # n 109 | type_name: str, # t 110 | data_type: Any, 111 | base_type: Any, 112 | cache: CacheABC, 113 | ): 114 | """ 115 | Simple group-specific helper for from ctypes data type 116 | """ 117 | 118 | if type_name in ('c_int', 'c_long', 'c_longlong'): 119 | type_name = f'c_int{ctypes.sizeof(base_type)*8:d}' 120 | if type_name in ('c_uint', 'c_ulong', 'c_ulonglong'): 121 | type_name = f'c_uint{ctypes.sizeof(base_type)*8:d}' 122 | 123 | return cls( 124 | flags = flags, 125 | field_name = field_name, 126 | type_name = type_name, 127 | data_type = data_type, 128 | base_type = base_type, 129 | ) 130 | -------------------------------------------------------------------------------- /benchmark/memsync.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | 5 | ZUGBRUECKE 6 | Calling routines in Windows DLLs from Python scripts running on unixlike systems 7 | https://github.com/pleiszenburg/zugbruecke 8 | 9 | benchmark/memsync.py: Minimal call, two parameters, no pointers or memsync 10 | 11 | Required to run on platform / side: [UNIX, WINE] 12 | 13 | Copyright (C) 2017-2023 Sebastian M. Ernst 14 | 15 | 16 | The contents of this file are subject to the GNU Lesser General Public License 17 | Version 2.1 ("LGPL" or "License"). You may not use this file except in 18 | compliance with the License. You may obtain a copy of the License at 19 | https://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt 20 | https://github.com/pleiszenburg/zugbruecke/blob/master/LICENSE 21 | 22 | Software distributed under the License is distributed on an "AS IS" basis, 23 | WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the 24 | specific language governing rights and limitations under the License. 25 | 26 | 27 | """ 28 | 29 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 30 | # C 31 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 32 | 33 | HEADER = """ 34 | {{ PREFIX }} void {{ SUFFIX }} bubblesort( 35 | float *a, 36 | int n 37 | ); 38 | """ 39 | 40 | SOURCE = """ 41 | {{ PREFIX }} void {{ SUFFIX }} bubblesort( 42 | float *a, 43 | int n 44 | ) 45 | { 46 | int i, j; 47 | for (i = 0; i < n - 1; ++i) 48 | { 49 | for (j = 0; j < n - i - 1; ++j) 50 | { 51 | if (a[j] > a[j + 1]) 52 | { 53 | float tmp = a[j]; 54 | a[j] = a[j + 1]; 55 | a[j + 1] = tmp; 56 | } 57 | } 58 | } 59 | } 60 | """ 61 | 62 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 63 | # IMPORT 64 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 65 | 66 | from math import isclose 67 | from typing import List 68 | 69 | from tests.lib.benchmark import benchmark 70 | 71 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 72 | # BENCHMARK(s) 73 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 74 | 75 | 76 | def init(ctypes, dll_handle, conv): 77 | 78 | bubblesort_dll = dll_handle.bubblesort 79 | bubblesort_dll.argtypes = (ctypes.POINTER(ctypes.c_float), ctypes.c_int) 80 | bubblesort_dll.memsync = [ # Regular ctypes on Windows should ignore this statement 81 | dict( 82 | pointer = [0], # "path" to argument containing the pointer 83 | length = [1], # "path" to argument containing the length 84 | type = ctypes.c_float, # type of argument (optional, default char/byte): sizeof(type) * length == bytes 85 | ) 86 | ] 87 | 88 | def bubblesort(values: List[float]): 89 | """ 90 | User-facing wrapper around DLL function 91 | """ 92 | 93 | ct_values = ((ctypes.c_float) * len(values))(*values) 94 | ct_ptr = ctypes.cast(ctypes.pointer(ct_values), ctypes.POINTER(ctypes.c_float)) 95 | bubblesort_dll(ct_ptr, len(values)) 96 | values[:] = ct_values[:] 97 | 98 | return bubblesort 99 | 100 | 101 | @benchmark(fn = __file__, initializer = init) 102 | def memsync(ctypes, func): 103 | """ 104 | The "memsync" benchmark is a basic test of bidirectional memory synchronization 105 | via a ``memsync`` directive for a pointer argument, 106 | an array of single-precision floating point numbers. 107 | The benchmark uses 10 numbers per array. 108 | It is passed to the DLL function, 109 | next to the array's length as an ``c_int``. 110 | The DLL function performs a classic bubblesort algorithm in-place 111 | on the passed / synchronized memory. 112 | """ 113 | 114 | data = [5.74, 3.72, 6.28, 8.6, 9.34, 6.47, 2.05, 9.09, 4.39, 4.75] 115 | expected = [2.05, 3.72, 4.39, 4.75, 5.74, 6.28, 6.47, 8.6, 9.09, 9.34] 116 | 117 | func(data) 118 | 119 | diff = sum(abs(round(a, 2) - b) for a, b in zip(data, expected)) 120 | assert isclose(0.0, diff) 121 | -------------------------------------------------------------------------------- /tests/lib/run.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | 5 | ZUGBRUECKE 6 | Calling routines in Windows DLLs from Python scripts running on unixlike systems 7 | https://github.com/pleiszenburg/zugbruecke 8 | 9 | tests/lib/run.py: Test runner 10 | 11 | Required to run on platform / side: [UNIX, WINE] 12 | 13 | Copyright (C) 2017-2023 Sebastian M. Ernst 14 | 15 | 16 | The contents of this file are subject to the GNU Lesser General Public License 17 | Version 2.1 ("LGPL" or "License"). You may not use this file except in 18 | compliance with the License. You may obtain a copy of the License at 19 | https://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt 20 | https://github.com/pleiszenburg/zugbruecke/blob/master/LICENSE 21 | 22 | Software distributed under the License is distributed on an "AS IS" basis, 23 | WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the 24 | specific language governing rights and limitations under the License. 25 | 26 | 27 | """ 28 | 29 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 30 | # IMPORT 31 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 32 | 33 | from logging import DEBUG 34 | import os 35 | import sys 36 | 37 | from typeguard import typechecked 38 | from wenv import EnvConfig 39 | 40 | from .const import PYTHONBUILDS_FN 41 | from .cmd import run_cmd 42 | from .pythonversion import read_python_builds 43 | 44 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 45 | # ROUTINES 46 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 47 | 48 | def run_tests(): 49 | """ 50 | Dispatcher: Wine/Unix 51 | """ 52 | 53 | if len(sys.argv) < 2: 54 | raise SystemError('expected at least 2 arguments, got other', len(sys.argv)) 55 | 56 | target = sys.argv[1] 57 | 58 | if target == 'wine': 59 | _run_tests_wine(*sys.argv[2:]) 60 | elif target == 'unix': 61 | _run_tests_unix(*sys.argv[2:]) 62 | else: 63 | raise SystemError('unknown test target', target) 64 | 65 | 66 | @typechecked 67 | def _run_tests_wine(*args: str): 68 | """ 69 | Runs test suite on Wine as if it was running on Windows, 70 | i.e. testing & verifying against original ctypes. 71 | No coverage recorded. 72 | 73 | Args: 74 | - args: Remaining command line arguments 75 | """ 76 | 77 | cfg = EnvConfig() 78 | builds = read_python_builds(fn = os.path.join(cfg['prefix'], PYTHONBUILDS_FN)) 79 | 80 | for arch, _builds in builds.items(): 81 | for build in _builds: 82 | run_cmd( 83 | cmd = ['make', '_clean_py'], 84 | ) 85 | run_cmd( 86 | cmd = [ 87 | 'wenv', 'pytest', 88 | '--hypothesis-show-statistics', *args 89 | ], 90 | env = { 91 | 'WENV_DEBUG': '1', 92 | 'WENV_ARCH': arch, 93 | 'WENV_PYTHONVERSION': str(build), 94 | }, 95 | ) 96 | 97 | 98 | @typechecked 99 | def _run_tests_unix(*args: str): 100 | """ 101 | Does a single run of pytest. ARCH and PYTHONVERSION are parameterized within pytest. 102 | 103 | Args: 104 | - args: Remaining command line arguments 105 | """ 106 | 107 | run_cmd( 108 | cmd = ['make', '_clean_py'], 109 | ) 110 | run_cmd( 111 | cmd = [ 112 | 'pytest', 113 | '--cov=zugbruecke', 114 | # '--cov-config=setup.cfg', 115 | '--hypothesis-show-statistics', 116 | # '--capture=no', 117 | *args, 118 | ], 119 | env = { 120 | 'WENV_DEBUG': '1', 121 | 'ZUGBRUECKE_DEBUG': '1', 122 | 'ZUGBRUECKE_LOG_LEVEL': f'{DEBUG:d}', 123 | # 'ZUGBRUECKE_LOG_WRITE': 'True', 124 | }, 125 | ) 126 | 127 | 128 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 129 | # MODULE ENTRY POINT 130 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 131 | 132 | if __name__ == "__main__": 133 | 134 | run_tests() 135 | -------------------------------------------------------------------------------- /tests/test_memsync_in_struct.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | 5 | ZUGBRUECKE 6 | Calling routines in Windows DLLs from Python scripts running on unixlike systems 7 | https://github.com/pleiszenburg/zugbruecke 8 | 9 | tests/test_memsync_in_struct.py: Test bidirectional memory sync for pointers in struct 10 | 11 | Required to run on platform / side: [UNIX, WINE] 12 | 13 | Copyright (C) 2017-2023 Sebastian M. Ernst 14 | 15 | 16 | The contents of this file are subject to the GNU Lesser General Public License 17 | Version 2.1 ("LGPL" or "License"). You may not use this file except in 18 | compliance with the License. You may obtain a copy of the License at 19 | https://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt 20 | https://github.com/pleiszenburg/zugbruecke/blob/master/LICENSE 21 | 22 | Software distributed under the License is distributed on an "AS IS" basis, 23 | WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the 24 | specific language governing rights and limitations under the License. 25 | 26 | 27 | """ 28 | 29 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 30 | # C 31 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 32 | 33 | HEADER = """ 34 | typedef struct bubblesort_data { 35 | float *a; 36 | int n; 37 | } bubblesort_data; 38 | 39 | {{ PREFIX }} void {{ SUFFIX }} bubblesort_struct( 40 | bubblesort_data *data 41 | ); 42 | """ 43 | 44 | SOURCE = """ 45 | {{ PREFIX }} void {{ SUFFIX }} bubblesort_struct( 46 | bubblesort_data *data 47 | ) 48 | { 49 | int i, j; 50 | for (i = 0; i < data->n - 1; ++i) 51 | { 52 | for (j = 0; j < data->n - i - 1; ++j) 53 | { 54 | if (data->a[j] > data->a[j + 1]) 55 | { 56 | float tmp = data->a[j]; 57 | data->a[j] = data->a[j + 1]; 58 | data->a[j + 1] = tmp; 59 | } 60 | } 61 | } 62 | } 63 | """ 64 | 65 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 66 | # IMPORT 67 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 68 | 69 | from typing import List 70 | 71 | from .lib.ctypes import get_context 72 | 73 | import pytest 74 | 75 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 76 | # TEST(s) 77 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 78 | 79 | 80 | @pytest.mark.parametrize("arch,conv,ctypes,dll_handle", get_context(__file__)) 81 | def test_memsync_in_struct(arch, conv, ctypes, dll_handle): 82 | """ 83 | Memsync pointer and length argument within struct that is passed as a pointer itself 84 | """ 85 | 86 | class BubblesortData(ctypes.Structure): 87 | _fields_ = [("a", ctypes.POINTER(ctypes.c_float)), ("n", ctypes.c_int)] 88 | 89 | bubblesort_struct_dll = dll_handle.bubblesort_struct 90 | bubblesort_struct_dll.memsync = [ # Regular ctypes on Windows should ignore this statement 91 | dict( 92 | pointer = [0, "a"], # "path" to argument containing the pointer 93 | length = [0, "n"], # "path" to argument containing the length 94 | type = ctypes.c_float, # type of argument (optional, default char/byte): sizeof(type) * length == bytes 95 | ) 96 | ] 97 | bubblesort_struct_dll.argtypes = (ctypes.POINTER(BubblesortData),) 98 | 99 | def bubblesort_struct(values: List[float]): 100 | """ 101 | User-facing wrapper around DLL function 102 | """ 103 | 104 | ct_values = ((ctypes.c_float) * len(values))(*values) 105 | ct_ptr = ctypes.cast(ctypes.pointer(ct_values), ctypes.POINTER(ctypes.c_float)) 106 | bubblesort_struct_dll(ctypes.pointer(BubblesortData(ct_ptr, len(values)))) 107 | values[:] = ct_values[:] 108 | 109 | data = [5.74, 3.72, 6.28, 8.6, 9.34, 6.47, 2.05, 9.09, 4.39, 4.75] 110 | bubblesort_struct(data) 111 | 112 | data = [round(number, 2) for number in data] 113 | expected = [2.05, 3.72, 4.39, 4.75, 5.74, 6.28, 6.47, 8.6, 9.09, 9.34] 114 | diff = sum(abs(a - b) for a, b in zip(data, expected)) 115 | 116 | assert pytest.approx(0.0, 0.0000001) == diff 117 | -------------------------------------------------------------------------------- /tests/test_pointers_and_array_in_struct_malloc_by_dll.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | 5 | ZUGBRUECKE 6 | Calling routines in Windows DLLs from Python scripts running on unixlike systems 7 | https://github.com/pleiszenburg/zugbruecke 8 | 9 | tests/test_pointers_and_array_in_struct_malloc_by_dll.py: Test array pointer in struct and allocation of memory by DLL 10 | 11 | Required to run on platform / side: [UNIX, WINE] 12 | 13 | Copyright (C) 2017-2023 Sebastian M. Ernst 14 | 15 | 16 | The contents of this file are subject to the GNU Lesser General Public License 17 | Version 2.1 ("LGPL" or "License"). You may not use this file except in 18 | compliance with the License. You may obtain a copy of the License at 19 | https://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt 20 | https://github.com/pleiszenburg/zugbruecke/blob/master/LICENSE 21 | 22 | Software distributed under the License is distributed on an "AS IS" basis, 23 | WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the 24 | specific language governing rights and limitations under the License. 25 | 26 | 27 | """ 28 | 29 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 30 | # C 31 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 32 | 33 | HEADER = """ 34 | typedef struct int_array_data { 35 | int16_t *data; 36 | int16_t len; 37 | } int_array_data; 38 | 39 | {{ PREFIX }} void {{ SUFFIX }} square_int_array_with_struct( 40 | int_array_data *in_array, 41 | int_array_data *out_array 42 | ); 43 | """ 44 | 45 | SOURCE = """ 46 | {{ PREFIX }} void {{ SUFFIX }} square_int_array_with_struct( 47 | int_array_data *in_array, 48 | int_array_data *out_array 49 | ) 50 | { 51 | int i; 52 | out_array->len = in_array->len; 53 | out_array->data = malloc(sizeof(int16_t) * out_array->len); 54 | for(i = 0; i < in_array->len; i++) 55 | { 56 | out_array->data[i] = in_array->data[i] * in_array->data[i]; 57 | } 58 | } 59 | """ 60 | 61 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 62 | # IMPORT 63 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 64 | 65 | from typing import List 66 | 67 | from .lib.ctypes import get_context 68 | 69 | import pytest 70 | 71 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 72 | # TEST(s) 73 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 74 | 75 | 76 | @pytest.mark.parametrize("arch,conv,ctypes,dll_handle", get_context(__file__)) 77 | def test_pointers_and_array_in_struct_malloc_by_dll(arch, conv, ctypes, dll_handle): 78 | """ 79 | Test array pointer in struct and allocation of memory by DLL 80 | """ 81 | 82 | class ArrayData(ctypes.Structure): 83 | _fields_ = [ 84 | ("data", ctypes.POINTER(ctypes.c_int16)), 85 | ("len", ctypes.c_int16), 86 | ] 87 | 88 | square_int_array_with_struct_dll = dll_handle.square_int_array_with_struct 89 | square_int_array_with_struct_dll.argtypes = ( 90 | ctypes.POINTER(ArrayData), 91 | ctypes.POINTER(ArrayData), 92 | ) 93 | square_int_array_with_struct_dll.memsync = [ 94 | dict( 95 | pointer = [0, "data"], 96 | length = [0, "len"], 97 | type = ctypes.c_int16, 98 | ), 99 | dict( 100 | pointer = [1, "data"], 101 | length = [1, "len"], 102 | type = ctypes.c_int16, 103 | ), 104 | ] 105 | 106 | def square_int_array_with_struct(data: List[int]) -> List[int]: 107 | """ 108 | User-facing wrapper around DLL function 109 | """ 110 | 111 | in_struct = ArrayData( 112 | ctypes.cast( 113 | ctypes.pointer((ctypes.c_int16 * len(data))(*data)), 114 | ctypes.POINTER(ctypes.c_int16), 115 | ), 116 | len(data), 117 | ) 118 | out_struct = ArrayData() 119 | 120 | square_int_array_with_struct_dll(in_struct, out_struct) 121 | 122 | return ctypes.cast( 123 | out_struct.data, ctypes.POINTER(ctypes.c_int16 * len(data)) 124 | ).contents[:] 125 | 126 | assert [4, 16, 9, 25] == square_int_array_with_struct([2, 4, 3, 5]) 127 | -------------------------------------------------------------------------------- /tests/test_pointers_byref.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | 5 | ZUGBRUECKE 6 | Calling routines in Windows DLLs from Python scripts running on unixlike systems 7 | https://github.com/pleiszenburg/zugbruecke 8 | 9 | tests/test_pointers_byref.py: Testing "light-weight pointers" 10 | 11 | Required to run on platform / side: [UNIX, WINE] 12 | 13 | Copyright (C) 2017-2023 Sebastian M. Ernst 14 | 15 | 16 | The contents of this file are subject to the GNU Lesser General Public License 17 | Version 2.1 ("LGPL" or "License"). You may not use this file except in 18 | compliance with the License. You may obtain a copy of the License at 19 | https://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt 20 | https://github.com/pleiszenburg/zugbruecke/blob/master/LICENSE 21 | 22 | Software distributed under the License is distributed on an "AS IS" basis, 23 | WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the 24 | specific language governing rights and limitations under the License. 25 | 26 | 27 | """ 28 | 29 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 30 | # C 31 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 32 | 33 | HEADER = """ 34 | typedef struct subparam { 35 | int a; 36 | int b; 37 | int diff; 38 | } subparam; 39 | 40 | typedef struct mulparam { 41 | float a; 42 | float b; 43 | float prod; 44 | } mulparam; 45 | 46 | {{ PREFIX }} void {{ SUFFIX }} add_int( 47 | int a, 48 | int b, 49 | int *sum 50 | ); 51 | 52 | {{ PREFIX }} void {{ SUFFIX }} sub_int( 53 | subparam *values 54 | ); 55 | 56 | {{ PREFIX }} void {{ SUFFIX }} mul_float( 57 | mulparam *values 58 | ); 59 | """ 60 | 61 | SOURCE = """ 62 | {{ PREFIX }} void {{ SUFFIX }} add_int( 63 | int a, 64 | int b, 65 | int *sum 66 | ) 67 | { 68 | *sum = a + b; 69 | } 70 | 71 | {{ PREFIX }} void {{ SUFFIX }} sub_int( 72 | subparam *values 73 | ) 74 | { 75 | values->diff = values->a - values->b; 76 | } 77 | 78 | {{ PREFIX }} void {{ SUFFIX }} mul_float( 79 | mulparam *values 80 | ) 81 | { 82 | values->prod = values->a * values->b; 83 | } 84 | """ 85 | 86 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 87 | # IMPORT 88 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 89 | 90 | from .lib.ctypes import get_context 91 | 92 | import pytest 93 | 94 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 95 | # TEST(s) 96 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 97 | 98 | 99 | @pytest.mark.parametrize("arch,conv,ctypes,dll_handle", get_context(__file__)) 100 | def test_pointers_byref_simple(arch, conv, ctypes, dll_handle): 101 | """ 102 | Tests byref with simple data type 103 | """ 104 | 105 | add_int = dll_handle.add_int 106 | add_int.argtypes = (ctypes.c_int, ctypes.c_int, ctypes.POINTER(ctypes.c_int)) 107 | 108 | sum_ = ctypes.c_int() 109 | add_int(3, 4, ctypes.byref(sum_)) 110 | assert sum_.value == 7 111 | 112 | 113 | @pytest.mark.parametrize("arch,conv,ctypes,dll_handle", get_context(__file__)) 114 | def test_pointers_byref_struct_int(arch, conv, ctypes, dll_handle): 115 | """ 116 | Tests byref with struct data type containing int 117 | """ 118 | 119 | class SubParam(ctypes.Structure): 120 | _fields_ = [ 121 | ("a", ctypes.c_int), 122 | ("b", ctypes.c_int), 123 | ("diff", ctypes.c_int), 124 | ] 125 | 126 | sub_int = dll_handle.sub_int 127 | sub_int.argtypes = (ctypes.POINTER(SubParam),) 128 | 129 | param = SubParam(17, 15, 0) 130 | sub_int(ctypes.byref(param)) 131 | assert param.diff == 2 132 | 133 | 134 | @pytest.mark.parametrize("arch,conv,ctypes,dll_handle", get_context(__file__)) 135 | def test_pointers_byref_struct_float(arch, conv, ctypes, dll_handle): 136 | """ 137 | Tests byref with struct data type containing float 138 | """ 139 | 140 | class MulParam(ctypes.Structure): 141 | _fields_ = [ 142 | ("a", ctypes.c_float), 143 | ("b", ctypes.c_float), 144 | ("prod", ctypes.c_float), 145 | ] 146 | 147 | mul_float = dll_handle.mul_float 148 | mul_float.argtypes = (ctypes.POINTER(MulParam),) 149 | 150 | param = MulParam(5.0, 6.0, 0.0) 151 | mul_float(ctypes.byref(param)) 152 | assert pytest.approx(30.0) == param.prod 153 | -------------------------------------------------------------------------------- /tests/test_memsync_struct_array.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | 5 | ZUGBRUECKE 6 | Calling routines in Windows DLLs from Python scripts running on unixlike systems 7 | https://github.com/pleiszenburg/zugbruecke 8 | 9 | tests/test_memsync_struct_array.py: Tests memsync on an array of structs 10 | 11 | Required to run on platform / side: [UNIX, WINE] 12 | 13 | Copyright (C) 2017-2023 Sebastian M. Ernst 14 | 15 | 16 | The contents of this file are subject to the GNU Lesser General Public License 17 | Version 2.1 ("LGPL" or "License"). You may not use this file except in 18 | compliance with the License. You may obtain a copy of the License at 19 | https://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt 20 | https://github.com/pleiszenburg/zugbruecke/blob/master/LICENSE 21 | 22 | Software distributed under the License is distributed on an "AS IS" basis, 23 | WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the 24 | specific language governing rights and limitations under the License. 25 | 26 | 27 | """ 28 | 29 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 30 | # C 31 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 32 | 33 | HEADER = """ 34 | typedef struct Vector5d { 35 | int16_t x, y, z, u, w; 36 | } Vector5d; 37 | 38 | {{ PREFIX }} Vector5d {{ SUFFIX }} *vector5d_add_array( 39 | Vector5d *v, 40 | int16_t len 41 | ); 42 | """ 43 | 44 | SOURCE = """ 45 | {{ PREFIX }} Vector5d {{ SUFFIX }} *vector5d_add_array( 46 | Vector5d *v, 47 | int16_t len 48 | ) 49 | { 50 | 51 | int16_t i; 52 | 53 | Vector5d *v_out = malloc(sizeof(Vector5d)); 54 | v_out->x = 0; 55 | v_out->y = 0; 56 | v_out->z = 0; 57 | v_out->u = 0; 58 | v_out->w = 0; 59 | 60 | for(i = 0; i < len; i++) 61 | { 62 | v_out->x += v[i].x; 63 | v_out->y += v[i].y; 64 | v_out->z += v[i].z; 65 | v_out->u += v[i].u; 66 | v_out->w += v[i].w; 67 | } 68 | 69 | return v_out; 70 | 71 | } 72 | """ 73 | 74 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 75 | # IMPORT 76 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 77 | 78 | from typing import Dict, List 79 | 80 | from .lib.ctypes import get_context 81 | 82 | import pytest 83 | 84 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 85 | # TEST(s) 86 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 87 | 88 | 89 | @pytest.mark.parametrize("arch,conv,ctypes,dll_handle", get_context(__file__)) 90 | def test_memsync_struct_array(arch, conv, ctypes, dll_handle): 91 | """ 92 | Tests memsync on an array of struct objects 93 | """ 94 | 95 | class Vector5d(ctypes.Structure): 96 | _fields_ = [ 97 | ("x", ctypes.c_int16), 98 | ("y", ctypes.c_int16), 99 | ("z", ctypes.c_int16), 100 | ("u", ctypes.c_int16), 101 | ("w", ctypes.c_int16), 102 | ] 103 | 104 | vector5d_add_array_dll = dll_handle.vector5d_add_array 105 | vector5d_add_array_dll.argtypes = ( 106 | ctypes.POINTER(Vector5d), 107 | ctypes.c_int16, 108 | ) 109 | vector5d_add_array_dll.restype = ctypes.POINTER(Vector5d) 110 | vector5d_add_array_dll.memsync = [ 111 | {"p": [0], "l": [1], "t": Vector5d} 112 | ] 113 | 114 | def vector5d_add_array(vectors: List[Dict[str, int]]) -> Dict[str, int]: 115 | """ 116 | User-facing wrapper around DLL function 117 | """ 118 | 119 | length = len(vectors) 120 | 121 | ct_vectors = (Vector5d * length)() 122 | for idx, vector in enumerate(vectors): 123 | for name, value in vector.items(): 124 | setattr(ct_vectors[idx], name, value) 125 | 126 | return dict_from_struct( 127 | vector5d_add_array_dll( 128 | ctypes.cast( 129 | ctypes.pointer(ct_vectors), ctypes.POINTER(Vector5d) 130 | ), 131 | length, 132 | ) 133 | ) 134 | 135 | def dict_from_struct(vector: Vector5d) -> Dict[str, int]: 136 | """ 137 | Helper 138 | """ 139 | 140 | return {name: getattr(vector.contents, name) for name, _ in Vector5d._fields_} 141 | 142 | v = [ 143 | {"x": 5, "y": 7, "z": 2, "u": 4, "w": 6}, 144 | {"x": 1, "y": 9, "z": 8, "u": 3, "w": 2}, 145 | ] 146 | added = {"x": 6, "y": 16, "z": 10, "u": 7, "w": 8} 147 | 148 | assert added == vector5d_add_array(v) 149 | -------------------------------------------------------------------------------- /tests/lib/install.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | 5 | ZUGBRUECKE 6 | Calling routines in Windows DLLs from Python scripts running on unixlike systems 7 | https://github.com/pleiszenburg/zugbruecke 8 | 9 | tests/lib/install.py: Sets Wine Python environments up 10 | 11 | Required to run on platform / side: [UNIX] 12 | 13 | Copyright (C) 2017-2023 Sebastian M. Ernst 14 | 15 | 16 | The contents of this file are subject to the GNU Lesser General Public License 17 | Version 2.1 ("LGPL" or "License"). You may not use this file except in 18 | compliance with the License. You may obtain a copy of the License at 19 | https://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt 20 | https://github.com/pleiszenburg/zugbruecke/blob/master/LICENSE 21 | 22 | Software distributed under the License is distributed on an "AS IS" basis, 23 | WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the 24 | specific language governing rights and limitations under the License. 25 | 26 | 27 | """ 28 | 29 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 30 | # IMPORT 31 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 32 | 33 | import os 34 | from subprocess import Popen 35 | from typing import Dict, List 36 | 37 | from toml import loads 38 | from typeguard import typechecked 39 | 40 | from zugbruecke import Config, Env 41 | 42 | from wenv import ( 43 | EnvConfig, 44 | PythonVersion, 45 | get_available_python_builds, 46 | get_latest_python_build, 47 | ) 48 | 49 | from .const import ( 50 | ARCHS, 51 | PYTHONBUILDS_FN, 52 | PYTHON_MINOR_MAX, 53 | PYTHON_MINOR_MIN, 54 | ) 55 | from .pythonversion import write_python_builds 56 | 57 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 58 | # ROUTINES 59 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 60 | 61 | def install(): 62 | """ 63 | Create all required Wine Python environments for testing 64 | """ 65 | 66 | cfg = EnvConfig() 67 | builds = _get_latest_python_builds() 68 | 69 | write_python_builds(fn = os.path.join(cfg['prefix'], PYTHONBUILDS_FN), builds = builds) 70 | 71 | for arch in ARCHS: 72 | for build in builds[arch]: 73 | _install_env(arch, build) 74 | 75 | 76 | @typechecked 77 | def _get_latest_python_builds() -> Dict[str, List[PythonVersion]]: 78 | """ 79 | Create a list per architecture of the latest Python maintenance release per minor version 80 | """ 81 | 82 | _builds = get_available_python_builds() 83 | 84 | builds = { 85 | arch: [ 86 | get_latest_python_build(arch, 3, minor, builds = _builds) 87 | for minor in range( 88 | PYTHON_MINOR_MIN, # min minor version 89 | PYTHON_MINOR_MAX + 1, # max major version 90 | ) 91 | ] 92 | for arch in ARCHS 93 | } 94 | for value in builds.values(): 95 | value.sort() 96 | 97 | return builds 98 | 99 | 100 | @typechecked 101 | def _install_env(arch: str, build: PythonVersion): 102 | """ 103 | Create a Wine Python environment 104 | 105 | Args: 106 | - arch: Architecture 107 | - build: Python version 108 | """ 109 | 110 | print(f'') 111 | 112 | envvars = os.environ.copy() 113 | envvars.update({ 114 | 'WENV_DEBUG': '1', 115 | 'WENV_ARCH': arch, 116 | 'WENV_PYTHONVERSION': str(build), 117 | }) 118 | 119 | with open('pyproject.toml', mode = 'r', encoding = 'utf-8') as f: 120 | pyproject = loads(f.read()) 121 | 122 | for cmd in ( 123 | ['wenv', 'init'], 124 | ['wenv', 'pip', 'install'] + pyproject['project']['optional-dependencies']['test'], 125 | ['wenv', 'init_coverage'], 126 | ): 127 | proc = Popen(cmd, env = envvars) 128 | proc.wait() 129 | if proc.returncode != 0: 130 | raise SystemError('wenv setup command failed', arch, build, cmd) 131 | 132 | env = Env(**EnvConfig( 133 | arch = arch, 134 | pythonversion = build, 135 | copy_modules = Config()['copy_modules'], # pass option on 136 | ).export_dict()) 137 | env.setup_zugbruecke() # link packages to wenv python environment 138 | 139 | 140 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 141 | # MODULE ENTRY POINT 142 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 143 | 144 | if __name__ == "__main__": 145 | 146 | install() 147 | -------------------------------------------------------------------------------- /tests/test_callback_optional.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | 5 | ZUGBRUECKE 6 | Calling routines in Windows DLLs from Python scripts running on unixlike systems 7 | https://github.com/pleiszenburg/zugbruecke 8 | 9 | tests/test_callback_optional.py: Optional callback routines as arguments 10 | 11 | Required to run on platform / side: [UNIX, WINE] 12 | 13 | Copyright (C) 2017-2023 Sebastian M. Ernst 14 | 15 | 16 | The contents of this file are subject to the GNU Lesser General Public License 17 | Version 2.1 ("LGPL" or "License"). You may not use this file except in 18 | compliance with the License. You may obtain a copy of the License at 19 | https://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt 20 | https://github.com/pleiszenburg/zugbruecke/blob/master/LICENSE 21 | 22 | Software distributed under the License is distributed on an "AS IS" basis, 23 | WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the 24 | specific language governing rights and limitations under the License. 25 | 26 | 27 | """ 28 | 29 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 30 | # C 31 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 32 | 33 | HEADER = """ 34 | typedef int16_t {{ SUFFIX }} (*conveyor_belt)(int16_t index); 35 | 36 | {{ PREFIX }} int16_t {{ SUFFIX }} use_optional_callback_a( 37 | int16_t in_data, 38 | conveyor_belt process_data 39 | ); 40 | 41 | {{ PREFIX }} int16_t {{ SUFFIX }} use_optional_callback_b( 42 | int16_t in_data, 43 | conveyor_belt process_data 44 | ); 45 | """ 46 | 47 | SOURCE = """ 48 | {{ PREFIX }} int16_t {{ SUFFIX }} use_optional_callback_a( 49 | int16_t in_data, 50 | conveyor_belt process_data 51 | ) 52 | { 53 | int16_t tmp; 54 | if(process_data) { 55 | tmp = process_data(in_data); 56 | } else { 57 | tmp = in_data; 58 | } 59 | return tmp * 2; 60 | } 61 | 62 | {{ PREFIX }} int16_t {{ SUFFIX }} use_optional_callback_b( 63 | int16_t in_data, 64 | conveyor_belt process_data 65 | ) 66 | { 67 | int16_t tmp; 68 | if(process_data) { 69 | tmp = process_data(in_data); 70 | } else { 71 | tmp = in_data; 72 | } 73 | return tmp * 2; 74 | } 75 | """ 76 | 77 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 78 | # IMPORT 79 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 80 | 81 | from .lib.ctypes import get_context 82 | 83 | import pytest 84 | 85 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 86 | # TEST(s) 87 | # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 88 | 89 | 90 | @pytest.mark.parametrize("arch,conv,ctypes,dll_handle", get_context(__file__)) 91 | def test_use_optional_callback(arch, conv, ctypes, dll_handle): 92 | """ 93 | Optional callback routines as arguments: With callbark 94 | """ 95 | 96 | if conv == "cdll": 97 | func_type = ctypes.CFUNCTYPE 98 | elif conv == "windll": 99 | func_type = ctypes.WINFUNCTYPE 100 | else: 101 | raise ValueError("unknown calling convention", conv) 102 | 103 | ConveyorBelt = func_type(ctypes.c_int16, ctypes.c_int16) 104 | 105 | use_optional_callback_dll = dll_handle.use_optional_callback_a 106 | use_optional_callback_dll.argtypes = (ctypes.c_int16, ConveyorBelt) 107 | use_optional_callback_dll.restype = ctypes.c_int16 108 | 109 | @ConveyorBelt 110 | def process_data(number): 111 | """ 112 | Callback function, called by DLL function 113 | """ 114 | 115 | return number ** 2 116 | 117 | def use_optional_callback(number: int) -> int: 118 | """ 119 | User-facing wrapper around DLL function 120 | """ 121 | 122 | return use_optional_callback_dll(number, process_data) 123 | 124 | assert 18 == use_optional_callback(3) 125 | 126 | 127 | @pytest.mark.parametrize("arch,conv,ctypes,dll_handle", get_context(__file__)) 128 | def test_do_not_use_optional_callback(arch, conv, ctypes, dll_handle): 129 | """ 130 | Optional callback routines as arguments: Without callbark 131 | """ 132 | 133 | use_optional_callback_dll = dll_handle.use_optional_callback_b 134 | use_optional_callback_dll.argtypes = (ctypes.c_int16, ctypes.c_void_p) 135 | use_optional_callback_dll.restype = ctypes.c_int16 136 | 137 | def do_not_use_optional_callback(number): 138 | """ 139 | User-facing wrapper around DLL function 140 | """ 141 | 142 | return use_optional_callback_dll(number, None) 143 | 144 | assert 14 == do_not_use_optional_callback(7) 145 | --------------------------------------------------------------------------------