├── .github └── workflows │ ├── ci.yml │ └── docs.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE.txt ├── README.md ├── conftest.py ├── cspell.json ├── docs ├── api │ ├── activex.md │ ├── client64.md │ ├── exceptions.md │ ├── freeze_server32.md │ ├── load_library.md │ ├── server32.md │ ├── types.md │ └── utils.md ├── assets │ └── images │ │ ├── dotpeek_lib.png │ │ ├── favicon.ico │ │ └── labview_lib.png ├── examples │ ├── cpp32.md │ ├── cpp64.md │ ├── dotnet32.md │ ├── dotnet64.md │ ├── echo32.md │ ├── echo64.md │ ├── fortran32.md │ ├── fortran64.md │ ├── index.md │ ├── kernel32.md │ ├── kernel64.md │ ├── labview32.md │ └── labview64.md ├── faq │ ├── freeze.md │ ├── mock.md │ └── streams.md ├── index.md ├── install.md ├── license.md ├── release-notes.md └── usage │ ├── direct │ ├── activex.md │ ├── com.md │ ├── cpp.md │ ├── dotnet.md │ ├── fortran.md │ ├── index.md │ ├── java.md │ ├── labview.md │ └── stdcall.md │ ├── ipc │ ├── cpp.md │ ├── dotnet.md │ ├── echo.md │ ├── fortran.md │ ├── index.md │ ├── labview.md │ └── stdcall.md │ ├── overview.md │ └── refreeze.md ├── hatch_build.py ├── mkdocs.yml ├── pyproject.toml ├── src └── msl │ ├── examples │ └── loadlib │ │ ├── Trig.class │ │ ├── Trig.java │ │ ├── __init__.py │ │ ├── cpp32.py │ │ ├── cpp64.py │ │ ├── cpp_lib.cpp │ │ ├── cpp_lib.h │ │ ├── cpp_lib32.dll │ │ ├── cpp_lib32.so │ │ ├── cpp_lib64.dll │ │ ├── cpp_lib64.dylib │ │ ├── cpp_lib64.so │ │ ├── cpp_libarm64.dylib │ │ ├── dotnet32.py │ │ ├── dotnet64.py │ │ ├── dotnet_lib.cs │ │ ├── dotnet_lib32.dll │ │ ├── dotnet_lib64.dll │ │ ├── echo32.py │ │ ├── echo64.py │ │ ├── fortran32.py │ │ ├── fortran64.py │ │ ├── fortran_lib.f90 │ │ ├── fortran_lib32.dll │ │ ├── fortran_lib32.so │ │ ├── fortran_lib64.dll │ │ ├── fortran_lib64.dylib │ │ ├── fortran_lib64.so │ │ ├── fortran_libarm64.dylib │ │ ├── java_lib.jar │ │ ├── kernel32.py │ │ ├── kernel64.py │ │ ├── labview32.py │ │ ├── labview64.py │ │ ├── labview_lib.h │ │ ├── labview_lib32.dll │ │ ├── labview_lib64.dll │ │ └── nz │ │ └── msl │ │ └── examples │ │ ├── MathUtils.java │ │ ├── Matrix.java │ │ └── readme.txt │ └── loadlib │ ├── Py4JWrapper.java │ ├── __about__.py │ ├── __init__.py │ ├── _constants.py │ ├── _types.py │ ├── activex.py │ ├── client64.py │ ├── exceptions.py │ ├── freeze_server32.py │ ├── load_library.py │ ├── py.typed │ ├── py4j-wrapper.jar │ ├── server32-linux │ ├── server32-linux.config │ ├── server32-windows.exe │ ├── server32-windows.exe.config │ ├── server32.py │ ├── start_server32.py │ └── utils.py └── tests ├── bad_servers ├── bad_init_args.py ├── bad_init_args2.py ├── bad_lib_path.py ├── bad_lib_type.py ├── bad_super_init.py ├── import_error.py ├── no_init.py ├── no_server32_subclass.py ├── no_super.py ├── unexpected_error.py ├── unexpected_error2.py ├── unexpected_error3.py └── wrong_bitness.py ├── check_pythonw.py ├── check_server32_imports.py ├── dotnet_config ├── invalid_xml.exe ├── invalid_xml.exe.config ├── legacy_v2_runtime.py ├── legacy_v2_runtime │ ├── AssemblyInfo.cpp │ ├── legacy_v2_runtime.cpp │ ├── legacy_v2_runtime.sln │ ├── legacy_v2_runtime.vcproj │ ├── legacy_v2_runtime_x64.dll │ └── legacy_v2_runtime_x86.dll ├── no_config_exists.exe ├── root_tag_is_not_configuration.exe ├── root_tag_is_not_configuration.exe.config ├── set_to_false.exe ├── set_to_false.exe.config └── startup_element_does_not_exist.exe ├── namespace_with_dots ├── Namespace.With.Dots.cs ├── Namespace.With.Dots.csproj └── Namespace.With.Dots.dll ├── nested_namespaces ├── nested_namespaces.cs ├── nested_namespaces.csproj └── nested_namespaces.dll ├── server32_comtypes ├── activex_media_player.py ├── ctypes_union_error.py └── shell32.py ├── sew_eurodrive ├── FirstDll.dll ├── SecondDll.dll └── sew32.py ├── test_application.py ├── test_client64.py ├── test_context_manager.py ├── test_dotnet_legacy_v2_runtime.py ├── test_loadlib.py ├── test_mock.py ├── test_server32.py ├── test_server32_argparse.py ├── test_server32_bad_modules.py ├── test_server32_examples_dir.py ├── test_server32_is_interpreter.py ├── test_server32_issue24a.py ├── test_server32_issue24b.py ├── test_server32_property.py ├── test_server32_protocol.py ├── test_server32_remove_site_packages.py ├── test_server32_rpc_timeout.py ├── test_server32_sew_eurodrive.py ├── test_server32_shutdown.py ├── test_server32_stdout_stderr.py ├── test_utils.py └── uñicödé ├── Namespace.With.Dots-uñicödé.dll ├── Trig.class ├── cpp32unicode.py ├── cpp_lib32-uñicödé.dll ├── cpp_lib32-uñicödé.so ├── cpp_lib64-uñicödé.dll ├── cpp_lib64-uñicödé.dylib ├── cpp_lib64-uñicödé.so └── cpp_libarm64-uñicödé.dylib /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request] 4 | 5 | env: 6 | PY_COLORS: 1 7 | COLUMNS: 110 8 | PIP_DISABLE_PIP_VERSION_CHECK: 1 9 | 10 | jobs: 11 | test: 12 | name: Test 13 | runs-on: ${{ matrix.os }} 14 | timeout-minutes: 10 15 | strategy: 16 | fail-fast: false 17 | matrix: 18 | python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] 19 | os: [windows-latest, ubuntu-22.04, macos-latest, macos-13] 20 | architecture: [x64, x86] 21 | exclude: 22 | - os: ubuntu-22.04 23 | architecture: x86 24 | - os: macos-latest 25 | architecture: x86 26 | - os: macos-13 27 | architecture: x86 28 | steps: 29 | - name: Checkout repository 30 | uses: actions/checkout@v4 31 | - name: Install system dependencies (Ubuntu) 32 | if: ${{ matrix.os == 'ubuntu-22.04' }} 33 | run: | 34 | sudo dpkg --add-architecture i386 35 | sudo apt-get update 36 | sudo apt-get install -y libgfortran5:i386 37 | - name: Mono version (non Windows) 38 | if: ${{ matrix.os != 'windows-latest' }} 39 | run: mono --version 40 | - name: Java version 41 | run: java -version 42 | - name: Set up Python ${{ matrix.python-version }} (Windows) 43 | if: ${{ matrix.os == 'windows-latest' }} 44 | uses: actions/setup-python@v5 45 | with: 46 | python-version: ${{ matrix.python-version }} 47 | architecture: ${{ matrix.architecture }} 48 | - name: Set up Python ${{ matrix.python-version }} (non Windows) 49 | if: ${{ matrix.os != 'windows-latest' }} 50 | uses: actions/setup-python@v5 51 | with: 52 | python-version: ${{ matrix.python-version }} 53 | - name: Upgrade pip 54 | run: python -m pip install --upgrade pip 55 | - name: Install test dependencies 56 | run: python -m pip install "comtypes;sys_platform=='win32'" py4j pythonnet numpy pytest pytest-cov 57 | - name: Build custom wheels 58 | run: | 59 | python -m pip install --upgrade hatch 60 | hatch build --target custom 61 | - name: Install package from custom-built wheel 62 | run: python -m pip install --find-links dist --no-index msl-loadlib 63 | - name: Run tests 64 | run: python -m pytest 65 | 66 | lint: 67 | name: Lint 68 | runs-on: windows-latest 69 | env: 70 | MYPYPATH: src 71 | steps: 72 | - name: Checkout repository 73 | uses: actions/checkout@v4 74 | - name: Setup uv 75 | uses: astral-sh/setup-uv@v5 76 | with: 77 | enable-cache: false 78 | python-version: '3.13' 79 | - name: Spelling 80 | uses: streetsidesoftware/cspell-action@v6 81 | with: 82 | incremental_files_only: false 83 | - name: Linting 84 | run: uv run -- ruff check --force-exclude --no-fix . 85 | - name: Formatting 86 | run: uv run -- ruff format --force-exclude --check . 87 | - name: Typing:BasedPyright 88 | run: uv run -- basedpyright . 89 | - name: Typing:MyPy 90 | run: uv run -- mypy . 91 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: Docs 2 | 3 | on: 4 | push: 5 | branches: ['main'] 6 | tags: 7 | - '*' 8 | pull_request: 9 | branches: ['main'] 10 | 11 | permissions: 12 | contents: write 13 | pages: write 14 | id-token: write 15 | 16 | # Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. 17 | # However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. 18 | concurrency: 19 | group: 'pages' 20 | cancel-in-progress: false 21 | 22 | jobs: 23 | deploy: 24 | name: Build and deploy documentation 25 | runs-on: ubuntu-latest 26 | steps: 27 | - name: Checkout repository 28 | uses: actions/checkout@v4 29 | with: 30 | fetch-depth: 0 # fetch all commits/branches 31 | - name: Install uv 32 | uses: astral-sh/setup-uv@v5 33 | with: 34 | enable-cache: false 35 | - name: Configure Git for GitHub Actions bot 36 | run: | 37 | git config --local user.name "github-actions[bot]" 38 | git config --local user.email "github-actions[bot]@users.noreply.github.com" 39 | - name: Validate docs 40 | run: uv run -- mkdocs build --strict 41 | - name: Deploy dev docs 42 | if: github.ref == 'refs/heads/main' 43 | run: | 44 | uv run -- mike deploy --push --update-aliases dev 45 | - name: Deploy release docs 46 | if: github.ref_type == 'tag' 47 | run: | 48 | uv run -- mike deploy --push --update-aliases ${{ github.ref_name }} latest 49 | uv run -- mike set-default --push latest 50 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Global directories 2 | __pycache__/ 3 | 4 | # Global files 5 | *.py[codz] 6 | 7 | # Root directories 8 | /.cache/ 9 | /.idea/ 10 | /.vscode/ 11 | /.venv/ 12 | /dist/ 13 | /site/ 14 | /venv/ 15 | 16 | # Eclipse project files for Py4JWrapper 17 | py4jwrapper/bin/ 18 | py4jwrapper/.classpath 19 | 20 | # Root files 21 | /uv.lock 22 | 23 | # Auto-generated during build 24 | /src/msl/loadlib/_version.py 25 | 26 | # Auto-generated during tests 27 | /tests/check_pythonw.txt 28 | /a_new_file.txt 29 | 30 | # Auto-generated when freezing the server 31 | file_version_info.txt 32 | server32.spec 33 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 - 2025, Measurement Standards Laboratory of New Zealand 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MSL-LoadLib 2 | 3 | [![CI Status](https://github.com/MSLNZ/msl-loadlib/actions/workflows/ci.yml/badge.svg)](https://github.com/MSLNZ/msl-loadlib/actions/workflows/ci.yml) 4 | [![Docs Status](https://github.com/MSLNZ/msl-loadlib/actions/workflows/docs.yml/badge.svg)](https://github.com/MSLNZ/msl-loadlib/actions/workflows/docs.yml) 5 | [![PyPI - Version](https://img.shields.io/pypi/v/msl-loadlib?logo=pypi&logoColor=gold&label=PyPI&color=blue)](https://pypi.org/project/msl-loadlib/) 6 | 7 | This package loads a library in Python. It is basically just a thin wrapper around [ctypes] (for libraries that use the `__cdecl` or `__stdcall` calling convention), [Python.NET] (for libraries that use Microsoft .NET, `CLR`), [Py4J] (for Java `.jar` or `.class` files) and [comtypes] (for libraries that use the [Component Object Model] or [ActiveX]). 8 | 9 | However, the primary advantage is that it is possible to communicate with a 32-bit library from 64-bit Python. 10 | 11 | `msl-loadlib` is a pure-python package, but [Python.NET] depends on the .NET Common Language Runtime (CLR) on Windows and Mono Runtime on Linux/macOS, and [Py4J] depends on having a [Java Virtual Machine] installed. 12 | 13 | ## Install 14 | `msl-loadlib` is available for installation via the [Python Package Index](https://pypi.org/project/msl-loadlib/) 15 | 16 | ```console 17 | pip install msl-loadlib 18 | ``` 19 | 20 | Optional dependencies: 21 | 22 | * [Python.NET] 23 | * [Py4J] 24 | * [comtypes] 25 | 26 | 27 | 28 | To set up your environment on Linux, please follow the instructions on the [prerequisites](https://mslnz.github.io/msl-loadlib/latest/install/#linux) section of the documentation. 29 | 30 | ## Examples 31 | If you are loading a 64-bit library in 64-bit Python (or a 32-bit library in 32-bit Python), then you can directly load the library using `LoadLibrary`. 32 | 33 | *The following examples load a 64-bit library in a 64-bit Python interpreter. If you are using a 32-bit Python interpreter replace `64` with `32` in the filename.* 34 | 35 | Import the `LoadLibrary` class and the directory where the example libraries are located 36 | 37 | 41 | 42 | ```pycon 43 | >>> from msl.loadlib import LoadLibrary 44 | >>> from msl.examples.loadlib import EXAMPLES_DIR 45 | 46 | ``` 47 | 48 | If the file extension is not included then a default extension, `.dll` (Windows), `.so` (Linux) or `.dylib` (macOS), is used. 49 | 50 | Load the [example C++](https://github.com/MSLNZ/msl-loadlib/blob/main/src/msl/examples/loadlib/cpp_lib.cpp) library and call the `add` function 51 | 52 | ```pycon 53 | >>> cpp = LoadLibrary(EXAMPLES_DIR / "cpp_lib64") 54 | >>> cpp.lib.add(1, 2) 55 | 3 56 | 57 | ``` 58 | 59 | Load the [example FORTRAN](https://github.com/MSLNZ/msl-loadlib/blob/main/src/msl/examples/loadlib/fortran_lib.f90) library and call the `factorial` function 60 | 61 | ```pycon 62 | >>> fortran = LoadLibrary(EXAMPLES_DIR / "fortran_lib64") 63 | 64 | ``` 65 | 66 | With a FORTRAN library you must pass values by reference using [ctypes], and, since the returned value is not of type `c_int` we must configure [ctypes] for a value of type `c_double` to be returned 67 | 68 | ```pycon 69 | >>> from ctypes import byref, c_int, c_double 70 | >>> fortran.lib.factorial.restype = c_double 71 | >>> fortran.lib.factorial(byref(c_int(37))) 72 | 1.3763753091226343e+43 73 | 74 | ``` 75 | 76 | Load the [example Java](https://github.com/MSLNZ/msl-loadlib/blob/main/src/msl/examples/loadlib/Trig.java) byte code and call the `cos` function 77 | 78 | ```pycon 79 | >>> java = LoadLibrary(EXAMPLES_DIR / "Trig.class") 80 | >>> java.lib.Trig.cos(1.234) 81 | 0.33046510807172985 82 | 83 | ``` 84 | 85 | Python interacts with the [Java Virtual Machine] via a local network socket and therefore the connection must be closed when you are done using the Java library 86 | 87 | ```pycon 88 | >>> java.gateway.shutdown() 89 | 90 | ``` 91 | 92 | Load the [example .NET](https://github.com/MSLNZ/msl-loadlib/blob/main/src/msl/examples/loadlib/dotnet_lib.cs) library and call the `reverse_string` function, we must specify that the library type is a .NET library by including the `"net"` argument 93 | 94 | 98 | 99 | ```pycon 100 | >>> net = LoadLibrary(EXAMPLES_DIR / "dotnet_lib64.dll", "net") 101 | >>> net.lib.StringManipulation().reverse_string("abcdefghijklmnopqrstuvwxyz") 102 | 'zyxwvutsrqponmlkjihgfedcba' 103 | 104 | ``` 105 | 106 | 111 | 112 | To load a [Component Object Model] (COM) library pass in the library's Program ID. *NOTE: This example will only work on Windows.* 113 | 114 | Here we load the [FileSystemObject](https://learn.microsoft.com/en-us/office/vba/language/reference/user-interface-help/filesystemobject-object) library and include the `"com"` argument to indicate that it is a COM library. 115 | 116 | 120 | 121 | ```pycon 122 | >>> com = LoadLibrary("Scripting.FileSystemObject", "com") 123 | 124 | ``` 125 | 126 | We then use the library to create, edit and close a text file 127 | 128 | ```pycon 129 | >>> f = com.lib.CreateTextFile("a_new_file.txt") 130 | >>> f.WriteLine("This is a test") 131 | 0 132 | >>> f.Close() 133 | 0 134 | 135 | ``` 136 | 137 | 142 | 143 | [Inter-process communication] is used to access a 32-bit library from a module that is running within a 64-bit Python interpreter. The procedure uses a client-server protocol where the client is a subclass of ``msl.loadlib.Client64`` and the server is a subclass of ``msl.loadlib.Server32``. See the [examples](https://mslnz.github.io/msl-loadlib/latest/examples) for examples on how to implement [Inter-process communication]. 144 | 145 | ## Documentation 146 | The documentation for `msl-loadlib` can be found [here](https://mslnz.github.io/msl-loadlib/latest/). 147 | 148 | [ctypes]: https://docs.python.org/3/library/ctypes.html 149 | [Python.NET]: https://pythonnet.github.io/ 150 | [Py4J]: https://www.py4j.org/ 151 | [Inter-process communication]: https://en.wikipedia.org/wiki/Inter-process_communication 152 | [Java Virtual Machine]: https://en.wikipedia.org/wiki/Java_virtual_machine 153 | [comtypes]: https://comtypes.readthedocs.io/en/stable/index.html 154 | [Component Object Model]: https://learn.microsoft.com/en-us/windows/win32/com/component-object-model--com--portal 155 | [ActiveX]: https://learn.microsoft.com/en-us/windows/win32/com/activex-controls 156 | -------------------------------------------------------------------------------- /conftest.py: -------------------------------------------------------------------------------- 1 | """Configuration file for pytest.""" 2 | 3 | from __future__ import annotations 4 | 5 | import os 6 | import platform 7 | import sys 8 | from pathlib import Path 9 | from typing import Callable 10 | 11 | import pytest 12 | 13 | try: 14 | import clr # type: ignore[import-untyped] # pyright: ignore[reportMissingTypeStubs] 15 | except (ImportError, RuntimeError): 16 | clr = None 17 | 18 | from msl.loadlib import IS_PYTHON_64BIT 19 | 20 | IS_WINDOWS: bool = sys.platform == "win32" 21 | IS_MAC: bool = sys.platform == "darwin" 22 | IS_MAC_ARM64 = IS_MAC and platform.machine() == "arm64" 23 | 24 | 25 | def has_labview_runtime(*, x86: bool) -> bool: 26 | """Check if a LabVIEW Run-Time Engine >= 2017 is installed.""" 27 | root = Path("C:/Program Files (x86)") if x86 else Path("C:/Program Files") 28 | root = root / "National Instruments" / "Shared" / "LabVIEW Run-Time" 29 | if not root.is_dir(): 30 | return False 31 | # cSpell: ignore lvrt 32 | return any(int(folder.name) >= MIN_LABVIEW_RUNTIME and (folder / "lvrt.dll").is_file() for folder in root.iterdir()) 33 | 34 | 35 | MIN_LABVIEW_RUNTIME = 2017 36 | HAS_32BIT_LABVIEW_RUNTIME = has_labview_runtime(x86=True) 37 | HAS_64BIT_LABVIEW_RUNTIME = has_labview_runtime(x86=False) 38 | 39 | 40 | def not_windows() -> None: 41 | """Skip doctest if not Windows.""" 42 | if not IS_WINDOWS: 43 | pytest.skip("not Windows") 44 | 45 | 46 | def is_mac() -> None: 47 | """Skip doctest if macOS.""" 48 | if IS_MAC: 49 | pytest.skip("is macOS") 50 | 51 | 52 | def bit64() -> None: 53 | """Skip doctest if 64-bit Python.""" 54 | if IS_PYTHON_64BIT: 55 | pytest.skip("requires 32-bit Python") 56 | 57 | 58 | def bit32() -> None: 59 | """Skip doctest if 32-bit Python.""" 60 | if not IS_PYTHON_64BIT: 61 | pytest.skip("requires 64-bit Python") 62 | 63 | 64 | def readme_all() -> None: 65 | """Skip all doctest in README.""" 66 | if not IS_PYTHON_64BIT or IS_MAC_ARM64: 67 | pytest.skip("skipped all tests") 68 | 69 | 70 | def readme_dotnet() -> None: 71 | """Skip from .NET doctest in README.""" 72 | if clr is None: 73 | pytest.skip("skipped at .NET test") 74 | 75 | 76 | def readme_com() -> None: 77 | """Skip from COM doctest in README.""" 78 | if not IS_WINDOWS: 79 | pytest.skip("skipped at COM test") 80 | 81 | 82 | def mac_arm64() -> None: 83 | """Skip doctest if macOS and ARM64.""" 84 | if IS_MAC_ARM64: 85 | pytest.skip("ignore on macOS arm64") 86 | 87 | 88 | def no_labview64() -> None: 89 | """Skip doctest if an appropriate 64-bit LabVIEW Run-Time Engine is not installed.""" 90 | if not HAS_64BIT_LABVIEW_RUNTIME: 91 | pytest.skip("requires 64-bit LabVIEW Run-Time Engine") 92 | 93 | 94 | def no_labview32() -> None: 95 | """Skip doctest if an appropriate 32-bit LabVIEW Run-Time Engine is not installed.""" 96 | if not HAS_32BIT_LABVIEW_RUNTIME: 97 | pytest.skip("requires 32-bit LabVIEW Run-Time Engine") 98 | 99 | 100 | def no_pythonnet() -> None: 101 | """Skip doctest if pythonnet is not installed.""" 102 | if clr is None: 103 | pytest.skip("pythonnet is not installed") 104 | 105 | 106 | def win32_github_actions() -> None: 107 | """Skip doctest if using a Windows running on GitHub Actions.""" 108 | if IS_WINDOWS and os.getenv("GITHUB_ACTIONS") == "true": 109 | pytest.skip("flaky test on Windows and GHA") 110 | 111 | 112 | @pytest.fixture(autouse=True) 113 | def doctest_skipif(doctest_namespace: dict[str, Callable[[], None]]) -> None: 114 | """Inject skipif conditions for doctest.""" 115 | doctest_namespace.update( 116 | { 117 | "SKIP_IF_32BIT": bit32, 118 | "SKIP_IF_64BIT": bit64, 119 | "SKIP_IF_MACOS": is_mac, 120 | "SKIP_IF_MACOS_ARM64": mac_arm64, 121 | "SKIP_IF_NO_LABVIEW32": no_labview32, 122 | "SKIP_IF_NO_LABVIEW64": no_labview64, 123 | "SKIP_IF_NO_PYTHONNET": no_pythonnet, 124 | "SKIP_IF_NOT_WINDOWS": not_windows, 125 | "SKIP_IF_WINDOWS_GITHUB_ACTIONS": win32_github_actions, 126 | "SKIP_README_ALL": readme_all, 127 | "SKIP_README_COM": readme_com, 128 | "SKIP_README_DOTNET": readme_dotnet, 129 | } 130 | ) 131 | 132 | 133 | skipif_no_comtypes = pytest.mark.skipif(not IS_WINDOWS, reason="comtypes is only supported on Windows") 134 | skipif_no_pythonnet = pytest.mark.skipif(clr is None, reason="pythonnet is not installed") 135 | skipif_no_server32 = pytest.mark.skipif(IS_MAC, reason="32-bit server does not exist") 136 | skipif_not_windows = pytest.mark.skipif(not IS_WINDOWS, reason="not Windows") 137 | 138 | xfail_windows_ga = pytest.mark.xfail( 139 | IS_WINDOWS and os.getenv("GITHUB_ACTIONS") == "true", reason="flaky test on Windows and GHA" 140 | ) 141 | -------------------------------------------------------------------------------- /cspell.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/streetsidesoftware/cspell/main/cspell.schema.json", 3 | "version": "0.2", 4 | "language": "en-GB", 5 | "languageId": "python", 6 | "useGitignore": true, 7 | "dictionaries": [ 8 | "!aws", 9 | "!companies", 10 | "!cryptocurrencies", 11 | "!web-services" 12 | ], 13 | "cache": { 14 | "cacheFormat": "universal", 15 | "cacheLocation": ".cache/.cspellcache", 16 | "cacheStrategy": "content", 17 | "useCache": true 18 | }, 19 | "ignorePaths": [ 20 | "cspell.json", 21 | "docs/conf.py", 22 | "mkdocs.yml", 23 | "pyproject.toml", 24 | "tests/dotnet_config/legacy_v2_runtime/legacy_v2_runtime.vcproj", 25 | "tests/namespace_with_dots/Namespace.With.Dots.csproj", 26 | "tests/nested_namespaces/nested_namespaces.csproj" 27 | ], 28 | "words": [ 29 | "ACCEPTFILES", 30 | "activex", 31 | "APPLMODAL", 32 | "APPWINDOW", 33 | "arange", 34 | "argtypes", 35 | "ASYNCWINDOWPOS", 36 | "autoclass", 37 | "autointenum", 38 | "automodule", 39 | "autosummary", 40 | "autouse", 41 | "besselj", 42 | "bitness", 43 | "bltin", 44 | "Borbely", 45 | "Bstatic", 46 | "builtin", 47 | "byref", 48 | "bysource", 49 | "BYTEALIGNCLIENT", 50 | "BYTEALIGNWINDOW", 51 | "CANCELTRYCONTINUE", 52 | "caplog", 53 | "cdecl", 54 | "CDLL", 55 | "centered", 56 | "CHILDWINDOW", 57 | "CLASSDC", 58 | "cleanup", 59 | "CLIENTEDGE", 60 | "CLIPCHILDREN", 61 | "CLIPSIBLINGS", 62 | "CLSID", 63 | "coinit", 64 | "comtypes", 65 | "conn", 66 | "CONTEXTHELP", 67 | "CONTROLPARENT", 68 | "coreclr", 69 | "cplusplus", 70 | "creationflags", 71 | "ctypes", 72 | "DBLCLKS", 73 | "declspec", 74 | "DEFBUTTON", 75 | "DEFERERASE", 76 | "directivetype", 77 | "distpath", 78 | "DLGFRAME", 79 | "DLGMODALFRAME", 80 | "dllexport", 81 | "dlrow", 82 | "dnalae", 83 | "docstrings", 84 | "doctest", 85 | "dotpeek", 86 | "dpkg", 87 | "DRAWFRAME", 88 | "DROPSHADOW", 89 | "DUMPBIN", 90 | "dylib", 91 | "enddo", 92 | "eurodrive", 93 | "exitmsg", 94 | "extcode", 95 | "extern", 96 | "fileno", 97 | "filevers", 98 | "FORCEMINIMIZE", 99 | "FRAMECHANGED", 100 | "fspath", 101 | "getattr", 102 | "getdefaulttimeout", 103 | "getpid", 104 | "getsitepackages", 105 | "gfortran", 106 | "GLOBALCLASS", 107 | "GNUC", 108 | "GRAYED", 109 | "HIDEWINDOW", 110 | "HREDRAW", 111 | "HRESULT", 112 | "ICONASTERISK", 113 | "ICONERROR", 114 | "ICONEXCLAMATION", 115 | "ICONHAND", 116 | "ICONINFORMATION", 117 | "ICONQUESTION", 118 | "ICONSTOP", 119 | "ICONWARNING", 120 | "Inproc", 121 | "isattr", 122 | "isinstance", 123 | "javac", 124 | "karna", 125 | "Klass", 126 | "kwargs", 127 | "labview", 128 | "LAYOUTRTL", 129 | "LEFTSCROLLBAR", 130 | "levelname", 131 | "libarm", 132 | "libgfortran", 133 | "libid", 134 | "libstdc", 135 | "libtype", 136 | "LIBTYPES", 137 | "Linq", 138 | "literalinclude", 139 | "loadlib", 140 | "lpfn", 141 | "lpsz", 142 | "LTRREADING", 143 | "LVDLL", 144 | "manylinux", 145 | "MATMUL", 146 | "MAXIMIZEBOX", 147 | "maxsize", 148 | "MDICHILD", 149 | "MEIPASS", 150 | "membername", 151 | "MENUBARBREAK", 152 | "MENUBREAK", 153 | "MINIMIZEBOX", 154 | "modname", 155 | "musllinux", 156 | "mypackage", 157 | "namedtuple", 158 | "ndarray", 159 | "NOACTIVATE", 160 | "NOCLOSE", 161 | "noconfirm", 162 | "NOCOPYBITS", 163 | "NOINHERITLAYOUT", 164 | "NOMOVE", 165 | "NONINFRINGEMENT", 166 | "NOOWNERZORDER", 167 | "NOPARENTNOTIFY", 168 | "NOREDIRECTIONBITMAP", 169 | "NOREDRAW", 170 | "NOREPOSITION", 171 | "NOSENDCHANGING", 172 | "NOSIZE", 173 | "NOZORDER", 174 | "numpy", 175 | "objtype", 176 | "olleh", 177 | "onefile", 178 | "OVERLAPPEDWINDOW", 179 | "OWNDC", 180 | "OWNERDRAW", 181 | "PALETTEWINDOW", 182 | "PARENTDC", 183 | "pickleable", 184 | "popups", 185 | "POPUPWINDOW", 186 | "Proc", 187 | "prodvers", 188 | "pycon", 189 | "pyinstaller", 190 | "pypi", 191 | "pytest", 192 | "PYTHONNET", 193 | "pythonw", 194 | "RDWR", 195 | "recwarn", 196 | "regrtest", 197 | "releaselevel", 198 | "repr", 199 | "restype", 200 | "returncode", 201 | "rightarrow", 202 | "RIGHTSCROLLBAR", 203 | "RTLREADING", 204 | "SAVEBITS", 205 | "SETFOREGROUND", 206 | "shdocvw", 207 | "SHOWDEFAULT", 208 | "SHOWMAXIMIZED", 209 | "SHOWMINIMIZED", 210 | "SHOWMINNOACTIVE", 211 | "SHOWNA", 212 | "SHOWNOACTIVATE", 213 | "SHOWNORMAL", 214 | "SHOWWINDOW", 215 | "SIZEBOX", 216 | "specpath", 217 | "Spyder", 218 | "STATICEDGE", 219 | "stdev", 220 | "sublicense", 221 | "SYSMENU", 222 | "SYSTEMMODAL", 223 | "SYSTEMTIME", 224 | "TABSTOP", 225 | "tanh", 226 | "TASKMODAL", 227 | "teardown", 228 | "THICKFRAME", 229 | "TILEDWINDOW", 230 | "titlebar", 231 | "tobytes", 232 | "toctree", 233 | "tolist", 234 | "TOOLWINDOW", 235 | "undoc", 236 | "uñicödé", 237 | "unpickled", 238 | "USEDEFAULT", 239 | "vcproj", 240 | "virtualenv", 241 | "VREDRAW", 242 | "WINDOWEDGE", 243 | "WINFUNCTYPE", 244 | "WNDCLASSEXW", 245 | "WNDPROC", 246 | "workpath", 247 | "xout", 248 | "ytniatrecnu", 249 | "zyxwvutsrqponmlkjihgfedcba" 250 | ] 251 | } 252 | -------------------------------------------------------------------------------- /docs/api/activex.md: -------------------------------------------------------------------------------- 1 | # activex 2 | 3 | ::: msl.loadlib.activex 4 | options: 5 | filters: ["!^(_|WNDCLASSEXW)"] 6 | -------------------------------------------------------------------------------- /docs/api/client64.md: -------------------------------------------------------------------------------- 1 | # Client64 2 | 3 | ::: msl.loadlib.client64 4 | options: 5 | filters: ["!(^_|MockClient|HTTPClient)"] 6 | -------------------------------------------------------------------------------- /docs/api/exceptions.md: -------------------------------------------------------------------------------- 1 | # exceptions 2 | 3 | ::: msl.loadlib.exceptions 4 | -------------------------------------------------------------------------------- /docs/api/freeze_server32.md: -------------------------------------------------------------------------------- 1 | # freeze_server32 2 | 3 | ::: msl.loadlib.freeze_server32 4 | -------------------------------------------------------------------------------- /docs/api/load_library.md: -------------------------------------------------------------------------------- 1 | # LoadLibrary 2 | 3 | ::: msl.loadlib.load_library 4 | options: 5 | filters: ["!(^_|DotNet)"] 6 | -------------------------------------------------------------------------------- /docs/api/server32.md: -------------------------------------------------------------------------------- 1 | # Server32 2 | 3 | ::: msl.loadlib.server32 4 | -------------------------------------------------------------------------------- /docs/api/types.md: -------------------------------------------------------------------------------- 1 | # types 2 | 3 | ::: msl.loadlib._types 4 | -------------------------------------------------------------------------------- /docs/api/utils.md: -------------------------------------------------------------------------------- 1 | # utils 2 | 3 | ::: msl.loadlib.utils 4 | -------------------------------------------------------------------------------- /docs/assets/images/dotpeek_lib.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MSLNZ/msl-loadlib/c3f9e7e6f4d0c739e6af608bbbe865504030f503/docs/assets/images/dotpeek_lib.png -------------------------------------------------------------------------------- /docs/assets/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MSLNZ/msl-loadlib/c3f9e7e6f4d0c739e6af608bbbe865504030f503/docs/assets/images/favicon.ico -------------------------------------------------------------------------------- /docs/assets/images/labview_lib.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MSLNZ/msl-loadlib/c3f9e7e6f4d0c739e6af608bbbe865504030f503/docs/assets/images/labview_lib.png -------------------------------------------------------------------------------- /docs/examples/cpp32.md: -------------------------------------------------------------------------------- 1 | # Cpp32 2 | 3 | ::: msl.examples.loadlib.cpp32 4 | -------------------------------------------------------------------------------- /docs/examples/cpp64.md: -------------------------------------------------------------------------------- 1 | # Cpp64 2 | 3 | ::: msl.examples.loadlib.cpp64 4 | -------------------------------------------------------------------------------- /docs/examples/dotnet32.md: -------------------------------------------------------------------------------- 1 | # DotNet32 2 | 3 | ::: msl.examples.loadlib.dotnet32 4 | -------------------------------------------------------------------------------- /docs/examples/dotnet64.md: -------------------------------------------------------------------------------- 1 | # DotNet64 2 | 3 | ::: msl.examples.loadlib.dotnet64 4 | -------------------------------------------------------------------------------- /docs/examples/echo32.md: -------------------------------------------------------------------------------- 1 | # Echo32 2 | 3 | ::: msl.examples.loadlib.echo32 4 | -------------------------------------------------------------------------------- /docs/examples/echo64.md: -------------------------------------------------------------------------------- 1 | # Echo64 2 | 3 | ::: msl.examples.loadlib.echo64 4 | -------------------------------------------------------------------------------- /docs/examples/fortran32.md: -------------------------------------------------------------------------------- 1 | # Fortran32 2 | 3 | ::: msl.examples.loadlib.fortran32 4 | -------------------------------------------------------------------------------- /docs/examples/fortran64.md: -------------------------------------------------------------------------------- 1 | # Fortran64 2 | 3 | ::: msl.examples.loadlib.fortran64 4 | -------------------------------------------------------------------------------- /docs/examples/index.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | 3 | The following classes illustrate how the [Client-Server][] communication works. 4 | 5 | Classes that end in *32* contain a class that is a subclass of [Server32][]. This class is a wrapper around a 32-bit library and is hosted on a 32-bit server. Viewing the source code of this class may be useful to see how different data types (e.g., strings, numbers, arrays) are passed between Python and a function in the library. 6 | 7 | Classes that end in *64* contain a class that is a subclass of [Client64][]. This class sends a request to the corresponding [Server32][] subclass to communicate with the 32-bit library. 8 | 9 | * [Cpp32][] 10 | * [Cpp64][] 11 | * [DotNet32][] 12 | * [DotNet64][] 13 | * [Echo32][] 14 | * [Echo64][] 15 | * [Fortran32][] 16 | * [Fortran64][] 17 | * [Kernel32][] 18 | * [Kernel64][] 19 | * [Labview32][] 20 | * [Labview64][] 21 | -------------------------------------------------------------------------------- /docs/examples/kernel32.md: -------------------------------------------------------------------------------- 1 | # Kernel32 (Windows `__stdcall`) {: #Kernel32 } 2 | 3 | ::: msl.examples.loadlib.kernel32 4 | -------------------------------------------------------------------------------- /docs/examples/kernel64.md: -------------------------------------------------------------------------------- 1 | # Kernel64 (Windows `__stdcall`) {: #Kernel64 } 2 | 3 | ::: msl.examples.loadlib.kernel64 4 | -------------------------------------------------------------------------------- /docs/examples/labview32.md: -------------------------------------------------------------------------------- 1 | # Labview32 2 | 3 | ::: msl.examples.loadlib.labview32 4 | -------------------------------------------------------------------------------- /docs/examples/labview64.md: -------------------------------------------------------------------------------- 1 | # Labview64 2 | 3 | ::: msl.examples.loadlib.labview64 4 | -------------------------------------------------------------------------------- /docs/faq/freeze.md: -------------------------------------------------------------------------------- 1 | # Freezing the `msl-loadlib` package {: #faq-freeze } 2 | 3 | If you want to use [PyInstaller]{:target="_blank"} or [cx-Freeze]{:target="_blank"} to bundle `msl-loadlib` in a frozen application, the 32-bit server must be added as a data file. 4 | 5 | For example, using [PyInstaller]{:target="_blank"} on Windows you would include an ``--add-data`` option 6 | 7 | ```console 8 | pyinstaller --add-data "..\site-packages\msl\loadlib\server32-windows.exe:." 9 | ``` 10 | 11 | where you must replace the leading `..` prefix with the parent directories to the file (i.e., specify the absolute path to the file). On Linux, replace `server32-windows.exe:.` with `server32-linux:.` 12 | 13 | If the server is loading a .NET library that was compiled with .NET < 4.0, you must also add the `server32-windows.exe.config` data file. Otherwise, you do not need to add this config file. 14 | 15 | [cx-Freeze]{:target="_blank"} appears to automatically bundle the 32-bit server (tested with [cx-Freeze]{:target="_blank"} version 6.14.5) so there may not be anything you need to do. If the `server32` executable is not bundled, you can specify the absolute path to the `server32` executable as the `include_files` option for the `build_exe` command. 16 | 17 | You may also wish to [refreeze][] the 32-bit server and add your custom server to your application. 18 | 19 | [PyInstaller]: https://pyinstaller.org/en/stable/ 20 | [cx-Freeze]: https://cx-freeze.readthedocs.io/en/latest/index.html 21 | -------------------------------------------------------------------------------- /docs/faq/mock.md: -------------------------------------------------------------------------------- 1 | # Mocking the connection to the server {: #faq-mock } 2 | 3 | You may mock the connection to the server by passing in `host=None` when you instantiate [Client64][]. Also, the [Server32][] may need to decide which library to load. 4 | 5 | When the connection is mocked, both [Client64][] and [Server32][] instances will run in the same Python interpreter, therefore the server must load a library that is the same bitness as the Python interpreter that the client is running in. The [pickle][]{:target="_blank"} module is not used when the connection is mocked, so there is no overhead of using a file as a middle step to process requests and responses (which has a side effect that a mocked connection can return objects in a server's response that are not pickleable). 6 | 7 | One reason that you may want to mock the connection is that you wrote a lot of code that had to load a 32-bit library but now a 64-bit version of the library is available. You may also need to support the 32-bit and 64-bit libraries at the same time. Instead of making a relatively large change to your code, or managing different code bases, you can simply specify a keyword argument when instantiating your client class to decide whether to use the 32-bit library or the 64-bit library and the client class behaves exactly the same. 8 | 9 | Here is an example on how a client (running within 64-bit Python) can have a [Server32][] subclass load a 32-bit library or a 64-bit library. 10 | 11 | ```python 12 | from msl.loadlib import Client64, Server32 13 | 14 | class MockableServer(Server32): 15 | 16 | def __init__(self, host, port, **kwargs): 17 | # Decide which library to load on the server. 18 | # `host` is `None` when the connection is mocked. 19 | if host is None: 20 | path = "path/to/64bit/c/library.so" 21 | else: 22 | path = "path/to/32bit/c/library.so" 23 | super().__init__(path, "cdll", host, port) 24 | 25 | class MockableClient(Client64): 26 | 27 | def __init__(self, **kwargs): 28 | super().__init__(__file__, **kwargs) 29 | 30 | if __name__ == "__main__": 31 | client_uses_32bit_library = MockableClient() 32 | client_uses_64bit_library = MockableClient(host=None) 33 | ``` 34 | -------------------------------------------------------------------------------- /docs/faq/streams.md: -------------------------------------------------------------------------------- 1 | # Access `stdout` and `stderr` from the server {: #faq-streams } 2 | 3 | You have access to the console output, `stdout` and `stderr`, of the 32-bit server once it has shut down. For example, suppose your 64-bit client class is called `MyClient`, you could do something like 4 | 5 | ```python 6 | c = MyClient() 7 | c.do_something() 8 | c.do_something_else() 9 | stdout, stderr = c.shutdown_server32() 10 | print(stdout.read()) 11 | print(stderr.read()) 12 | ``` 13 | 14 | If you want to be able to poll the console output in real time (while the server is running) you have two options: 15 | 16 | 1. Have the server write to a file and the client read from the file. 17 | 18 | 2. Implement a `polling` method on the server for the client to send requests to. The following is a runnable example 19 | 20 | ```python 21 | import os 22 | 23 | from msl.loadlib import Client64, Server32 24 | 25 | class Polling32(Server32): 26 | 27 | def __init__(self, host, port): 28 | # Loading a "dummy" 32-bit library for this example 29 | path = os.path.join(Server32.examples_dir(), "cpp_lib32") 30 | super().__init__(path, "cdll", host, port) 31 | 32 | # Create a list to store 'print' messages in 33 | self._stdout = [] 34 | 35 | # Use your own print method instead of calling the 36 | # builtin print() function 37 | self.print("Polling32 has been initiated") 38 | 39 | def say_hello(self, name): 40 | """Return a greeting.""" 41 | self.print(f"say_hello was called with argument {name!r}") 42 | return f"Hello, {name}!" 43 | 44 | def poll(self): 45 | """Get the latest message.""" 46 | try: 47 | return self._stdout[-1] 48 | except IndexError: 49 | return "" 50 | 51 | def flush(self): 52 | """Get all messages and clear the cache.""" 53 | messages = "\n".join(self._stdout) 54 | self._stdout.clear() 55 | return messages 56 | 57 | def print(self, message): 58 | """Append a message.""" 59 | self._stdout.append(message) 60 | 61 | class Polling64(Client64): 62 | 63 | def __init__(self): 64 | super().__init__(__file__) 65 | 66 | def __getattr__(self, name): 67 | def send(*args, **kwargs): 68 | return self.request32(name, *args, **kwargs) 69 | return send 70 | 71 | # Only execute this section of code on the 64-bit client 72 | # (not on the 32-bit server). You may also prefer to write the 73 | # Server32 class and the Client64 class in separate files. 74 | if __name__ == "__main__": 75 | p = Polling64() 76 | print("poll ->", p.poll()) 77 | print("say_hello ->", p.say_hello("world")) 78 | print("poll ->", p.poll()) 79 | print("flush ->", repr(p.flush())) 80 | print("poll ->", repr(p.poll())) 81 | p.shutdown_server32() 82 | ``` 83 | 84 | Running the above script will output: 85 | 86 | ```console 87 | poll -> Polling32 has been initiated 88 | say_hello -> Hello, world! 89 | poll -> say_hello was called with argument 'world' 90 | flush -> "Polling32 has been initiated\nsay_hello was called with argument 'world'" 91 | poll -> '' 92 | ``` 93 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # Overview 2 | 3 | This package loads a library in Python. It is basically just a thin wrapper around [ctypes][]{:target="_blank"} (for libraries that use the `__cdecl` or `__stdcall` calling convention), [Python.NET]{:target="_blank"} (for libraries that use .NET, `CLR`), [Py4J]{:target="_blank"} (for Java `.jar` or `.class` files) and [comtypes]{:target="_blank"} (for libraries that use the [Component Object Model]{:target="_blank"} or [ActiveX]{:target="_blank"}). 4 | 5 | However, the primary advantage is that it is possible to communicate with a 32-bit library from 64-bit Python. For various reasons, mainly to do with the differences in pointer sizes, it is not possible to load a 32-bit library (e.g., `.dll`, `.so`, `.dylib` files) in a 64-bit process, and vice versa. This package contains a [Server32][] class that hosts a 32-bit library and a [Client64][] class that sends requests to the server to communicate with the 32-bit library as a form of [inter-process communication]{:target="_blank"}. 6 | 7 | [ActiveX]: https://learn.microsoft.com/en-us/windows/win32/com/activex-controls 8 | [Component Object Model]: https://learn.microsoft.com/en-us/windows/win32/com/component-object-model--com--portal 9 | [comtypes]: https://comtypes.readthedocs.io/en/stable/index.html 10 | [inter-process communication]: https://en.wikipedia.org/wiki/Inter-process_communication 11 | [Py4J]: https://www.py4j.org/ 12 | [Python.NET]: https://pythonnet.github.io/ 13 | -------------------------------------------------------------------------------- /docs/license.md: -------------------------------------------------------------------------------- 1 | # License 2 | 3 | ``` 4 | --8<-- "LICENSE.txt" 5 | ``` -------------------------------------------------------------------------------- /docs/release-notes.md: -------------------------------------------------------------------------------- 1 | --8<-- "CHANGELOG.md" 2 | -------------------------------------------------------------------------------- /docs/usage/direct/activex.md: -------------------------------------------------------------------------------- 1 | # ActiveX {: #direct-activex } 2 | 3 | The following runnable example shows how to create a main window that (could) contain [ActiveX]{:target="_blank"} controls and how to create a menubar in the application to handle callbacks. This example is only valid on Windows and requires [comtypes]{:target="_blank"} to be installed. 4 | 5 | ```python 6 | from __future__ import annotations 7 | 8 | import sys 9 | 10 | from msl.loadlib.activex import Application, Icon, MenuGroup, MenuItem 11 | 12 | 13 | def letter_clicked(item: MenuItem) -> None: 14 | """A callback function. You could interact with the `ocx` object.""" 15 | print(item, item.data) 16 | if item.text == "C": 17 | item.checked = not item.checked 18 | 19 | 20 | def group_clicked(item: MenuItem) -> None: 21 | """A callback function. You could interact with the `ocx` object.""" 22 | print(item) 23 | group.checked = item 24 | 25 | 26 | # Create an application window 27 | app = Application(title="My ActiveX Control", icon=Icon(sys.executable)) 28 | 29 | # Create a new 'Letters' menu 30 | letters = app.menu.create("Letters") 31 | 32 | # Append items to the 'Letters' menu 33 | app.menu.append(letters, "A") 34 | app.menu.append(letters, "B", callback=letter_clicked, data=1) 35 | app.menu.append_separator(letters) 36 | app.menu.append(letters, "C", callback=letter_clicked, data=[1, 2, 3]) 37 | 38 | # Create a new menu group 39 | group = MenuGroup() 40 | group.append("Group 1", callback=group_clicked) 41 | group.append("Group 2", callback=group_clicked) 42 | group.append("Group 3", callback=group_clicked) 43 | 44 | # Create a new 'Numbers' menu 45 | numbers = app.menu.create("Numbers") 46 | # Add the group to the 'Numbers' menu 47 | app.menu.append_group(numbers, group) 48 | # Add a separator then another item 49 | app.menu.append_separator(numbers) 50 | app.menu.append(numbers, "Not in Group") 51 | 52 | # Define the size of the main application window 53 | width = 300 54 | height = 300 55 | 56 | # Uncomment the next line to load your ActiveX control in the main window 57 | # ocx = app.load("My.OCX.Application", width=width, height=height) 58 | 59 | app.set_window_size(width, height) 60 | app.show() 61 | 62 | # Calling `app.run` is a blocking call. You may not want to call it yet 63 | # (or at all) and interact with the `ocx` object. It is shown here to keep 64 | # the window open until it is manually closed. 65 | app.run() 66 | ``` 67 | 68 | [ActiveX]: https://learn.microsoft.com/en-us/windows/win32/com/activex-controls 69 | [comtypes]: https://comtypes.readthedocs.io/en/stable/index.html 70 | -------------------------------------------------------------------------------- /docs/usage/direct/com.md: -------------------------------------------------------------------------------- 1 | # COM {: #direct-com } 2 | 3 | To load a [Component Object Model]{:target="_blank"} (COM) library you pass in the library's Program or Class ID. To view the COM libraries that are available on your computer you can use the [get_com_info][msl.loadlib.utils.get_com_info] function. 4 | 5 | !!! attention 6 | This example is only valid on Windows. 7 | 8 | Here we load the [FileSystemObject]{:target="_blank"} and include the `"com"` argument to indicate that it is a COM library 9 | 10 | 14 | 15 | ```pycon 16 | >>> from msl.loadlib import LoadLibrary 17 | >>> com = LoadLibrary("Scripting.FileSystemObject", "com") 18 | >>> com 19 | 20 | 21 | ``` 22 | 23 | We can then use the library to create, edit and close a text file by using the [CreateTextFile]{:target="_blank"} method 24 | 25 | ```pycon 26 | >>> fp = com.lib.CreateTextFile("a_new_file.txt") 27 | >>> fp.Write("This is a test.") 28 | 0 29 | >>> fp.Close() 30 | 0 31 | 32 | ``` 33 | 34 | Verify that the file exists and that the text is correct 35 | 36 | ```pycon 37 | >>> com.lib.FileExists("a_new_file.txt") 38 | True 39 | >>> file = com.lib.OpenTextFile("a_new_file.txt") 40 | >>> file.ReadAll() 41 | 'This is a test.' 42 | >>> file.Close() 43 | 0 44 | 45 | ``` 46 | 47 | 52 | 53 | [Component Object Model]: https://learn.microsoft.com/en-us/windows/win32/com/component-object-model--com--portal 54 | [FileSystemObject]: https://learn.microsoft.com/en-us/office/vba/language/reference/user-interface-help/filesystemobject-object 55 | [CreateTextFile]: https://learn.microsoft.com/en-us/office/vba/language/reference/user-interface-help/createtextfile-method 56 | -------------------------------------------------------------------------------- /docs/usage/direct/cpp.md: -------------------------------------------------------------------------------- 1 | # C++ {: #direct-cpp } 2 | 3 | Load the example 64-bit C++ library in 64-bit Python. *To load the 32-bit library in 32-bit Python use `"cpp_lib32"` as the filename.* 4 | 5 | !!! tip 6 | If the file extension is not specified, a default extension, `.dll` (Windows), `.so` (Linux) or `.dylib` (macOS) is used. 7 | 8 | ## Example 9 | 10 | Load the example [C++ library][cpp-lib] 11 | 12 | 16 | 17 | ```pycon 18 | >>> from msl.loadlib import LoadLibrary 19 | >>> from msl.examples.loadlib import EXAMPLES_DIR 20 | >>> cpp = LoadLibrary(EXAMPLES_DIR / "cpp_lib64") 21 | 22 | ``` 23 | 24 | By default, [ctypes][]{:target="_blank"} treats all input argument types and the return type of a library function to be a [c_int][ctypes.c_int]{:target="_blank"}. Therefore, the [argtypes][ctypes-specifying-required-argument-types]{:target="_blank"} and the [restype][ctypes-return-types]{:target="_blank"} should be defined for each function in the library. A few examples for the [C++ library][cpp-lib] are shown below 25 | 26 | ```pycon 27 | >>> from ctypes import c_char_p, c_float, c_int32 28 | >>> cpp.lib.subtract.argtypes = [c_float, c_float] 29 | >>> cpp.lib.subtract.restype = c_float 30 | >>> cpp.lib.reverse_string_v1.argtypes = [c_char_p, c_int32, c_char_p] 31 | >>> cpp.lib.reverse_string_v1.restype = None 32 | 33 | ``` 34 | 35 | Call the `add` function to calculate the sum of two integers 36 | 37 | ```pycon 38 | >>> cpp.lib.add(1, 2) 39 | 3 40 | 41 | ``` 42 | 43 | Call the `subtract` function to calculate the difference between two floats 44 | 45 | ```pycon 46 | >>> cpp.lib.subtract(7.1, 2.1) 47 | 5.0 48 | 49 | ``` 50 | 51 | Call the `reverse_string_v1` function to reverse the characters in a byte string. Python manages the memory of the reversed sting by creating a string buffer of the necessary length 52 | 53 | ```pycon 54 | >>> from ctypes import create_string_buffer 55 | >>> original = b"olleh" 56 | >>> reverse = create_string_buffer(len(original)) 57 | >>> cpp.lib.reverse_string_v1(original, len(original), reverse) 58 | >>> reverse.raw.decode() 59 | 'hello' 60 | 61 | ``` 62 | 63 | ## C++ Source Code {: #cpp-lib } 64 | 65 | ??? example "cpp_lib" 66 | 67 | === "cpp_lib.cpp" 68 | ```cpp 69 | --8<-- "src/msl/examples/loadlib/cpp_lib.cpp" 70 | ``` 71 | 72 | === "cpp_lib.h" 73 | ```cpp 74 | --8<-- "src/msl/examples/loadlib/cpp_lib.h" 75 | ``` 76 | -------------------------------------------------------------------------------- /docs/usage/direct/dotnet.md: -------------------------------------------------------------------------------- 1 | # .NET {: #direct-dotnet } 2 | 3 | Load a 64-bit C# library (a .NET Framework) in 64-bit Python. Include the `"net"` argument to indicate that the `.dll` file is for the .NET Framework. *To load the 32-bit library in 32-bit Python use `"dotnet_lib32.dll"` as the filename.* 4 | 5 | ## Example 6 | 7 | Load the example [.NET library][dotnet-lib] 8 | 9 | !!! tip 10 | `"clr"` is an alias for `"net"` and can also be used as the value of `libtype` when instantiating [LoadLibrary][msl.loadlib.load_library.LoadLibrary]. 11 | 12 | 16 | 17 | ```pycon 18 | >>> from msl.loadlib import LoadLibrary 19 | >>> from msl.examples.loadlib import EXAMPLES_DIR 20 | >>> net = LoadLibrary(EXAMPLES_DIR / "dotnet_lib64.dll", "net") 21 | 22 | ``` 23 | 24 | The library contains a reference to the `DotNetMSL` module (which is a C# namespace), the `StaticClass` class, the `StringManipulation` class and the [System]{:target="_blank"} namespace. 25 | 26 | Create an instance of the `BasicMath` class in the `DotNetMSL` namespace and call the `multiply_doubles` method 27 | 28 | ```pycon 29 | >>> bm = net.lib.DotNetMSL.BasicMath() 30 | >>> bm.multiply_doubles(2.3, 5.6) 31 | 12.879999... 32 | 33 | ``` 34 | 35 | Create an instance of the `ArrayManipulation` class in the `DotNetMSL` namespace and call the `scalar_multiply` method 36 | 37 | ```pycon 38 | >>> am = net.lib.DotNetMSL.ArrayManipulation() 39 | >>> values = am.scalar_multiply(2., [1., 2., 3., 4., 5.]) 40 | >>> values 41 | 42 | >>> [val for val in values] 43 | [2.0, 4.0, 6.0, 8.0, 10.0] 44 | 45 | ``` 46 | 47 | Call the `reverse_string` method in the `StringManipulation` class to reverse a string 48 | 49 | ```pycon 50 | >>> net.lib.StringManipulation().reverse_string("abcdefghijklmnopqrstuvwxyz") 51 | 'zyxwvutsrqponmlkjihgfedcba' 52 | 53 | ``` 54 | 55 | Call the static `add_multiple` method in the `StaticClass` class to add five integers 56 | 57 | ```pycon 58 | >>> net.lib.StaticClass.add_multiple(1, 2, 3, 4, 5) 59 | 15 60 | 61 | ``` 62 | 63 | One can create objects from the [System]{:target="_blank"} namespace, 64 | 65 | ```pycon 66 | >>> System = net.lib.System 67 | 68 | ``` 69 | 70 | for example, to create a 32-bit signed integer, 71 | 72 | ```pycon 73 | >>> System.Int32(9) 74 | 75 | 76 | ``` 77 | 78 | or, a one-dimensional [Array]{:target="_blank"} of the specified [Type]{:target="_blank"} 79 | 80 | ```pycon 81 | >>> array = System.Array[int](list(range(10))) 82 | >>> array 83 | 84 | >>> list(array) 85 | [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] 86 | >>> array[0] = -1 87 | >>> list(array) 88 | [-1, 1, 2, 3, 4, 5, 6, 7, 8, 9] 89 | 90 | ``` 91 | 92 | 97 | 98 | ## Configure a .NET runtime {: #config-runtime } 99 | 100 | To configure `pythonnet` to use the .NET Core runtime, you must either run 101 | 102 | ```python 103 | from pythonnet import load 104 | load("coreclr") 105 | ``` 106 | 107 | or define a `PYTHONNET_RUNTIME=coreclr` environment variable, e.g., 108 | 109 | ```python 110 | import os 111 | os.environ["PYTHONNET_RUNTIME"] = "coreclr" 112 | ``` 113 | 114 | before [LoadLibrary][msl.loadlib.load_library.LoadLibrary] is called. To use the Mono runtime, replace `"coreclr"` with `"mono"`. 115 | 116 | ## .NET Source Code {: #dotnet-lib } 117 | 118 | ??? example "dotnet_lib.cs" 119 | ```csharp 120 | --8<-- "src/msl/examples/loadlib/dotnet_lib.cs" 121 | ``` 122 | 123 | [System]: https://docs.microsoft.com/en-us/dotnet/api/system 124 | [Array]: https://docs.microsoft.com/en-us/dotnet/api/system.array 125 | [Type]: https://docs.microsoft.com/en-us/dotnet/api/system.type 126 | -------------------------------------------------------------------------------- /docs/usage/direct/fortran.md: -------------------------------------------------------------------------------- 1 | # FORTRAN {: #direct-fortran } 2 | 3 | Load a 64-bit FORTRAN library in 64-bit Python. *To load the 32-bit library in 32-bit Python use `"fortran_lib32"` as the filename.* 4 | 5 | !!! tip 6 | If the file extension is not specified, a default extension, `.dll` (Windows), `.so` (Linux) or `.dylib` (macOS) is used. 7 | 8 | ## Example 9 | 10 | Load the example [FORTRAN library][fortran-lib] 11 | 12 | 16 | 17 | ```pycon 18 | >>> from msl.loadlib import LoadLibrary 19 | >>> from msl.examples.loadlib import EXAMPLES_DIR 20 | >>> fortran = LoadLibrary(EXAMPLES_DIR / "fortran_lib64") 21 | 22 | ``` 23 | 24 | Call the `factorial` function. With a FORTRAN library you must pass values by reference using [byref][ctypes.byref]{:target="_blank"} and since the returned value is not of type [c_int][ctypes.c_int]{:target="_blank"} the [restype][ctypes-return-types]{:target="_blank"} must be configured for a value of type [c_double][ctypes.c_double]{:target="_blank"} to be returned from the library function 25 | 26 | ```pycon 27 | >>> from ctypes import byref, c_int, c_double 28 | >>> fortran.lib.factorial.restype = c_double 29 | >>> fortran.lib.factorial(byref(c_int(37))) 30 | 1.3763753091226343e+43 31 | 32 | ``` 33 | 34 | ## FORTRAN Source Code {: #fortran-lib } 35 | 36 | ??? example "fortran_lib.f90" 37 | ```fortran 38 | --8<-- "src/msl/examples/loadlib/fortran_lib.f90" 39 | ``` 40 | -------------------------------------------------------------------------------- /docs/usage/direct/index.md: -------------------------------------------------------------------------------- 1 | # Directly loading a library {: #direct } 2 | 3 | If you are loading a 64-bit library in 64-bit Python (or a 32-bit library in 32-bit Python) then you can directly load the library using the [LoadLibrary][msl.loadlib.load_library.LoadLibrary] class. 4 | 5 | !!! attention 6 | See [Client-Server][client-server] if you want to load a 32-bit library in 64-bit Python. 7 | 8 | The following examples are included with the `msl-loadlib` package: 9 | 10 | * [C++][direct-cpp] — compiled in 32- and 64-bit Windows and Linux and in 64-bit macOS 11 | * [FORTRAN][direct-fortran] — compiled in 32- and 64-bit Windows and Linux and in 64-bit macOS 12 | * [.NET][direct-dotnet] — complied in 32- and 64-bit using Microsoft Visual Studio 2017 13 | * [Java][direct-java] — platform and bitness independent since it runs in the [JVM]{:target="_blank"} 14 | * [COM][direct-com] — load a [Component Object Model]{:target="_blank"} library on Windows 15 | * [ActiveX][direct-activex] — illustrates how to load [ActiveX]{:target="_blank"} controls on Windows 16 | * [Windows __stdcall][direct-stdcall] — a 32-bit library that uses the `__stdcall` calling convention 17 | * [LabVIEW][direct-labview] — built using 32- and 64-bit LabVIEW on Windows 18 | 19 | [Component Object Model]: https://learn.microsoft.com/en-us/windows/win32/com/component-object-model--com--portal 20 | [JVM]: https://en.wikipedia.org/wiki/Java_virtual_machine 21 | [ActiveX]: https://learn.microsoft.com/en-us/windows/win32/com/activex-controls 22 | -------------------------------------------------------------------------------- /docs/usage/direct/java.md: -------------------------------------------------------------------------------- 1 | # Java {: #direct-java } 2 | 3 | Since Java byte code is executed in a [JVM]{:target="_blank"} it doesn't matter whether it was built with a 32- or 64-bit Java Development Kit. The Python interpreter does not load the Java byte code but communicates with the [JVM]{:target="_blank"} through a local network socket that is managed by [Py4J]{:target="_blank"}. 4 | 5 | ## Example (.jar) 6 | Load the example [Java archive][java-lib], `java_lib.jar` 7 | 8 | ```pycon 9 | >>> from msl.loadlib import LoadLibrary 10 | >>> from msl.examples.loadlib import EXAMPLES_DIR 11 | >>> jar = LoadLibrary(EXAMPLES_DIR / "java_lib.jar") 12 | >>> jar 13 | 14 | >>> jar.gateway 15 | 16 | 17 | ``` 18 | 19 | The Java archive contains a `nz.msl.examples` package with two classes, `MathUtils` and `Matrix` 20 | 21 | ```pycon 22 | >>> MathUtils = jar.lib.nz.msl.examples.MathUtils 23 | >>> Matrix = jar.lib.nz.msl.examples.Matrix 24 | 25 | ``` 26 | 27 | Calculate the square root of a number using the `MathUtils` class 28 | 29 | ```pycon 30 | >>> MathUtils.sqrt(32.4) 31 | 5.692099788303... 32 | 33 | ``` 34 | 35 | Solve a linear system of equations, `Ax=b`, using the `Matrix` library class and the `gateway` object to allocate memory for the `A` and `b` arrays 36 | 37 | ```pycon 38 | >>> A = jar.gateway.new_array(jar.lib.Double, 3, 3) 39 | >>> coefficients = [[3, 2, -1], [7, -2, 4], [-1, 5, 1]] 40 | >>> for i in range(3): 41 | ... for j in range(3): 42 | ... A[i][j] = float(coefficients[i][j]) 43 | ... 44 | >>> b = jar.gateway.new_array(jar.lib.Double, 3) 45 | >>> b[0] = 4.0 46 | >>> b[1] = 15.0 47 | >>> b[2] = 12.0 48 | >>> x = Matrix.solve(Matrix(A), Matrix(b)) 49 | >>> print(x.toString()) 50 | +1.000000e+00 51 | +2.000000e+00 52 | +3.000000e+00 53 | 54 | ``` 55 | 56 | Verify that `x` is the solution 57 | 58 | ```pycon 59 | >>> for i in range(3): 60 | ... x_i = 0.0 61 | ... for j in range(3): 62 | ... x_i += coefficients[i][j] * x.getValue(j,0) 63 | ... assert abs(x_i - b[i]) < 1e-12 64 | ... 65 | 66 | ``` 67 | 68 | Shutdown the connection to the [JVM]{:target="_blank"} when finished 69 | 70 | ```pycon 71 | >>> jar.gateway.shutdown() 72 | 73 | ``` 74 | 75 | ## Example (.class) 76 | 77 | Load the example [Java byte code][java-lib], `Trig.class` 78 | 79 | ```pycon 80 | >>> cls = LoadLibrary(EXAMPLES_DIR / "Trig.class") 81 | >>> cls 82 | 83 | >>> cls.lib 84 | 85 | 86 | ``` 87 | 88 | The Java library contains a `Trig` class, which calculates various trigonometric quantities 89 | 90 | ```pycon 91 | >>> Trig = cls.lib.Trig 92 | >>> Trig 93 | 94 | >>> Trig.cos(1.2) 95 | 0.3623577544766... 96 | >>> Trig.asin(0.6) 97 | 0.6435011087932... 98 | >>> Trig.tanh(1.3) 99 | 0.8617231593133... 100 | 101 | ``` 102 | 103 | Shutdown the connection to the [JVM]{:target="_blank"} when finished 104 | 105 | ```pycon 106 | >>> cls.gateway.shutdown() 107 | 108 | ``` 109 | 110 | ## Java Source Code {: #java-lib } 111 | 112 | ??? example "java_lib.jar" 113 | 114 | === "MathUtils.java" 115 | ```java 116 | --8<-- "src/msl/examples/loadlib/nz/msl/examples/MathUtils.java" 117 | ``` 118 | 119 | === "Matrix.java" 120 | ```java 121 | --8<-- "src/msl/examples/loadlib/nz/msl/examples/Matrix.java" 122 | ``` 123 | 124 | ??? example "Trig.class" 125 | ```java 126 | --8<-- "src/msl/examples/loadlib/Trig.java" 127 | ``` 128 | 129 | [JVM]: https://en.wikipedia.org/wiki/Java_virtual_machine 130 | [Py4J]: https://www.py4j.org/ 131 | -------------------------------------------------------------------------------- /docs/usage/direct/labview.md: -------------------------------------------------------------------------------- 1 | # LabVIEW {: #direct-labview } 2 | 3 | Load a 64-bit [LabVIEW]{:target="_blank"} library in 64-bit Python. A [LabVIEW Run-Time Engine]{:target="_blank"} ≥ 2017 (of the appropriate bitness) must be installed. The [LabVIEW]{:target="_blank"} example is only valid on Windows. *To load the 32-bit library in 32-bit Python use `labview_lib32.dll` as the filename.* 4 | 5 | !!! note 6 | A LabVIEW library can be built into a DLL using the `__cdecl` or `__stdcall` calling convention. Make sure that you specify the appropriate `libtype` when instantiating the [LoadLibrary][msl.loadlib.load_library.LoadLibrary] class for your LabVIEW library. The example library uses the C calling convention `__cdecl`. 7 | 8 | ## Example 9 | 10 | Load the example [LabVIEW library][labview-lib] 11 | 12 | 16 | 17 | ```pycon 18 | >>> from msl.loadlib import LoadLibrary 19 | >>> from msl.examples.loadlib import EXAMPLES_DIR 20 | >>> labview = LoadLibrary(EXAMPLES_DIR / "labview_lib64.dll") 21 | >>> labview 22 | 23 | >>> labview.lib 24 | 25 | 26 | ``` 27 | 28 | Create some data to calculate the mean, variance and standard deviation of 29 | 30 | ```pycon 31 | >>> data = [1, 2, 3, 4, 5, 6, 7, 8, 9] 32 | 33 | ``` 34 | 35 | Convert `data` to a [ctypes][]{:target="_blank"} array and allocate memory for the parameters 36 | 37 | ```pycon 38 | >>> from ctypes import c_double 39 | >>> x = (c_double * len(data))(*data) 40 | >>> mean, variance, std = c_double(), c_double(), c_double() 41 | 42 | ``` 43 | 44 | Calculate the sample standard deviation (i.e., the third argument is set to 0) and variance. Pass `mean`, `variance`, `std` by reference using [byref][ctypes.byref]{:target="_blank"} so that LabVIEW can write to the memory locations 45 | 46 | ```pycon 47 | >>> from ctypes import byref 48 | >>> ret = labview.lib.stdev(x, len(data), 0, byref(mean), byref(variance), byref(std)) 49 | >>> mean.value 50 | 5.0 51 | >>> variance.value 52 | 7.5 53 | >>> std.value 54 | 2.7386127875258306 55 | 56 | ``` 57 | 58 | Calculate the population standard deviation (i.e., the third argument is set to 1) and variance 59 | 60 | ```pycon 61 | >>> ret = labview.lib.stdev(x, len(data), 1, byref(mean), byref(variance), byref(std)) 62 | >>> mean.value 63 | 5.0 64 | >>> variance.value 65 | 6.666666666666667 66 | >>> std.value 67 | 2.581988897471611 68 | 69 | ``` 70 | 71 | ## LabVIEW Source Code {: #labview-lib } 72 | 73 | ??? example "labview_lib" 74 | 75 | === "labview_lib.vi" 76 | ![labview_lib](../../assets/images/labview_lib.png) 77 | 78 | === "labview_lib.h" 79 | ```cpp 80 | --8<-- "src/msl/examples/loadlib/labview_lib.h" 81 | ``` 82 | 83 | [LabVIEW]: https://www.ni.com/en-us/shop/labview.html 84 | [LabVIEW Run-Time Engine]: https://www.ni.com/en/support/downloads/software-products/download.labview-runtime.html 85 | -------------------------------------------------------------------------------- /docs/usage/direct/stdcall.md: -------------------------------------------------------------------------------- 1 | # Windows __stdcall {: #direct-stdcall } 2 | 3 | Load a 32-bit Windows `__stdcall` library in 32-bit Python, see [kernel32]{:target="_blank"}. Include the `"windll"` argument to specify that the calling convention is `__stdcall`. 4 | 5 | 9 | 10 | ```pycon 11 | >>> from msl.loadlib import LoadLibrary 12 | >>> kernel = LoadLibrary(r"C:\Windows\SysWOW64\kernel32.dll", "windll") 13 | >>> kernel 14 | 15 | >>> kernel.lib 16 | 17 | 18 | ``` 19 | 20 | Create an instance of the [SYSTEMTIME]{:target="_blank"} structure 21 | 22 | ```pycon 23 | >>> from ctypes import byref 24 | >>> from msl.examples.loadlib.kernel32 import SystemTime 25 | >>> st = SystemTime() 26 | >>> time = kernel.lib.GetLocalTime(byref(st)) 27 | 28 | ``` 29 | 30 | Now that we have a [SYSTEMTIME]{:target="_blank"} structure we can access its attributes and compare the values to the builtin [datetime.datetime][]{:target="_blank"} module 31 | 32 | ```pycon 33 | >>> from datetime import datetime 34 | >>> today = datetime.today() 35 | >>> st.wYear == today.year 36 | True 37 | >>> st.wMonth == today.month 38 | True 39 | >>> st.wDay == today.day 40 | True 41 | 42 | ``` 43 | 44 | See [here][ipc-stdcall] for an example on how to communicate with [kernel32]{:target="_blank"} from 64-bit Python. 45 | 46 | [kernel32]: https://www.geoffchappell.com/studies/windows/win32/kernel32/api/ 47 | [SYSTEMTIME]: https://docs.microsoft.com/en-us/windows/win32/api/minwinbase/ns-minwinbase-systemtime 48 | -------------------------------------------------------------------------------- /docs/usage/ipc/dotnet.md: -------------------------------------------------------------------------------- 1 | # .NET {: #ipc-dotnet } 2 | 3 | This example shows how to access a 32-bit .NET library from 64-bit Python (Windows only — [Mono]{:target="_blank"} can load both 32-bit and 64-bit libraries on 64-bit Linux and therefore a 32-bit .NET library can be loaded directly via [LoadLibrary][msl.loadlib.load_library.LoadLibrary] on 64-bit Linux). [DotNet32][msl.examples.loadlib.dotnet32.DotNet32] is the 32-bit server and [DotNet64][msl.examples.loadlib.dotnet64.DotNet64] is the 64-bit client. The source code of the C# program is available [here][dotnet-lib]. 4 | 5 | ??? tip "Decompile a .NET assembly" 6 | The [JetBrains dotPeek]{:target="_blank"} program can be used to decompile a .NET assembly. For example, *peeking* inside the example [dotnet_lib32.dll][dotnet-lib] library, that the [DotNet32][msl.examples.loadlib.dotnet32.DotNet32] class is a wrapper around, gives 7 | 8 | ![dotpeek_lib.png](../../assets/images/dotpeek_lib.png) 9 | 10 | 11 | ??? note "Configure a .NET runtime" 12 | To configure `pythonnet` to use the .NET Core runtime, you must either run 13 | 14 | ```python 15 | from pythonnet import load 16 | load("coreclr") 17 | ``` 18 | 19 | or define a `PYTHONNET_RUNTIME=coreclr` environment variable, e.g., 20 | 21 | ```python 22 | import os 23 | os.environ["PYTHONNET_RUNTIME"] = "coreclr" 24 | ``` 25 | 26 | before [super()][super]{:target="_blank"} is called in the [Server32][] subclass. To use the Mono runtime, replace `"coreclr"` with `"mono"`. 27 | 28 | Create a [DotNet64][msl.examples.loadlib.dotnet64.DotNet64] client to communicate with the 32-bit [dotnet_lib32.dll][dotnet-lib] library 29 | 30 | 34 | 35 | ```pycon 36 | >>> from msl.examples.loadlib import DotNet64 37 | >>> dn = DotNet64() 38 | 39 | ``` 40 | 41 | ## Numeric types {: #ipc-cpp-numerics } 42 | 43 | Add two integers, see [DotNet64.add_integers][msl.examples.loadlib.dotnet64.DotNet64.add_integers] 44 | 45 | ```pycon 46 | >>> dn.add_integers(8, 2) 47 | 10 48 | 49 | ``` 50 | 51 | Divide two C# floating-point numbers, see [DotNet64.divide_floats][msl.examples.loadlib.dotnet64.DotNet64.divide_floats] 52 | 53 | ```pycon 54 | >>> dn.divide_floats(3., 2.) 55 | 1.5 56 | 57 | ``` 58 | 59 | Multiple two C# double-precision numbers, see [DotNet64.multiply_doubles][msl.examples.loadlib.dotnet64.DotNet64.multiply_doubles] 60 | 61 | ```pycon 62 | >>> dn.multiply_doubles(872.24, 525.525) 63 | 458383.926 64 | 65 | ``` 66 | 67 | Add or subtract two C# double-precision numbers, see [DotNet64.add_or_subtract][msl.examples.loadlib.dotnet64.DotNet64.add_or_subtract] 68 | 69 | ```pycon 70 | >>> dn.add_or_subtract(99., 9., do_addition=True) 71 | 108.0 72 | >>> dn.add_or_subtract(99., 9., do_addition=False) 73 | 90.0 74 | 75 | ``` 76 | 77 | ## Arrays {: #ipc-dotnet-arrays } 78 | 79 | Multiply a 1D array by a number, see [DotNet64.scalar_multiply][msl.examples.loadlib.dotnet64.DotNet64.scalar_multiply] 80 | 81 | ```pycon 82 | >>> a = [float(val) for val in range(10)] 83 | >>> a 84 | [0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0] 85 | >>> dn.scalar_multiply(2.0, a) 86 | [0.0, 2.0, 4.0, 6.0, 8.0, 10.0, 12.0, 14.0, 16.0, 18.0] 87 | 88 | ``` 89 | 90 | Multiply two matrices, see [DotNet64.multiply_matrices][msl.examples.loadlib.dotnet64.DotNet64.multiply_matrices] 91 | 92 | ```pycon 93 | >>> m1 = [[1., 2., 3.], [4., 5., 6.]] 94 | >>> m2 = [[1., 2.], [3., 4.], [5., 6.]] 95 | >>> dn.multiply_matrices(m1, m2) 96 | [[22.0, 28.0], [49.0, 64.0]] 97 | 98 | ``` 99 | 100 | ## Strings {: #ipc-dotnet-strings } 101 | 102 | Get the names of the classes in the .NET library module, see [DotNet64.get_class_names][msl.examples.loadlib.dotnet64.DotNet64.get_class_names] 103 | 104 | ```pycon 105 | >>> dn.get_class_names() 106 | ['StringManipulation', 'StaticClass', 'DotNetMSL.BasicMath', 'DotNetMSL.ArrayManipulation'] 107 | 108 | ``` 109 | 110 | Reverse a string, see [DotNet64.reverse_string][msl.examples.loadlib.dotnet64.DotNet64.reverse_string] 111 | 112 | ```pycon 113 | >>> dn.reverse_string("New Zealand") 114 | 'dnalaeZ weN' 115 | 116 | ``` 117 | 118 | ## Static Class {: #ipc-dotnet-static } 119 | 120 | Call the static methods in the `StaticClass` class 121 | 122 | ```pycon 123 | >>> dn.add_multiple(1, 2, 3, 4, 5) 124 | 15 125 | >>> dn.concatenate("the", " experiment", " worked", False, " temporarily") 126 | 'the experiment worked' 127 | >>> dn.concatenate("the", " experiment", " worked", True, " temporarily") 128 | 'the experiment worked temporarily' 129 | 130 | ``` 131 | 132 | You have access to the server's `stdout` and `stderr` streams when you shut down the server 133 | 134 | ```pycon 135 | >>> stdout, stderr = dn.shutdown_server32() 136 | 137 | ``` 138 | 139 | [JetBrains dotPeek]: https://www.jetbrains.com/decompiler/ 140 | [Mono]: https://www.mono-project.com/download/stable/ 141 | -------------------------------------------------------------------------------- /docs/usage/ipc/echo.md: -------------------------------------------------------------------------------- 1 | # *Echo* {: #ipc-echo } 2 | 3 | This example illustrates that Python data types are preserved when they are passed from the [Echo64][msl.examples.loadlib.echo64.Echo64] client to the [Echo32][msl.examples.loadlib.echo32.Echo32] server and back. The [Echo32.received_data][msl.examples.loadlib.echo32.Echo32.received_data] method simply returns a [tuple][]{:target="_blank"} of the `(args, kwargs)` that it received back to the [Echo64.send_data][msl.examples.loadlib.echo64.Echo64.send_data] method in the client. 4 | 5 | Create an [Echo64][msl.examples.loadlib.echo64.Echo64] instance 6 | 7 | 11 | 12 | ```pycon 13 | >>> from msl.examples.loadlib import Echo64 14 | >>> echo = Echo64() 15 | 16 | ``` 17 | 18 | send a boolean as an argument 19 | 20 | ```pycon 21 | >>> echo.send_data(True) 22 | ((True,), {}) 23 | 24 | ``` 25 | 26 | send a boolean as a keyword argument 27 | 28 | ```pycon 29 | >>> echo.send_data(boolean=True) 30 | ((), {'boolean': True}) 31 | 32 | ``` 33 | 34 | send multiple data types as arguments and as keyword arguments 35 | 36 | ```pycon 37 | >>> echo.send_data(1.2, {"my_list":[1, 2, 3]}, 0.2j, range(10), x=True, y="hello world!") 38 | ((1.2, {'my_list': [1, 2, 3]}, 0.2j, range(0, 10)), {'x': True, 'y': 'hello world!'}) 39 | 40 | ``` 41 | 42 | You have access to the server's `stdout` and `stderr` streams when you shut down the server 43 | 44 | ```pycon 45 | >>> stdout, stderr = echo.shutdown_server32() 46 | 47 | ``` 48 | -------------------------------------------------------------------------------- /docs/usage/ipc/fortran.md: -------------------------------------------------------------------------------- 1 | # FORTRAN {: #ipc-fortran } 2 | 3 | This example shows how to access a 32-bit FORTRAN library from 64-bit Python. [Fortran32][msl.examples.loadlib.fortran32.Fortran32] is the 32-bit server and [Fortran64][msl.examples.loadlib.fortran64.Fortran64] is the 64-bit client. The source code of the FORTRAN program is available [here][fortran-lib]. 4 | 5 | !!! attention 6 | If you have issues running the example make sure that you have the [prerequisites][] installed. 7 | 8 | !!! important 9 | By default, [ctypes][]{:target="_blank"} expects that a [c_int][ctypes.c_int]{:target="_blank"} data type is returned from the library call. If the returned value from the library is not a [c_int][ctypes.c_int]{:target="_blank"} then you must redefine the ctypes [restype][ctypes-return-types]{:target="_blank"} value to be the appropriate data type. Also, the input arguments must be passed [by reference][ctypes.byref]{:target="_blank"}. The [Fortran32][msl.examples.loadlib.fortran32.Fortran32] class shows various examples of passing arguments by reference and defining the [restype][ctypes-return-types]{:target="_blank"} value. 10 | 11 | Create a [Fortran64][msl.examples.loadlib.fortran64.Fortran64] client to communicate with the 32-bit [fortran_lib32][fortran-lib] library 12 | 13 | 17 | 18 | ```pycon 19 | >>> from msl.examples.loadlib import Fortran64 20 | >>> f = Fortran64() 21 | 22 | ``` 23 | 24 | ## Numeric types {: #ipc-fortran-numerics } 25 | 26 | Add two `int8` values, see [Fortran64.sum_8bit][msl.examples.loadlib.fortran64.Fortran64.sum_8bit] 27 | 28 | ```pycon 29 | >>> f.sum_8bit(-50, 110) 30 | 60 31 | 32 | ``` 33 | 34 | Add two `int16` values, see [Fortran64.sum_16bit][msl.examples.loadlib.fortran64.Fortran64.sum_16bit] 35 | 36 | ```pycon 37 | >>> f.sum_16bit(2**15-1, -1) 38 | 32766 39 | 40 | ``` 41 | 42 | Add two `int32` values, see [Fortran64.sum_32bit][msl.examples.loadlib.fortran64.Fortran64.sum_32bit] 43 | 44 | ```pycon 45 | >>> f.sum_32bit(123456788, 1) 46 | 123456789 47 | 48 | ``` 49 | 50 | Add two `int64` values, see [Fortran64.sum_64bit][msl.examples.loadlib.fortran64.Fortran64.sum_64bit] 51 | 52 | ```pycon 53 | >>> f.sum_64bit(2**63, -2**62) 54 | 4611686018427387904 55 | 56 | ``` 57 | 58 | Multiply two `float32` values, see [Fortran64.multiply_float32][msl.examples.loadlib.fortran64.Fortran64.multiply_float32] 59 | 60 | ```pycon 61 | >>> f.multiply_float32(2.0, 3.0) 62 | 6.0 63 | 64 | ``` 65 | 66 | Multiply two `float64` values, see [Fortran64.multiply_float64][msl.examples.loadlib.fortran64.Fortran64.multiply_float64] 67 | 68 | ```pycon 69 | >>> f.multiply_float64(1e30, 2e3) 70 | 2.00000000000...e+33 71 | 72 | ``` 73 | 74 | Check if a value is positive, see [Fortran64.is_positive][msl.examples.loadlib.fortran64.Fortran64.is_positive] 75 | 76 | ```pycon 77 | >>> f.is_positive(1.0) 78 | True 79 | >>> f.is_positive(-0.1) 80 | False 81 | 82 | ``` 83 | 84 | Add or subtract two integers, see [Fortran64.add_or_subtract][msl.examples.loadlib.fortran64.Fortran64.add_or_subtract] 85 | 86 | ```pycon 87 | >>> f.add_or_subtract(1000, 2000, do_addition=True) 88 | 3000 89 | >>> f.add_or_subtract(1000, 2000, do_addition=False) 90 | -1000 91 | 92 | ``` 93 | 94 | Calculate the n'th factorial, see [Fortran64.factorial][msl.examples.loadlib.fortran64.Fortran64.factorial] 95 | 96 | ```pycon 97 | >>> f.factorial(0) 98 | 1.0 99 | >>> f.factorial(127) 100 | 3.012660018457658e+213 101 | 102 | ``` 103 | 104 | Compute the Bessel function of the first kind of order 0, see [Fortran64.besselJ0][msl.examples.loadlib.fortran64.Fortran64.besselJ0] 105 | 106 | ```pycon 107 | >>> f.besselJ0(8.6) 108 | 0.0146229912787412... 109 | 110 | ``` 111 | 112 | ## Arrays {: #ipc-fortran-arrays } 113 | 114 | Calculate the standard deviation of a list of values, see [Fortran64.standard_deviation][msl.examples.loadlib.fortran64.Fortran64.standard_deviation] 115 | 116 | ```pycon 117 | >>> f.standard_deviation([float(val) for val in range(1,10)]) 118 | 2.73861278752583... 119 | 120 | ``` 121 | 122 | Add two 1D arrays, see [Fortran64.add_1d_arrays][msl.examples.loadlib.fortran64.Fortran64.add_1d_arrays] 123 | 124 | ```pycon 125 | >>> a = [float(val) for val in range(1, 10)] 126 | >>> b = [0.5*val for val in range(1, 10)] 127 | >>> a 128 | [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0] 129 | >>> b 130 | [0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0, 4.5] 131 | >>> f.add_1d_arrays(a, b) 132 | [1.5, 3.0, 4.5, 6.0, 7.5, 9.0, 10.5, 12.0, 13.5] 133 | 134 | ``` 135 | 136 | Multiply two matrices, see [Fortran64.matrix_multiply][msl.examples.loadlib.fortran64.Fortran64.matrix_multiply] 137 | 138 | ```pycon 139 | >>> m1 = [[1, 2, 3], [4, 5, 6]] 140 | >>> m2 = [[1, 2], [3, 4], [5, 6]] 141 | >>> f.matrix_multiply(m1, m2) 142 | [[22.0, 28.0], [49.0, 64.0]] 143 | 144 | ``` 145 | 146 | ## Strings {: #ipc-fortran-strings } 147 | 148 | Reverse a string, see [Fortran64.reverse_string][msl.examples.loadlib.fortran64.Fortran64.reverse_string] 149 | 150 | ```pycon 151 | >>> f.reverse_string("hello world!") 152 | '!dlrow olleh' 153 | 154 | ``` 155 | 156 | You have access to the server's `stdout` and `stderr` streams when you shut down the server 157 | 158 | ```pycon 159 | >>> stdout, stderr = f.shutdown_server32() 160 | 161 | ``` 162 | -------------------------------------------------------------------------------- /docs/usage/ipc/labview.md: -------------------------------------------------------------------------------- 1 | # LabVIEW {: #ipc-labview } 2 | 3 | This example shows how to access a 32-bit LabVIEW library from 64-bit Python. [Labview32][msl.examples.loadlib.labview32.Labview32] is the 32-bit server and [Labview64][msl.examples.loadlib.labview64.Labview64] is the 64-bit client. The source code of the LabVIEW program is available [here][labview-lib]. 4 | 5 | !!! attention 6 | This example requires that a 32-bit [LabVIEW Run-Time Engine]{:target="_blank"} ≥ 2017 is installed and that the operating system is Windows. 7 | 8 | Create a [Labview64][msl.examples.loadlib.labview64.Labview64] client to communicate with the 32-bit [labview_lib32][labview-lib] library 9 | 10 | 14 | 15 | ```pycon 16 | >>> from msl.examples.loadlib import Labview64 17 | >>> labview = Labview64() 18 | 19 | ``` 20 | 21 | Calculate the mean, the *sample* variance and the standard deviation of some data, see [Labview64.stdev][msl.examples.loadlib.labview64.Labview64.stdev] 22 | 23 | ```pycon 24 | >>> data = [1, 2, 3, 4, 5, 6, 7, 8, 9] 25 | >>> labview.stdev(data) 26 | (5.0, 7.5, 2.7386127875258306) 27 | 28 | ``` 29 | 30 | Calculate the mean, the *population* variance and the standard deviation of `data` 31 | 32 | ```pycon 33 | >>> labview.stdev(data, weighting=1) 34 | (5.0, 6.666666666666667, 2.581988897471611) 35 | 36 | ``` 37 | 38 | You have access to the server's `stdout` and `stderr` streams when you shut down the server 39 | 40 | ```pycon 41 | >>> stdout, stderr = labview.shutdown_server32() 42 | 43 | ``` 44 | 45 | [LabVIEW Run-Time Engine]: https://www.ni.com/en/support/downloads/software-products/download.labview-runtime.html 46 | [SYSTEMTIME]: https://docs.microsoft.com/en-us/windows/win32/api/minwinbase/ns-minwinbase-systemtime 47 | -------------------------------------------------------------------------------- /docs/usage/ipc/stdcall.md: -------------------------------------------------------------------------------- 1 | # Windows __stdcall {: #ipc-stdcall } 2 | 3 | This example shows how to access the 32-bit Windows [kernel32]{:target="_blank"} library from 64-bit Python. [Kernel32][msl.examples.loadlib.kernel32.Kernel32] is the 32-bit server and [Kernel64][msl.examples.loadlib.kernel64.Kernel64] is the 64-bit client. 4 | 5 | Create a [Kernel64][msl.examples.loadlib.kernel64.Kernel64] client to communicate with the 32-bit [kernel32]{:target="_blank"} library 6 | 7 | 11 | 12 | ```pycon 13 | >>> from msl.examples.loadlib import Kernel64 14 | >>> k = Kernel64() 15 | >>> k.lib32_path 16 | 'C:\\Windows\\SysWOW64\\kernel32.dll' 17 | 18 | ``` 19 | 20 | Call the library to get the current date and time by populating the [SYSTEMTIME]{:target="_blank"} structure, see [Kernel64.get_local_time][msl.examples.loadlib.kernel64.Kernel64.get_local_time] 21 | 22 | ```pycon 23 | >>> now = k.get_local_time() 24 | 25 | ``` 26 | 27 | 32 | 33 | You have access to the server's `stdout` and `stderr` streams when you shut down the server 34 | 35 | ```pycon 36 | >>> stdout, stderr = k.shutdown_server32() 37 | 38 | ``` 39 | 40 | [kernel32]: https://www.geoffchappell.com/studies/windows/win32/kernel32/api/ 41 | [SYSTEMTIME]: https://docs.microsoft.com/en-us/windows/win32/api/minwinbase/ns-minwinbase-systemtime 42 | -------------------------------------------------------------------------------- /docs/usage/overview.md: -------------------------------------------------------------------------------- 1 | # Overview 2 | 3 | There are two ways to load a library 4 | 5 | 1. [Directly][direct] load a 64-bit library in 64-bit Python (or a 32-bit library in 32-bit Python) 6 | 2. Use a [Client-Server][client-server] implementation to communicate with a 32-bit library from 64-bit Python. You may also create a [custom server][refreeze]. 7 | -------------------------------------------------------------------------------- /docs/usage/refreeze.md: -------------------------------------------------------------------------------- 1 | # Custom Server {: #refreeze } 2 | 3 | If you want to create a custom 32-bit server, you will need 4 | 5 | * a 32-bit version of Python (version 3.8 or later) installed 6 | * [PyInstaller]{:target="_blank"} installed in the 32-bit Python environment (ideally, you would use a [virtual environment][venv]{:target="_blank"} to install the necessary packages to create the server) 7 | 8 | Some reasons why you may want to create a custom 32-bit server are that you want to 9 | 10 | * run the server with a different version of Python, 11 | * build the server for a different architecture, 12 | * install a different version of `comtypes` or `pythonnet` (on Windows), 13 | * install additional packages on the server (e.g., `numpy`, `my_custom_package`), 14 | * embed your own data files in the frozen server. 15 | 16 | Using pip from a 32-bit Python interpreter, run 17 | 18 | !!! note "You may also want to install additional packages." 19 | 20 | ```console 21 | pip install msl-loadlib pyinstaller 22 | ``` 23 | 24 | You have two options to create a 32-bit server 25 | 26 | 1. [Using the API][refreeze-api] 27 | 2. [Using the CLI][refreeze-cli] 28 | 29 | and you have two options to use your custom server 30 | 31 | 1. Copy your `server32-*` file to the `../site-packages/msl/loadlib` directory where you have `msl-loadlib` installed in your 64-bit version of Python to replace the existing server file. 32 | 2. Specify the directory where your `server32-*` file is located as the value of the `server32_dir` keyword argument in [Client64][]. 33 | 34 | ## Using the API {: #refreeze-api } 35 | 36 | Create a script that calls the [freeze_server32.main][msl.loadlib.freeze_server32.main] function with the appropriate keyword arguments and run your script using a 32-bit Python interpreter. For example, the following script will include 32-bit `numpy` on the server 37 | 38 | ```python 39 | from msl.loadlib import freeze_server32 40 | freeze_server32.main(imports="numpy") 41 | ``` 42 | 43 | ## Using the CLI {: #refreeze-cli } 44 | 45 | When `msl-loadlib` is installed, a console script is included (named `freeze32`) that may be executed from the command line to create a new frozen 32-bit server. 46 | 47 | To see the help for `freeze32`, run 48 | 49 | ```console 50 | freeze32 --help 51 | ``` 52 | 53 | For example, if you want to include your own package and data files, you would run 54 | 55 | ```console 56 | freeze32 --imports my_package --data .\my_data\lib32.dll 57 | ``` 58 | 59 | [PyInstaller]: https://www.pyinstaller.org/ 60 | -------------------------------------------------------------------------------- /hatch_build.py: -------------------------------------------------------------------------------- 1 | """Build custom wheels. 2 | 3 | Each wheel bundles only the relevant files for the target platform. 4 | This is not like building a C extension, but only helps to reduce 5 | the file size of each wheel. 6 | 7 | Command to build all wheels 8 | 9 | $ hatch build -t custom 10 | 11 | or, for example, to build a wheel for 32-bit Windows 12 | 13 | $ hatch build -t custom:win32 14 | 15 | The corresponding table that is defined in pyproject.toml is 16 | 17 | [tool.hatch.build.targets.custom] 18 | versions = [ ... ] 19 | 20 | See https://hatch.pypa.io/latest/plugins/builder/custom/ 21 | """ 22 | 23 | from __future__ import annotations 24 | 25 | from typing import TYPE_CHECKING, Any, Callable 26 | 27 | from hatchling.builders.wheel import WheelBuilder 28 | 29 | if TYPE_CHECKING: 30 | from collections.abc import Iterable 31 | 32 | from hatchling.builders.plugin.interface import IncludedFile 33 | 34 | # files that endswith() any of the following characters are added to the wheel 35 | include = (".py", "py.typed", ".jar", ".class") 36 | 37 | # the keys used here must be the same as the values in the "versions" array 38 | # in the [tool.hatch.build.targets.custom] table of pyproject.toml 39 | versions: dict[str, tuple[str, ...]] = { 40 | "linux_i686": (*include, "lib32.so", "linux", "linux.config", "dotnet_lib32.dll"), 41 | "linux_x86_64": (*include, ".so", "linux", "linux.config", "dotnet_lib32.dll", "dotnet_lib64.dll"), 42 | "macos_arm64": (*include, "libarm64.dylib"), 43 | "macos_x86_64": (*include, "lib64.dylib", "dotnet_lib32.dll", "dotnet_lib64.dll"), 44 | "musl_aarch64": (*include,), 45 | "musl_x86_64": (*include,), 46 | "win32": (*include, ".dll", ".exe", ".exe.config"), 47 | "win_amd64": (*include, ".dll", ".exe", ".exe.config"), 48 | } 49 | 50 | 51 | class CustomWheelBuilder(WheelBuilder): 52 | """Build custom wheels.""" 53 | 54 | current_api: str = "" 55 | 56 | def build_linux_i686(self, directory: str, **build_data: Any) -> str: 57 | """Update the tag for a linux_i686 build.""" 58 | self.current_api = "linux_i686" 59 | build_data["tag"] = "py3-none-manylinux1_i686" 60 | return self.build_standard(directory, **build_data) 61 | 62 | def build_linux_x86_64(self, directory: str, **build_data: Any) -> str: 63 | """Update the tag for a linux_x86_64 build.""" 64 | self.current_api = "linux_x86_64" 65 | build_data["tag"] = "py3-none-manylinux1_x86_64" 66 | return self.build_standard(directory, **build_data) 67 | 68 | def build_macos_arm64(self, directory: str, **build_data: Any) -> str: 69 | """Update the tag for a macos_arm64 build.""" 70 | self.current_api = "macos_arm64" 71 | build_data["tag"] = "py3-none-macosx_11_0_arm64" 72 | return self.build_standard(directory, **build_data) 73 | 74 | def build_macos_x86_64(self, directory: str, **build_data: Any) -> str: 75 | """Update the tag for a macos_x86_64 build.""" 76 | self.current_api = "macos_x86_64" 77 | build_data["tag"] = "py3-none-macosx_10_6_x86_64" 78 | return self.build_standard(directory, **build_data) 79 | 80 | def build_musl_aarch64(self, directory: str, **build_data: Any) -> str: 81 | """Update the tag for a musl_aarch64 build.""" 82 | self.current_api = "musl_aarch64" 83 | build_data["tag"] = "py3-none-musllinux_1_1_aarch64" 84 | return self.build_standard(directory, **build_data) 85 | 86 | def build_musl_x86_64(self, directory: str, **build_data: Any) -> str: 87 | """Update the tag for a musl_x86_64 build.""" 88 | self.current_api = "musl_x86_64" 89 | build_data["tag"] = "py3-none-musllinux_1_1_x86_64" 90 | return self.build_standard(directory, **build_data) 91 | 92 | def build_win32(self, directory: str, **build_data: Any) -> str: 93 | """Update the tag for a win32 build.""" 94 | self.current_api = "win32" 95 | build_data["tag"] = "py3-none-win32" 96 | return self.build_standard(directory, **build_data) 97 | 98 | def build_win_amd64(self, directory: str, **build_data: Any) -> str: 99 | """Update the tag for a win_amd64 build.""" 100 | self.current_api = "win_amd64" 101 | build_data["tag"] = "py3-none-win_amd64" 102 | return self.build_standard(directory, **build_data) 103 | 104 | def get_version_api(self) -> dict[str, Callable[..., str]]: 105 | """Overrides abstractmethod BuilderInterface.get_version_api().""" 106 | return { 107 | "linux_i686": self.build_linux_i686, 108 | "linux_x86_64": self.build_linux_x86_64, 109 | "macos_arm64": self.build_macos_arm64, 110 | "macos_x86_64": self.build_macos_x86_64, 111 | "musl_aarch64": self.build_musl_aarch64, 112 | "musl_x86_64": self.build_musl_x86_64, 113 | "win32": self.build_win32, 114 | "win_amd64": self.build_win_amd64, 115 | } 116 | 117 | def recurse_included_files(self) -> Iterable[IncludedFile]: 118 | """Overrides WheelBuilder.recurse_included_files().""" 119 | for file in super().recurse_project_files(): 120 | if file.path.endswith(versions[self.current_api]): 121 | yield file 122 | 123 | 124 | def get_builder() -> type[CustomWheelBuilder]: 125 | """Adding this function fixes the following error. 126 | 127 | ValueError: Multiple subclasses of `BuilderInterface` found in `hatch_build.py`, 128 | select one by defining a function named `get_builder` 129 | """ 130 | return CustomWheelBuilder 131 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: MSL-LoadLib 2 | site_description: Load a library (and access a 32-bit library from 64-bit Python) 3 | copyright: Copyright © 2017 - 2025 Measurement Standards Laboratory of New Zealand 4 | repo_url: https://github.com/MSLNZ/msl-loadlib/ 5 | site_url: https://github.com/MSLNZ/msl-loadlib/ 6 | 7 | theme: 8 | name: material 9 | language: en 10 | favicon: assets/images/favicon.ico 11 | features: 12 | - content.code.copy 13 | - navigation.footer 14 | - navigation.indexes 15 | - navigation.top 16 | - navigation.tabs 17 | - navigation.tabs.sticky 18 | - search.highlight 19 | - search.suggest 20 | palette: 21 | # Palette toggle for automatic mode 22 | - media: '(prefers-color-scheme)' 23 | primary: indigo 24 | accent: indigo 25 | toggle: 26 | icon: material/brightness-auto 27 | name: Switch to light mode 28 | 29 | # Palette toggle for light mode 30 | - media: "(prefers-color-scheme: light)" 31 | scheme: default 32 | primary: indigo 33 | accent: indigo 34 | toggle: 35 | icon: material/weather-sunny 36 | name: Switch to dark mode 37 | 38 | # Palette toggle for dark mode 39 | - media: "(prefers-color-scheme: dark)" 40 | scheme: slate 41 | primary: black 42 | accent: deep orange 43 | toggle: 44 | icon: material/weather-night 45 | name: Switch to system preference 46 | 47 | watch: [src] 48 | 49 | nav: 50 | - Home: 51 | - index.md 52 | - install.md 53 | - release-notes.md 54 | - license.md 55 | - User Guide: 56 | - usage/overview.md 57 | - Direct: 58 | - usage/direct/index.md 59 | - usage/direct/cpp.md 60 | - usage/direct/fortran.md 61 | - usage/direct/dotnet.md 62 | - usage/direct/java.md 63 | - usage/direct/com.md 64 | - usage/direct/activex.md 65 | - usage/direct/stdcall.md 66 | - usage/direct/labview.md 67 | - Client-Server: 68 | - usage/ipc/index.md 69 | - usage/ipc/echo.md 70 | - usage/ipc/cpp.md 71 | - usage/ipc/fortran.md 72 | - usage/ipc/dotnet.md 73 | - usage/ipc/stdcall.md 74 | - usage/ipc/labview.md 75 | - usage/refreeze.md 76 | - Examples: 77 | - examples/index.md 78 | - examples/cpp32.md 79 | - examples/cpp64.md 80 | - examples/dotnet32.md 81 | - examples/dotnet64.md 82 | - examples/echo32.md 83 | - examples/echo64.md 84 | - examples/fortran32.md 85 | - examples/fortran64.md 86 | - examples/kernel32.md 87 | - examples/kernel64.md 88 | - examples/labview32.md 89 | - examples/labview64.md 90 | - API: 91 | - api/client64.md 92 | - api/load_library.md 93 | - api/server32.md 94 | - api/activex.md 95 | - api/exceptions.md 96 | - api/freeze_server32.md 97 | - api/types.md 98 | - api/utils.md 99 | - FAQ: 100 | - faq/streams.md 101 | - faq/freeze.md 102 | - faq/mock.md 103 | 104 | markdown_extensions: 105 | - admonition # enables: !!! tip 106 | - attr_list # enables: [link](url){:target="_blank"} and {: #rename-permalink } 107 | - pymdownx.details # enables collapsable admonition 108 | - pymdownx.snippets # enables: --8<-- 109 | - pymdownx.superfences # enables tabs in admonition 110 | - pymdownx.tabbed: 111 | alternate_style: true # tabs look better on narrower screen sizes (like mobile) 112 | - toc: 113 | permalink: "¤" 114 | 115 | plugins: 116 | - search 117 | - autorefs 118 | - mkdocstrings: 119 | handlers: 120 | python: 121 | paths: [src] 122 | inventories: 123 | - url: https://docs.python.org/3/objects.inv 124 | domains: [std, py] 125 | - https://numpy.org/doc/stable/objects.inv 126 | - https://www.py4j.org/objects.inv 127 | - https://comtypes.readthedocs.io/en/stable/objects.inv 128 | options: 129 | # General 130 | show_source: true 131 | 132 | # Headings 133 | show_root_toc_entry: false 134 | 135 | # Members 136 | filters: ["!^_"] 137 | inherited_members: false 138 | 139 | # Docstrings 140 | docstring_options: 141 | ignore_init_summary: true 142 | merge_init_into_class: true 143 | 144 | # Signatures 145 | separate_signature: true 146 | 147 | extra: 148 | version: 149 | provider: mike 150 | social: 151 | - icon: fontawesome/brands/github 152 | link: https://github.com/MSLNZ/msl-loadlib/ 153 | - icon: fontawesome/brands/python 154 | link: https://pypi.org/project/msl-loadlib/ 155 | -------------------------------------------------------------------------------- /src/msl/examples/loadlib/Trig.class: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MSLNZ/msl-loadlib/c3f9e7e6f4d0c739e6af608bbbe865504030f503/src/msl/examples/loadlib/Trig.class -------------------------------------------------------------------------------- /src/msl/examples/loadlib/Trig.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Compile with JDK 6 for maximal compatibility with Py4J 3 | * 4 | * javac Trig.java 5 | * 6 | */ 7 | 8 | public class Trig { 9 | 10 | /** Returns the trigonometric cosine of an angle. */ 11 | static public double cos(double x) { 12 | return Math.cos(x); 13 | } 14 | 15 | /** Returns the hyperbolic cosine of a value. */ 16 | static public double cosh(double x) { 17 | return Math.cosh(x); 18 | } 19 | 20 | /** Returns the arc cosine of a value, [0.0, pi]. */ 21 | static public double acos(double x) { 22 | return Math.acos(x); 23 | } 24 | 25 | /** Returns the trigonometric sine of an angle. */ 26 | static public double sin(double x) { 27 | return Math.sin(x); 28 | } 29 | 30 | /** Returns the hyperbolic sine of a value. */ 31 | static public double sinh(double x) { 32 | return Math.sinh(x); 33 | } 34 | 35 | /** Returns the arc sine of a value, [-pi/2, pi/2]. */ 36 | static public double asin(double x) { 37 | return Math.asin(x); 38 | } 39 | 40 | /** Returns the trigonometric tangent of an angle. */ 41 | static public double tan(double x) { 42 | return Math.tan(x); 43 | } 44 | 45 | /** Returns the hyperbolic tangent of a value. */ 46 | static public double tanh(double x) { 47 | return Math.tanh(x); 48 | } 49 | 50 | /** Returns the arc tangent of a value; [-pi/2, pi/2]. */ 51 | static public double atan(double x) { 52 | return Math.atan(x); 53 | } 54 | 55 | /** 56 | * Returns the angle theta from the conversion of rectangular coordinates 57 | * (x, y) to polar coordinates (r, theta). 58 | */ 59 | static public double atan2(double y, double x) { 60 | return Math.atan2(y, x); 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /src/msl/examples/loadlib/__init__.py: -------------------------------------------------------------------------------- 1 | """Examples showing how to load a 32-bit library in 64-bit Python.""" 2 | 3 | from __future__ import annotations 4 | 5 | from pathlib import Path 6 | 7 | from .cpp32 import Cpp32, FourPoints, NPoints, Point 8 | from .cpp64 import Cpp64 9 | from .dotnet32 import DotNet32 10 | from .dotnet64 import DotNet64 11 | from .echo32 import Echo32 12 | from .echo64 import Echo64 13 | from .fortran32 import Fortran32 14 | from .fortran64 import Fortran64 15 | from .kernel32 import Kernel32 16 | from .kernel64 import Kernel64 17 | from .labview32 import Labview32 18 | from .labview64 import Labview64 19 | 20 | EXAMPLES_DIR: Path = Path(__file__).parent 21 | 22 | __all__: list[str] = [ 23 | "Cpp32", 24 | "Cpp64", 25 | "DotNet32", 26 | "DotNet64", 27 | "Echo32", 28 | "Echo64", 29 | "Fortran32", 30 | "Fortran64", 31 | "FourPoints", 32 | "Kernel32", 33 | "Kernel64", 34 | "Labview32", 35 | "Labview64", 36 | "NPoints", 37 | "Point", 38 | ] 39 | -------------------------------------------------------------------------------- /src/msl/examples/loadlib/cpp64.py: -------------------------------------------------------------------------------- 1 | """Communicates with the [cpp_lib][cpp-lib] library via the [Cpp32][] class that is running on a server.""" 2 | 3 | from __future__ import annotations 4 | 5 | from pathlib import Path 6 | from typing import TYPE_CHECKING 7 | 8 | from msl.loadlib import Client64 9 | 10 | if TYPE_CHECKING: 11 | from collections.abc import Sequence 12 | 13 | from .cpp32 import FourPoints 14 | 15 | 16 | class Cpp64(Client64): 17 | """Communicates with a 32-bit C++ library.""" 18 | 19 | def __init__(self) -> None: 20 | """Communicates with a 32-bit C++ library via the server running [Cpp32][]. 21 | 22 | This class demonstrates how to communicate with a 32-bit C++ library if an 23 | instance of this class is created within a 64-bit Python interpreter. 24 | """ 25 | # specify the name of the corresponding 32-bit server module, cpp32, 26 | # which hosts the 32-bit C++ library -- cpp_lib32. 27 | super().__init__("cpp32", append_sys_path=Path(__file__).parent) 28 | 29 | def add(self, a: int, b: int) -> int: 30 | """Add two integers. 31 | 32 | See the corresponding [Cpp32.add][msl.examples.loadlib.cpp32.Cpp32.add] method. 33 | 34 | Args: 35 | a: First integer. 36 | b: Second integer. 37 | 38 | Returns: 39 | The sum, `a + b`. 40 | """ 41 | reply: int = self.request32("add", a, b) 42 | return reply 43 | 44 | def subtract(self, a: float, b: float) -> float: 45 | """Subtract two floating-point numbers *('float' refers to the C++ data type)*. 46 | 47 | See the corresponding [Cpp32.subtract][msl.examples.loadlib.cpp32.Cpp32.subtract] method. 48 | 49 | Args: 50 | a: First floating-point number. 51 | b: Second floating-point number. 52 | 53 | Returns: 54 | The difference, `a - b`. 55 | """ 56 | reply: float = self.request32("subtract", a, b) 57 | return reply 58 | 59 | def add_or_subtract(self, a: float, b: float, *, do_addition: bool) -> float: 60 | """Add or subtract two floating-point numbers *('double' refers to the C++ data type)*. 61 | 62 | See the corresponding [Cpp32.add_or_subtract][msl.examples.loadlib.cpp32.Cpp32.add_or_subtract] method. 63 | 64 | Args: 65 | a: First double-precision number. 66 | b: Second double-precision number. 67 | do_addition: Whether to add or subtract the numbers. 68 | 69 | Returns: 70 | `a + b` if `do_addition` is `True` else `a - b`. 71 | """ 72 | reply: float = self.request32("add_or_subtract", a, b, do_addition=do_addition) 73 | return reply 74 | 75 | def scalar_multiply(self, a: float, xin: Sequence[float]) -> list[float]: 76 | """Multiply each element in an array by a number. 77 | 78 | See the corresponding [Cpp32.scalar_multiply][msl.examples.loadlib.cpp32.Cpp32.scalar_multiply] method. 79 | 80 | Args: 81 | a: Scalar value. 82 | xin: Array to modify. 83 | 84 | Returns: 85 | A new array with each element in `xin` multiplied by `a`. 86 | """ 87 | reply: list[float] = self.request32("scalar_multiply", a, xin) 88 | return reply 89 | 90 | def reverse_string_v1(self, original: str) -> str: 91 | """Reverse a string (version 1). 92 | 93 | In this method Python allocates the memory for the reversed string 94 | and passes the string to C++. 95 | 96 | See the corresponding [Cpp32.reverse_string_v1][msl.examples.loadlib.cpp32.Cpp32.reverse_string_v1] method. 97 | 98 | Args: 99 | original: The original string. 100 | 101 | Returns: 102 | The string reversed. 103 | """ 104 | reply: str = self.request32("reverse_string_v1", original) 105 | return reply 106 | 107 | def reverse_string_v2(self, original: str) -> str: 108 | """Reverse a string (version 2). 109 | 110 | In this method C++ allocates the memory for the reversed string and passes 111 | the string to Python. 112 | 113 | See the corresponding [Cpp32.reverse_string_v2][msl.examples.loadlib.cpp32.Cpp32.reverse_string_v2] method. 114 | 115 | Args: 116 | original: The original string. 117 | 118 | Returns: 119 | The string reversed. 120 | """ 121 | reply: str = self.request32("reverse_string_v2", original) 122 | return reply 123 | 124 | def distance_4_points(self, points: FourPoints) -> float: 125 | """Calculates the total distance connecting 4 [Point][msl.examples.loadlib.cpp32.Point]s. 126 | 127 | See the corresponding [Cpp32.distance_4_points][msl.examples.loadlib.cpp32.Cpp32.distance_4_points] method. 128 | 129 | Args: 130 | points: The points to use to calculate the total distance. 131 | Since `points` is a struct that is a fixed size we can pass the 132 | [ctypes.Structure][]{:target="_blank"} object directly from 64-bit Python to 133 | the 32-bit Python. The [ctypes][]{:target="_blank"} module on the 32-bit server 134 | can load the [pickle][]{:target="_blank"}d [ctypes.Structure][]{:target="_blank"}. 135 | 136 | Returns: 137 | The total distance connecting the 4 points. 138 | """ 139 | reply: float = self.request32("distance_4_points", points) 140 | return reply 141 | 142 | def circumference(self, radius: float, n: int) -> float: 143 | """Estimates the circumference of a circle. 144 | 145 | This method calls the `distance_n_points` function in [cpp_lib][cpp-lib]. 146 | 147 | See the corresponding [Cpp32.circumference][msl.examples.loadlib.cpp32.Cpp32.circumference] method. 148 | 149 | Args: 150 | radius: The radius of the circle. 151 | n: The number of points to use to estimate the circumference. 152 | 153 | Returns: 154 | The estimated circumference of the circle. 155 | """ 156 | reply: float = self.request32("circumference", radius, n) 157 | return reply 158 | -------------------------------------------------------------------------------- /src/msl/examples/loadlib/cpp_lib.cpp: -------------------------------------------------------------------------------- 1 | // cpp_lib.cpp 2 | // Examples that show how to pass various data types between Python and a C++ library. 3 | // 4 | // Compiled using: 5 | // g++ cpp_lib.cpp -fPIC -shared -Bstatic -Wall -o cpp_lib64.so 6 | // 7 | #include 8 | #include "cpp_lib.h" 9 | 10 | int add(int a, int b) { 11 | return a + b; 12 | } 13 | 14 | float subtract(float a, float b) { 15 | return a - b; 16 | } 17 | 18 | double add_or_subtract(double a, double b, bool do_addition) { 19 | if (do_addition) { 20 | return a + b; 21 | } else { 22 | return a - b; 23 | } 24 | } 25 | 26 | void scalar_multiply(double a, double* xin, int n, double* xout) { 27 | for (int i = 0; i < n; i++) { 28 | xout[i] = a * xin[i]; 29 | } 30 | } 31 | 32 | void reverse_string_v1(const char* original, int n, char* reversed) { 33 | for (int i = 0; i < n; i++) { 34 | reversed[i] = original[n-i-1]; 35 | } 36 | } 37 | 38 | char* reverse_string_v2(char* original, int n) { 39 | char* reversed = new char[n]; 40 | for (int i = 0; i < n; i++) { 41 | reversed[i] = original[n - i - 1]; 42 | } 43 | return reversed; 44 | } 45 | 46 | // this function is not exported to the shared library 47 | double distance(Point p1, Point p2) { 48 | double d = sqrt(pow(p1.x - p2.x, 2) + pow(p1.y - p2.y, 2)); 49 | return d; 50 | } 51 | 52 | double distance_4_points(FourPoints p) { 53 | double d = distance(p.points[0], p.points[3]); 54 | for (int i = 1; i < 4; i++) { 55 | d += distance(p.points[i], p.points[i-1]); 56 | } 57 | return d; 58 | } 59 | 60 | double distance_n_points(NPoints p) { 61 | if (p.n < 2) { 62 | return 0.0; 63 | } 64 | double d = distance(p.points[0], p.points[p.n-1]); 65 | for (int i = 1; i < p.n; i++) { 66 | d += distance(p.points[i], p.points[i-1]); 67 | } 68 | return d; 69 | } 70 | -------------------------------------------------------------------------------- /src/msl/examples/loadlib/cpp_lib.h: -------------------------------------------------------------------------------- 1 | // cpp_lib.h 2 | // Contains the declaration of exported functions. 3 | // 4 | 5 | #if defined(_MSC_VER) 6 | // Microsoft 7 | #define EXPORT __declspec(dllexport) 8 | #elif defined(__GNUC__) 9 | // G++ 10 | #define EXPORT __attribute__((visibility("default"))) 11 | #else 12 | # error "Unknown EXPORT semantics" 13 | #endif 14 | 15 | struct Point { 16 | double x; 17 | double y; 18 | }; 19 | 20 | struct FourPoints { 21 | Point points[4]; 22 | }; 23 | 24 | struct NPoints { 25 | int n; 26 | Point *points; 27 | }; 28 | 29 | extern "C" { 30 | 31 | // a + b 32 | EXPORT int add(int a, int b); 33 | 34 | // a - b 35 | EXPORT float subtract(float a, float b); 36 | 37 | // IF do_addition IS TRUE THEN a + b ELSE a - b 38 | EXPORT double add_or_subtract(double a, double b, bool do_addition); 39 | 40 | // multiply each element in 'x' by 'a' 41 | EXPORT void scalar_multiply(double a, double* xin, int n, double* xout); 42 | 43 | // reverse a string 44 | EXPORT void reverse_string_v1(const char* original, int n, char* reversed); 45 | 46 | // reverse a string and return it 47 | EXPORT char* reverse_string_v2(char* original, int n); 48 | 49 | // calculate the total distance connecting 4 Points 50 | EXPORT double distance_4_points(FourPoints p); 51 | 52 | // calculate the total distance connecting N Points 53 | EXPORT double distance_n_points(NPoints p); 54 | 55 | } -------------------------------------------------------------------------------- /src/msl/examples/loadlib/cpp_lib32.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MSLNZ/msl-loadlib/c3f9e7e6f4d0c739e6af608bbbe865504030f503/src/msl/examples/loadlib/cpp_lib32.dll -------------------------------------------------------------------------------- /src/msl/examples/loadlib/cpp_lib32.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MSLNZ/msl-loadlib/c3f9e7e6f4d0c739e6af608bbbe865504030f503/src/msl/examples/loadlib/cpp_lib32.so -------------------------------------------------------------------------------- /src/msl/examples/loadlib/cpp_lib64.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MSLNZ/msl-loadlib/c3f9e7e6f4d0c739e6af608bbbe865504030f503/src/msl/examples/loadlib/cpp_lib64.dll -------------------------------------------------------------------------------- /src/msl/examples/loadlib/cpp_lib64.dylib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MSLNZ/msl-loadlib/c3f9e7e6f4d0c739e6af608bbbe865504030f503/src/msl/examples/loadlib/cpp_lib64.dylib -------------------------------------------------------------------------------- /src/msl/examples/loadlib/cpp_lib64.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MSLNZ/msl-loadlib/c3f9e7e6f4d0c739e6af608bbbe865504030f503/src/msl/examples/loadlib/cpp_lib64.so -------------------------------------------------------------------------------- /src/msl/examples/loadlib/cpp_libarm64.dylib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MSLNZ/msl-loadlib/c3f9e7e6f4d0c739e6af608bbbe865504030f503/src/msl/examples/loadlib/cpp_libarm64.dylib -------------------------------------------------------------------------------- /src/msl/examples/loadlib/dotnet_lib.cs: -------------------------------------------------------------------------------- 1 | // dotnet_lib.cs 2 | // Examples that show how to pass various data types between Python and a C# library. 3 | // 4 | using System; 5 | 6 | // The DotNetMSL namespace contains two classes: BasicMath, ArrayManipulation 7 | namespace DotNetMSL 8 | { 9 | // A class that is part of the DotNetMSL namespace 10 | public class BasicMath 11 | { 12 | 13 | public int add_integers(int a, int b) 14 | { 15 | return a + b; 16 | } 17 | 18 | public float divide_floats(float a, float b) 19 | { 20 | return a / b; 21 | } 22 | 23 | public double multiply_doubles(double a, double b) 24 | { 25 | return a * b; 26 | } 27 | 28 | public double add_or_subtract(double a, double b, bool do_addition) 29 | { 30 | if (do_addition) 31 | { 32 | return a + b; 33 | } 34 | else 35 | { 36 | return a - b; 37 | } 38 | } 39 | 40 | } 41 | 42 | // A class that is part of the DotNetMSL namespace 43 | public class ArrayManipulation 44 | { 45 | 46 | public double[] scalar_multiply(double a, double[] xin) 47 | { 48 | int n = xin.GetLength(0); 49 | double[] xout = new double[n]; 50 | for (int i = 0; i < n; i++) 51 | { 52 | xout[i] = a * xin[i]; 53 | } 54 | return xout; 55 | } 56 | 57 | public double[,] multiply_matrices(double[,] A, double[,] B) 58 | { 59 | int rA = A.GetLength(0); 60 | int cA = A.GetLength(1); 61 | int rB = B.GetLength(0); 62 | int cB = B.GetLength(1); 63 | double temp = 0; 64 | double[,] C = new double[rA, cB]; 65 | if (cA != rB) 66 | { 67 | Console.WriteLine("matrices can't be multiplied!"); 68 | return new double[0, 0]; 69 | } 70 | else 71 | { 72 | for (int i = 0; i < rA; i++) 73 | { 74 | for (int j = 0; j < cB; j++) 75 | { 76 | temp = 0; 77 | for (int k = 0; k < cA; k++) 78 | { 79 | temp += A[i, k] * B[k, j]; 80 | } 81 | C[i, j] = temp; 82 | } 83 | } 84 | return C; 85 | } 86 | } 87 | 88 | } 89 | } 90 | 91 | // A class that is not part of the DotNetMSL namespace 92 | public class StringManipulation 93 | { 94 | 95 | public string reverse_string(string original) 96 | { 97 | char[] charArray = original.ToCharArray(); 98 | Array.Reverse(charArray); 99 | return new string(charArray); 100 | } 101 | 102 | } 103 | 104 | // A static class 105 | public static class StaticClass 106 | { 107 | 108 | public static int add_multiple(int a, int b, int c, int d, int e) 109 | { 110 | return a + b + c + d + e; 111 | } 112 | 113 | public static string concatenate(string a, string b, string c, bool d, string e) 114 | { 115 | string res = a + b + c; 116 | if (d) 117 | { 118 | res += e; 119 | } 120 | return res; 121 | 122 | } 123 | 124 | } 125 | -------------------------------------------------------------------------------- /src/msl/examples/loadlib/dotnet_lib32.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MSLNZ/msl-loadlib/c3f9e7e6f4d0c739e6af608bbbe865504030f503/src/msl/examples/loadlib/dotnet_lib32.dll -------------------------------------------------------------------------------- /src/msl/examples/loadlib/dotnet_lib64.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MSLNZ/msl-loadlib/c3f9e7e6f4d0c739e6af608bbbe865504030f503/src/msl/examples/loadlib/dotnet_lib64.dll -------------------------------------------------------------------------------- /src/msl/examples/loadlib/echo32.py: -------------------------------------------------------------------------------- 1 | """An example of a 32-bit *echo* server. 2 | 3 | [Echo32][] is the 32-bit server class and [Echo64][] is the 64-bit client class. 4 | These *Echo* classes do not actually communicate with a library. The point of these 5 | *Echo* classes is to show that a Python data type in a 64-bit process appears as 6 | the same data type in the 32-bit process and vice versa. 7 | (*provided that the data type is [pickle][]{:target="_blank"}able.*) 8 | """ 9 | 10 | from __future__ import annotations 11 | 12 | from pathlib import Path 13 | from typing import Any 14 | 15 | from msl.loadlib import Server32 16 | 17 | 18 | class Echo32(Server32): 19 | """Example that shows Python data types are preserved between [Echo32][] and [Echo64][].""" 20 | 21 | def __init__(self, host: str, port: int) -> None: 22 | """Example that shows Python data types are preserved between [Echo32][] and [Echo64][]. 23 | 24 | Args: 25 | host: The IP address (or hostname) to use for the server. 26 | port: The port to open for the server. 27 | """ 28 | # even though this is a *echo* class that does not call a library 29 | # we still need to provide a library file that exists. Use the C++ library. 30 | path = Path(__file__).parent / "cpp_lib32" 31 | super().__init__(path, "cdll", host, port) 32 | 33 | @staticmethod 34 | def received_data(*args: Any, **kwargs: Any) -> tuple[tuple[Any, ...], dict[str, Any]]: # type: ignore[misc] 35 | """Process a request from the [Echo64.send_data][msl.examples.loadlib.echo64.Echo64.send_data] method. 36 | 37 | Args: 38 | args: The arguments. 39 | kwargs: The keyword arguments. 40 | 41 | Returns: 42 | The `args` and `kwargs` that were received. 43 | """ 44 | return args, kwargs 45 | -------------------------------------------------------------------------------- /src/msl/examples/loadlib/echo64.py: -------------------------------------------------------------------------------- 1 | """An example of a 64-bit *echo* client. 2 | 3 | [Echo32][] is the 32-bit server class and [Echo64][] is the 64-bit client class. 4 | These *Echo* classes do not actually communicate with a library. The point of these 5 | *Echo* classes is to show that a Python data type in a 64-bit process appears as 6 | the same data type in the 32-bit process and vice versa 7 | (*provided that the data type is [pickle][]{:target="_blank"}able.*) 8 | """ 9 | 10 | from __future__ import annotations 11 | 12 | from pathlib import Path 13 | from typing import Any 14 | 15 | from msl.loadlib import Client64 16 | 17 | 18 | class Echo64(Client64): 19 | """Example that shows Python data types are preserved between [Echo32][] and [Echo64][].""" 20 | 21 | def __init__(self) -> None: 22 | """Example that shows Python data types are preserved between [Echo32][] and [Echo64][].""" 23 | super().__init__("echo32", append_sys_path=Path(__file__).parent) 24 | 25 | def send_data(self, *args: Any, **kwargs: Any) -> tuple[tuple[Any, ...], dict[str, Any]]: 26 | """Send a request to [Echo32.received_data][msl.examples.loadlib.echo32.Echo32.received_data]. 27 | 28 | Args: 29 | args: The arguments to send. 30 | kwargs: The keyword arguments to send. 31 | 32 | Returns: 33 | The `args` and `kwargs` that were sent. 34 | """ 35 | reply: tuple[tuple[Any, ...], dict[str, Any]] = self.request32("received_data", *args, **kwargs) 36 | return reply 37 | -------------------------------------------------------------------------------- /src/msl/examples/loadlib/fortran_lib.f90: -------------------------------------------------------------------------------- 1 | ! fortran_lib.f90 2 | ! 3 | ! Basic examples of passing different data types to a FORTRAN function and subroutine. 4 | ! 5 | ! Compiled in Windows using: 6 | ! gfortran -fno-underscoring -fPIC fortran_lib.f90 -static -shared -o fortran_lib64.dll 7 | ! 8 | 9 | ! return the sum of two 8-bit signed integers 10 | function sum_8bit(a, b) result(value) 11 | !DEC$ ATTRIBUTES DLLEXPORT, ALIAS:'sum_8bit' :: sum_8bit 12 | implicit none 13 | integer(1) :: a, b, value 14 | value = a + b 15 | end function sum_8bit 16 | 17 | 18 | ! return the sum of two 16-bit signed integers 19 | function sum_16bit(a, b) result(value) 20 | !DEC$ ATTRIBUTES DLLEXPORT, ALIAS:'sum_16bit' :: sum_16bit 21 | implicit none 22 | integer(2) :: a, b, value 23 | value = a + b 24 | end function sum_16bit 25 | 26 | 27 | ! return the sum of two 32-bit signed integers 28 | function sum_32bit(a, b) result(value) 29 | !DEC$ ATTRIBUTES DLLEXPORT, ALIAS:'sum_32bit' :: sum_32bit 30 | implicit none 31 | integer(4) :: a, b, value 32 | value = a + b 33 | end function sum_32bit 34 | 35 | 36 | ! return the sum of two 64-bit signed integers 37 | function sum_64bit(a, b) result(value) 38 | !DEC$ ATTRIBUTES DLLEXPORT, ALIAS:'sum_64bit' :: sum_64bit 39 | implicit none 40 | integer(8) :: a, b, value 41 | value = a + b 42 | end function sum_64bit 43 | 44 | 45 | ! return the product of two 32-bit floating point numbers 46 | function multiply_float32(a, b) result(value) 47 | !DEC$ ATTRIBUTES DLLEXPORT, ALIAS:'multiply_float32' :: multiply_float32 48 | implicit none 49 | real(4) :: a, b, value 50 | value = a * b 51 | end function multiply_float32 52 | 53 | 54 | ! return the product of two 64-bit floating point numbers 55 | function multiply_float64(a, b) result(value) 56 | !DEC$ ATTRIBUTES DLLEXPORT, ALIAS:'multiply_float64' :: multiply_float64 57 | implicit none 58 | real(8) :: a, b, value 59 | value = a * b 60 | end function multiply_float64 61 | 62 | 63 | ! return True if 'a' > 0 else False 64 | function is_positive(a) result(value) 65 | !DEC$ ATTRIBUTES DLLEXPORT, ALIAS:'is_positive' :: is_positive 66 | implicit none 67 | logical :: value 68 | real(8) :: a 69 | value = a > 0.d0 70 | end function is_positive 71 | 72 | 73 | ! if do_addition is True return a+b otherwise return a-b 74 | function add_or_subtract(a, b, do_addition) result(value) 75 | !DEC$ ATTRIBUTES DLLEXPORT, ALIAS:'add_or_subtract' :: add_or_subtract 76 | implicit none 77 | logical :: do_addition 78 | integer(4) :: a, b, value 79 | if (do_addition) then 80 | value = a + b 81 | else 82 | value = a - b 83 | endif 84 | end function add_or_subtract 85 | 86 | 87 | ! compute the n'th factorial of a 8-bit signed integer, return a double-precision number 88 | function factorial(n) result(value) 89 | !DEC$ ATTRIBUTES DLLEXPORT, ALIAS:'factorial' :: factorial 90 | implicit none 91 | integer(1) :: n 92 | integer(4) :: i 93 | double precision value 94 | if (n < 0) then 95 | value = 0.d0 96 | print *, "Cannot compute the factorial of a negative number", n 97 | else 98 | value = 1.d0 99 | do i = 2, n 100 | value = value * i 101 | enddo 102 | endif 103 | end function factorial 104 | 105 | 106 | ! calculate the standard deviation of an array. 107 | function standard_deviation(a, n) result(var) 108 | !DEC$ ATTRIBUTES DLLEXPORT, ALIAS:'standard_deviation' :: standard_deviation 109 | integer :: n ! the length of the array 110 | double precision :: var, a(n) 111 | var = SUM(a)/SIZE(a) ! SUM is a built-in fortran function 112 | var = SQRT(SUM((a-var)**2)/(SIZE(a)-1.0)) 113 | end function standard_deviation 114 | 115 | 116 | ! compute the Bessel function of the first kind of order 0 of x 117 | function besselj0(x) result(val) 118 | !DEC$ ATTRIBUTES DLLEXPORT, ALIAS:'besselj0' :: besselj0 119 | double precision :: x, val 120 | val = BESSEL_J0(x) 121 | end function besselJ0 122 | 123 | 124 | ! reverse a string, 'n' is the length of the original string 125 | subroutine reverse_string(original, n, reversed) 126 | !DEC$ ATTRIBUTES DLLEXPORT, ALIAS:'reverse_string' :: reverse_string 127 | !DEC$ ATTRIBUTES REFERENCE :: original, reversed 128 | implicit none 129 | integer :: i, n 130 | character(len=n) :: original, reversed 131 | do i = 1, n 132 | reversed(i:i) = original(n-i+1:n-i+1) 133 | end do 134 | end subroutine reverse_string 135 | 136 | 137 | ! element-wise addition of two 1D double-precision arrays 138 | subroutine add_1d_arrays(a, in1, in2, n) 139 | !DEC$ ATTRIBUTES DLLEXPORT, ALIAS:'add_1d_arrays' :: add_1d_arrays 140 | implicit none 141 | integer(4) :: n ! the length of the input arrays 142 | double precision :: in1(n), in2(n) ! the arrays to add (element-wise) 143 | double precision :: a(n) ! the array that will contain the element-wise sum 144 | a(:) = in1(:) + in2(:) 145 | end subroutine add_1d_arrays 146 | 147 | 148 | ! multiply two 2D, double-precision arrays. 149 | ! NOTE: multi-dimensional arrays are column-major order in FORTRAN, 150 | ! whereas C (Python) is row-major order. 151 | subroutine matrix_multiply(a, a1, r1, c1, a2, r2, c2) 152 | !DEC$ ATTRIBUTES DLLEXPORT, ALIAS:'matrix_multiply' :: matrix_multiply 153 | implicit none 154 | integer(4) :: r1, c1, r2, c2 ! the dimensions of the input arrays 155 | double precision :: a1(r1,c1), a2(r2,c2) ! the arrays to multiply 156 | double precision :: a(r1,c2) ! resultant array 157 | a = MATMUL(a1, a2) 158 | end subroutine matrix_multiply 159 | -------------------------------------------------------------------------------- /src/msl/examples/loadlib/fortran_lib32.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MSLNZ/msl-loadlib/c3f9e7e6f4d0c739e6af608bbbe865504030f503/src/msl/examples/loadlib/fortran_lib32.dll -------------------------------------------------------------------------------- /src/msl/examples/loadlib/fortran_lib32.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MSLNZ/msl-loadlib/c3f9e7e6f4d0c739e6af608bbbe865504030f503/src/msl/examples/loadlib/fortran_lib32.so -------------------------------------------------------------------------------- /src/msl/examples/loadlib/fortran_lib64.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MSLNZ/msl-loadlib/c3f9e7e6f4d0c739e6af608bbbe865504030f503/src/msl/examples/loadlib/fortran_lib64.dll -------------------------------------------------------------------------------- /src/msl/examples/loadlib/fortran_lib64.dylib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MSLNZ/msl-loadlib/c3f9e7e6f4d0c739e6af608bbbe865504030f503/src/msl/examples/loadlib/fortran_lib64.dylib -------------------------------------------------------------------------------- /src/msl/examples/loadlib/fortran_lib64.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MSLNZ/msl-loadlib/c3f9e7e6f4d0c739e6af608bbbe865504030f503/src/msl/examples/loadlib/fortran_lib64.so -------------------------------------------------------------------------------- /src/msl/examples/loadlib/fortran_libarm64.dylib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MSLNZ/msl-loadlib/c3f9e7e6f4d0c739e6af608bbbe865504030f503/src/msl/examples/loadlib/fortran_libarm64.dylib -------------------------------------------------------------------------------- /src/msl/examples/loadlib/java_lib.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MSLNZ/msl-loadlib/c3f9e7e6f4d0c739e6af608bbbe865504030f503/src/msl/examples/loadlib/java_lib.jar -------------------------------------------------------------------------------- /src/msl/examples/loadlib/kernel32.py: -------------------------------------------------------------------------------- 1 | """Wrapper around the 32-bit Windows `__stdcall` library, [kernel32.dll]{:target="_blank"}. 2 | 3 | Example of a server that loads a 32-bit Windows library, [kernel32.dll]{:target="_blank"}, 4 | in a 32-bit Python interpreter to host the library. The corresponding [Kernel64][] class 5 | is created in a 64-bit Python interpreter and the [Kernel64][] class sends requests 6 | to the [Kernel32][] class which calls the 32-bit library to execute the request and 7 | then returns the response from the library. 8 | 9 | !!! note 10 | This example is only valid on Windows. 11 | 12 | [kernel32.dll]: https://www.geoffchappell.com/studies/windows/win32/kernel32/api/ 13 | """ 14 | 15 | from __future__ import annotations 16 | 17 | import ctypes 18 | from ctypes.wintypes import WORD 19 | from datetime import datetime 20 | 21 | from msl.loadlib import Server32 22 | 23 | 24 | class Kernel32(Server32): 25 | """Wrapper around the 32-bit Windows `__stdcall` library.""" 26 | 27 | def __init__(self, host: str, port: int) -> None: 28 | """Wrapper around the 32-bit Windows `__stdcall` library, [kernel32.dll]{:target="_blank"}. 29 | 30 | [kernel32.dll]: https://www.geoffchappell.com/studies/windows/win32/kernel32/api/ 31 | 32 | Args: 33 | host: The IP address (or hostname) to use for the server. 34 | port: The port to open for the server. 35 | """ 36 | super().__init__("C:/Windows/SysWOW64/kernel32.dll", "windll", host, port) 37 | 38 | def get_local_time(self) -> datetime: 39 | """Calls the [kernel32.GetLocalTime]{:target="_blank"} function to get the current date and time. 40 | 41 | See the corresponding [Kernel64.get_local_time][msl.examples.loadlib.kernel64.Kernel64.get_local_time] method. 42 | 43 | [kernel32.GetLocalTime]: https://learn.microsoft.com/en-us/windows/win32/api/sysinfoapi/nf-sysinfoapi-getlocaltime 44 | 45 | Returns: 46 | The current date and time. 47 | """ 48 | st = SystemTime() 49 | self.lib.GetLocalTime(ctypes.byref(st)) 50 | return datetime( # noqa: DTZ001 51 | st.wYear, 52 | month=st.wMonth, 53 | day=st.wDay, 54 | hour=st.wHour, 55 | minute=st.wMinute, 56 | second=st.wSecond, 57 | microsecond=st.wMilliseconds * 1000, 58 | ) 59 | 60 | 61 | class SystemTime(ctypes.Structure): 62 | """A [SYSTEMTIME]{:target="_blank"} [Structure][ctypes.Structure]{:target="_blank"}. 63 | 64 | [SYSTEMTIME]: https://msdn.microsoft.com/en-us/library/windows/desktop/ms724950(v=vs.85).aspx 65 | """ 66 | 67 | _fields_ = ( # pyright: ignore[reportUnannotatedClassAttribute] 68 | ("wYear", WORD), 69 | ("wMonth", WORD), 70 | ("wDayOfWeek", WORD), 71 | ("wDay", WORD), 72 | ("wHour", WORD), 73 | ("wMinute", WORD), 74 | ("wSecond", WORD), 75 | ("wMilliseconds", WORD), 76 | ) 77 | -------------------------------------------------------------------------------- /src/msl/examples/loadlib/kernel64.py: -------------------------------------------------------------------------------- 1 | """Communicate with the [kernel32.dll]{:target="_blank"} library via the [Kernel32][] class that is running on a server. 2 | 3 | !!! note 4 | This example is only valid on Windows. 5 | 6 | [kernel32.dll]: https://www.geoffchappell.com/studies/windows/win32/kernel32/api/ 7 | """ 8 | 9 | from __future__ import annotations 10 | 11 | from pathlib import Path 12 | from typing import TYPE_CHECKING 13 | 14 | from msl.loadlib import Client64 15 | 16 | if TYPE_CHECKING: 17 | from datetime import datetime 18 | 19 | 20 | class Kernel64(Client64): 21 | """Communicate with a 32-bit Windows `__stdcall` library.""" 22 | 23 | def __init__(self) -> None: 24 | """Communicate with a 32-bit Windows `__stdcall` library, [kernel32.dll]{:target="_blank"}. 25 | 26 | This class demonstrates how to communicate with a Windows 32-bit library if an 27 | instance of this class is created within a 64-bit Python interpreter. 28 | 29 | [kernel32.dll]: https://www.geoffchappell.com/studies/windows/win32/kernel32/api/ 30 | """ 31 | # specify the name of the corresponding 32-bit server module, kernel32, which hosts 32 | # the Windows 32-bit library -- kernel32.dll 33 | super().__init__(module32="kernel32", append_sys_path=Path(__file__).parent) 34 | 35 | def get_local_time(self) -> datetime: 36 | """Requests [kernel32.GetLocalTime]{:target="_blank"} function to get the current date and time. 37 | 38 | See the corresponding [Kernel32.get_local_time][msl.examples.loadlib.kernel32.Kernel32.get_local_time] method. 39 | 40 | [kernel32.GetLocalTime]: https://learn.microsoft.com/en-us/windows/win32/api/sysinfoapi/nf-sysinfoapi-getlocaltime 41 | 42 | Returns: 43 | The current date and time. 44 | """ 45 | reply: datetime = self.request32("get_local_time") 46 | return reply 47 | -------------------------------------------------------------------------------- /src/msl/examples/loadlib/labview32.py: -------------------------------------------------------------------------------- 1 | """Wrapper around a 32-bit LabVIEW library. 2 | 3 | Example of a server that loads a 32-bit LabVIEW library, [labview_lib][labview-lib], 4 | in a 32-bit Python interpreter to host the library. The corresponding [Labview64][] class 5 | is created in a 64-bit Python interpreter and the [Labview64][] class sends requests 6 | to the [Labview32][] class which calls the 32-bit library to execute the request and 7 | then returns the response from the library. 8 | 9 | !!! attention 10 | This example requires that a 32-bit 11 | [LabVIEW Run-Time Engine](https://www.ni.com/en/support/downloads/software-products/download.labview-runtime.html){:target="_blank"} 12 | ≥ 2017 is installed and that the operating system is Windows. 13 | """ 14 | 15 | from __future__ import annotations 16 | 17 | from ctypes import byref, c_double 18 | from pathlib import Path 19 | from typing import TYPE_CHECKING 20 | 21 | from msl.loadlib import Server32 22 | 23 | if TYPE_CHECKING: 24 | from collections.abc import Sequence 25 | 26 | 27 | class Labview32(Server32): 28 | """Wrapper around the 32-bit LabVIEW library, [labview_lib][labview-lib].""" 29 | 30 | def __init__(self, host: str, port: int) -> None: 31 | """Wrapper around the 32-bit LabVIEW library, [labview_lib][labview-lib]. 32 | 33 | Args: 34 | host: The IP address (or hostname) to use for the server. 35 | port: The port to open for the server. 36 | """ 37 | path = Path(__file__).parent / "labview_lib32.dll" 38 | super().__init__(path, "cdll", host, port) 39 | 40 | def stdev(self, x: Sequence[float], weighting: int = 0) -> tuple[float, float, float]: 41 | """Calculates the mean, variance and standard deviation of the values in the input `x`. 42 | 43 | See the corresponding [Labview64.stdev][msl.examples.loadlib.labview64.Labview64.stdev] method. 44 | 45 | Args: 46 | x: The data to calculate the mean, variance and standard deviation of. 47 | weighting: Whether to calculate the sample (`weighting = 0`) or the 48 | population (`weighting = 1`) standard deviation and variance. 49 | 50 | Returns: 51 | The mean, variance and standard deviation. 52 | """ 53 | data = (c_double * len(x))(*x) 54 | mean, variance, std = c_double(), c_double(), c_double() 55 | self.lib.stdev(data, len(x), weighting, byref(mean), byref(variance), byref(std)) 56 | return mean.value, variance.value, std.value 57 | -------------------------------------------------------------------------------- /src/msl/examples/loadlib/labview64.py: -------------------------------------------------------------------------------- 1 | """Communicates with the [labview_lib][labview-lib] library via the [Labview32][] class that is running on a server. 2 | 3 | !!! attention 4 | This example requires that a 32-bit 5 | [LabVIEW Run-Time Engine](https://www.ni.com/en/support/downloads/software-products/download.labview-runtime.html){:target="_blank"} 6 | ≥ 2017 is installed and that the operating system is Windows. 7 | """ 8 | 9 | from __future__ import annotations 10 | 11 | from pathlib import Path 12 | from typing import TYPE_CHECKING 13 | 14 | from msl.loadlib import Client64 15 | 16 | if TYPE_CHECKING: 17 | from collections.abc import Sequence 18 | 19 | 20 | class Labview64(Client64): 21 | """Communicates with a 32-bit LabVIEW library, [labview_lib][labview-lib].""" 22 | 23 | def __init__(self) -> None: 24 | """Communicates with a 32-bit LabVIEW library, [labview_lib][labview-lib]. 25 | 26 | This class demonstrates how to communicate with a 32-bit LabVIEW library if an 27 | instance of this class is created within a 64-bit Python interpreter. 28 | """ 29 | # specify the name of the corresponding 32-bit server module, labview32, which hosts 30 | # the 32-bit LabVIEW library -- labview_lib32.dll 31 | super().__init__(module32="labview32", append_sys_path=Path(__file__).parent) 32 | 33 | def stdev(self, x: Sequence[float], weighting: int = 0) -> tuple[float, float, float]: 34 | """Calculates the mean, variance and standard deviation of the values in the input `x`. 35 | 36 | See the corresponding [Labview32.stdev][msl.examples.loadlib.labview32.Labview32.stdev] method. 37 | 38 | Args: 39 | x: The data to calculate the mean, variance and standard deviation of. 40 | weighting: Whether to calculate the sample (`weighting = 0`) or the 41 | population (`weighting = 1`) standard deviation and variance. 42 | 43 | Returns: 44 | The mean, variance and standard deviation. 45 | """ 46 | if weighting not in {0, 1}: 47 | msg = f"The weighting must be either 0 or 1, got {weighting}" 48 | raise ValueError(msg) 49 | 50 | reply: tuple[float, float, float] = self.request32("stdev", x, weighting) 51 | return reply 52 | -------------------------------------------------------------------------------- /src/msl/examples/loadlib/labview_lib.h: -------------------------------------------------------------------------------- 1 | #include "extcode.h" 2 | #pragma pack(push) 3 | #pragma pack(1) 4 | 5 | #ifdef __cplusplus 6 | extern "C" { 7 | #endif 8 | typedef uint16_t Enum; 9 | #define Enum_Sample 0 10 | #define Enum_Population 1 11 | 12 | /*! 13 | * stdev 14 | */ 15 | void __cdecl stdev(double X[], int32_t lenX, Enum WeightingSample, 16 | double *mean, double *variance, double *standardDeviation); 17 | 18 | MgErr __cdecl LVDLLStatus(char *errStr, int errStrLen, void *module); 19 | 20 | #ifdef __cplusplus 21 | } // extern "C" 22 | #endif 23 | 24 | #pragma pack(pop) 25 | 26 | -------------------------------------------------------------------------------- /src/msl/examples/loadlib/labview_lib32.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MSLNZ/msl-loadlib/c3f9e7e6f4d0c739e6af608bbbe865504030f503/src/msl/examples/loadlib/labview_lib32.dll -------------------------------------------------------------------------------- /src/msl/examples/loadlib/labview_lib64.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MSLNZ/msl-loadlib/c3f9e7e6f4d0c739e6af608bbbe865504030f503/src/msl/examples/loadlib/labview_lib64.dll -------------------------------------------------------------------------------- /src/msl/examples/loadlib/nz/msl/examples/MathUtils.java: -------------------------------------------------------------------------------- 1 | package nz.msl.examples; 2 | 3 | public class MathUtils { 4 | 5 | /** Generate a random number between [0, 1) */ 6 | static public double random() { 7 | return Math.random(); 8 | } 9 | 10 | /** Calculate the square root of {@code x} */ 11 | static public double sqrt(double x) { 12 | return Math.sqrt(x); 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/msl/examples/loadlib/nz/msl/examples/readme.txt: -------------------------------------------------------------------------------- 1 | Use JDK 6 for maximal compatibility with Py4J 2 | $ cd msl-loadlib/msl/examples/loadlib 3 | $ javac nz/msl/examples/*.java 4 | $ jar cfv java_lib.jar nz/msl/examples/*.class 5 | 6 | -------------------------------------------------------------------------------- /src/msl/loadlib/Py4JWrapper.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Use JDK 6 for maximal compatibility with Py4J 3 | * 4 | * cd msl-loadlib/msl/loadlib/ 5 | * javac -cp \share\py4j\py4j0.10.6.jar Py4JWrapper.java 6 | * jar cfv py4j-wrapper.jar Py4JWrapper.class 7 | * 8 | */ 9 | import java.net.URL; 10 | import java.net.URLClassLoader; 11 | import java.net.MalformedURLException; 12 | 13 | import py4j.GatewayServer; 14 | 15 | public class Py4JWrapper { 16 | 17 | public static void main(String[] args) throws MalformedURLException { 18 | int port = Integer.parseInt(args[0]); 19 | URL url = new URL("file:" + args[1]); 20 | URLClassLoader loader = new URLClassLoader(new URL[]{url}); 21 | Thread.currentThread().setContextClassLoader(loader); 22 | GatewayServer server = new GatewayServer(null, port); 23 | server.start(); 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /src/msl/loadlib/__about__.py: -------------------------------------------------------------------------------- 1 | """Project information.""" 2 | 3 | from __future__ import annotations 4 | 5 | from ._version import __version__, version_tuple 6 | 7 | __author__: str = "Measurement Standards Laboratory of New Zealand" 8 | __copyright__: str = f"\xa9 2017 - 2025, {__author__}" 9 | 10 | __all__: list[str] = [ 11 | "__author__", 12 | "__copyright__", 13 | "__version__", 14 | "version_tuple", 15 | ] 16 | -------------------------------------------------------------------------------- /src/msl/loadlib/__init__.py: -------------------------------------------------------------------------------- 1 | """Load a library.""" 2 | 3 | from __future__ import annotations 4 | 5 | from .__about__ import __author__, __copyright__, __version__, version_tuple 6 | from ._constants import IS_PYTHON_64BIT 7 | from .client64 import Client64 8 | from .exceptions import ConnectionTimeoutError, ResponseTimeoutError, Server32Error 9 | from .load_library import LoadLibrary 10 | from .server32 import Server32 11 | from .utils import generate_com_wrapper, get_com_info 12 | 13 | __all__: list[str] = [ 14 | "IS_PYTHON_64BIT", 15 | "Client64", 16 | "ConnectionTimeoutError", 17 | "LoadLibrary", 18 | "ResponseTimeoutError", 19 | "Server32", 20 | "Server32Error", 21 | "__author__", 22 | "__copyright__", 23 | "__version__", 24 | "generate_com_wrapper", 25 | "get_com_info", 26 | "version_tuple", 27 | ] 28 | -------------------------------------------------------------------------------- /src/msl/loadlib/_constants.py: -------------------------------------------------------------------------------- 1 | """Package constants.""" 2 | 3 | # pyright: reportUnreachable=false 4 | from __future__ import annotations 5 | 6 | import sys 7 | 8 | __all__: list[str] = [ 9 | "IS_LINUX", 10 | "IS_MAC", 11 | "IS_PYTHON_64BIT", 12 | "IS_WINDOWS", 13 | "default_extension", 14 | "server_filename", 15 | ] 16 | 17 | IS_WINDOWS: bool = sys.platform == "win32" 18 | """Whether the operating system is Windows.""" 19 | 20 | IS_LINUX: bool = sys.platform.startswith("linux") 21 | """Whether the operating system is Linux.""" 22 | 23 | IS_MAC: bool = sys.platform == "darwin" 24 | """Whether the operating system is macOS.""" 25 | 26 | IS_PYTHON_64BIT: bool = sys.maxsize > 2**32 27 | """Whether the Python interpreter is 64-bits.""" 28 | 29 | server_filename: str 30 | default_extension: str 31 | if IS_WINDOWS: 32 | server_filename = "server32-windows.exe" 33 | default_extension = ".dll" 34 | elif IS_LINUX: 35 | server_filename = "server32-linux" 36 | default_extension = ".so" 37 | elif IS_MAC: 38 | server_filename = "server32-mac" 39 | default_extension = ".dylib" 40 | else: 41 | server_filename = "server32-unknown" 42 | default_extension = ".unknown" 43 | -------------------------------------------------------------------------------- /src/msl/loadlib/_types.py: -------------------------------------------------------------------------------- 1 | """Custom types.""" 2 | 3 | from __future__ import annotations 4 | 5 | import os 6 | from socket import socket as _socket # noqa: TC003 7 | from typing import Literal, Protocol, Union # pyright: ignore[reportDeprecated] 8 | 9 | LibType = Literal["cdll", "windll", "oledll", "net", "clr", "java", "com", "activex"] 10 | """Supported library types.""" 11 | 12 | PathLike = Union[str, bytes, os.PathLike[str], os.PathLike[bytes]] # pyright: ignore[reportDeprecated] 13 | """A [path-like object][]{:target="_blank"}.""" 14 | 15 | 16 | class Server32Subclass(Protocol): 17 | socket: _socket 18 | path: str 19 | 20 | def __init__(self, host: str | None, port: int, **kwargs: str) -> None: ... 21 | def serve_forever(self) -> None: ... 22 | def server_activate(self) -> None: ... 23 | def server_bind(self) -> None: ... 24 | def server_close(self) -> None: ... 25 | def shutdown(self) -> None: ... 26 | def shutdown_handler(self) -> None: ... 27 | -------------------------------------------------------------------------------- /src/msl/loadlib/exceptions.py: -------------------------------------------------------------------------------- 1 | """Exception classes.""" 2 | 3 | from __future__ import annotations 4 | 5 | from http.client import HTTPException 6 | 7 | 8 | class ConnectionTimeoutError(OSError): 9 | """Raised when the connection to the 32-bit server cannot be established.""" 10 | 11 | def __init__(self, message: str) -> None: 12 | """Raised when the connection to the 32-bit server cannot be established. 13 | 14 | Args: 15 | message: The error message. 16 | """ 17 | super().__init__(message) 18 | self.timeout_message: str = message 19 | self.reason: str = "" 20 | 21 | def __str__(self) -> str: 22 | """Returns the string representation.""" 23 | if self.reason: 24 | return f"{self.timeout_message}\n{self.reason}" 25 | return self.timeout_message 26 | 27 | 28 | class Server32Error(HTTPException): 29 | """Raised when an exception occurs on the 32-bit server.""" 30 | 31 | def __init__(self, value: str, *, name: str = "", traceback: str = "") -> None: 32 | """Raised when an exception occurs on the 32-bit server. 33 | 34 | Args: 35 | value: The error message. 36 | name: The name of the exception type. 37 | traceback: The exception traceback from the server. 38 | 39 | !!! note "Added in version 0.5" 40 | """ 41 | super().__init__(f"\n{traceback}\n{name}: {value}" if name else value) 42 | self._value: str = value 43 | self._name: str = name 44 | self._traceback: str = traceback 45 | 46 | @property 47 | def name(self) -> str: 48 | """[str][] — The name of the exception type.""" 49 | return self._name 50 | 51 | @property 52 | def traceback(self) -> str: 53 | """[str][] — The exception traceback from the server.""" 54 | return self._traceback 55 | 56 | @property 57 | def value(self) -> str: 58 | """[str][] — The error message.""" 59 | return self._value 60 | 61 | 62 | class ResponseTimeoutError(OSError): 63 | """Raised when a timeout occurs while waiting for a response from the 32-bit server. 64 | 65 | !!! note "Added in version 0.6" 66 | """ 67 | -------------------------------------------------------------------------------- /src/msl/loadlib/py.typed: -------------------------------------------------------------------------------- 1 | # Marker file for PEP 561 (inline types) -------------------------------------------------------------------------------- /src/msl/loadlib/py4j-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MSLNZ/msl-loadlib/c3f9e7e6f4d0c739e6af608bbbe865504030f503/src/msl/loadlib/py4j-wrapper.jar -------------------------------------------------------------------------------- /src/msl/loadlib/server32-linux: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MSLNZ/msl-loadlib/c3f9e7e6f4d0c739e6af608bbbe865504030f503/src/msl/loadlib/server32-linux -------------------------------------------------------------------------------- /src/msl/loadlib/server32-linux.config: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/msl/loadlib/server32-windows.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MSLNZ/msl-loadlib/c3f9e7e6f4d0c739e6af608bbbe865504030f503/src/msl/loadlib/server32-windows.exe -------------------------------------------------------------------------------- /src/msl/loadlib/server32-windows.exe.config: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /tests/bad_servers/bad_init_args.py: -------------------------------------------------------------------------------- 1 | from msl.loadlib import Server32 2 | 3 | 4 | class BadInitArgs(Server32): 5 | def __init__(self) -> None: # pyright: ignore[reportMissingSuperCall] 6 | pass 7 | -------------------------------------------------------------------------------- /tests/bad_servers/bad_init_args2.py: -------------------------------------------------------------------------------- 1 | from msl.loadlib import Server32 2 | 3 | 4 | class BadInitArgs2(Server32): 5 | def __init__(self, host: str, port: int, extra: int, **kwargs: str) -> None: # pyright: ignore[reportMissingSuperCall] 6 | pass 7 | -------------------------------------------------------------------------------- /tests/bad_servers/bad_lib_path.py: -------------------------------------------------------------------------------- 1 | from msl.loadlib import Server32 2 | 3 | 4 | class BadLibPath(Server32): 5 | def __init__(self, host: str, port: int, **kwargs: str) -> None: 6 | super().__init__("doesnotexist", "cdll", host, port, **kwargs) 7 | -------------------------------------------------------------------------------- /tests/bad_servers/bad_lib_type.py: -------------------------------------------------------------------------------- 1 | from msl.loadlib import Server32 2 | 3 | 4 | class BadLibType(Server32): 5 | def __init__(self, host: str, port: int, **kwargs: str) -> None: 6 | super().__init__("does_not_matter", "invalid", host, port, **kwargs) # type: ignore[arg-type] # pyright: ignore[reportArgumentType] 7 | -------------------------------------------------------------------------------- /tests/bad_servers/bad_super_init.py: -------------------------------------------------------------------------------- 1 | from msl.loadlib import Server32 2 | 3 | 4 | class BadSuperInit(Server32): 5 | def __init__(self, host: str, port: int, **kwargs: str) -> None: # noqa: ARG002 6 | super().__init__() # type: ignore[call-arg] # pyright: ignore[reportCallIssue] 7 | -------------------------------------------------------------------------------- /tests/bad_servers/import_error.py: -------------------------------------------------------------------------------- 1 | from msl.loadlib import Server32 # pyright: ignore[reportUnusedImport] # noqa: F401, I001 2 | 3 | import missing # type: ignore[import-not-found] # pyright: ignore[reportUnusedImport,reportMissingImports] # noqa: F401 4 | -------------------------------------------------------------------------------- /tests/bad_servers/no_init.py: -------------------------------------------------------------------------------- 1 | from msl.loadlib import Server32 2 | 3 | 4 | class NoInit(Server32): 5 | pass 6 | -------------------------------------------------------------------------------- /tests/bad_servers/no_server32_subclass.py: -------------------------------------------------------------------------------- 1 | from msl.loadlib import Server32 # pyright: ignore[reportUnusedImport] # noqa: F401 2 | 3 | value = "We imported Server32 but never made a subclass of it" 4 | -------------------------------------------------------------------------------- /tests/bad_servers/no_super.py: -------------------------------------------------------------------------------- 1 | from msl.loadlib import Server32 2 | 3 | 4 | class NoSuper(Server32): 5 | def __init__(self, host: str, port: int, **kwargs: str) -> None: # pyright: ignore[reportMissingSuperCall] 6 | pass 7 | -------------------------------------------------------------------------------- /tests/bad_servers/unexpected_error.py: -------------------------------------------------------------------------------- 1 | from msl.loadlib import Server32 2 | 3 | 4 | class UnexpectedError(Server32): 5 | def __init__(self, host: str, port: int, **kwargs: str) -> None: # pyright: ignore[reportMissingSuperCall] # noqa: ARG002 6 | _ = 1 / 0 7 | -------------------------------------------------------------------------------- /tests/bad_servers/unexpected_error2.py: -------------------------------------------------------------------------------- 1 | from msl.loadlib import Server32 2 | 3 | 4 | class UnexpectedError(Server32): 5 | def __init__(self, host: str, port: int, **kwargs: str) -> None: # pyright: ignore[reportMissingSuperCall] # noqa: ARG002 6 | _ = 1 + "hello" # type: ignore[operator] # pyright: ignore[reportOperatorIssue,reportUnknownVariableType] 7 | -------------------------------------------------------------------------------- /tests/bad_servers/unexpected_error3.py: -------------------------------------------------------------------------------- 1 | from msl.loadlib import Server32 # pyright: ignore[reportUnusedImport] # noqa: F401 2 | 3 | _ = 1 + y # type: ignore[name-defined] # pyright: ignore[reportUndefinedVariable,reportUnknownVariableType] # noqa: F821 4 | -------------------------------------------------------------------------------- /tests/bad_servers/wrong_bitness.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from msl.loadlib import Server32 4 | 5 | 6 | class WrongBitness(Server32): 7 | def __init__(self, host: str, port: int, **kwargs: str) -> None: 8 | path = os.path.join(Server32.examples_dir(), "cpp_lib64") # noqa: PTH118 9 | super().__init__(path, "cdll", host, port, **kwargs) 10 | -------------------------------------------------------------------------------- /tests/check_pythonw.py: -------------------------------------------------------------------------------- 1 | r"""Checks that running a script with pythonw.exe does not do the following. 2 | 3 | 1) create a new console 4 | 2) the console that pythonw.exe is executing in does not "flash" 5 | 6 | If the script executes successfully, a new file \tests\check_pythonw.txt is created. 7 | 8 | See Issue #31 https://github.com/MSLNZ/msl-loadlib/issues/31 9 | 10 | Usage 11 | 12 | .\.venv\Scripts\pythonw.exe .\tests\check_pythonw.py 13 | 14 | """ 15 | 16 | import sys 17 | from pathlib import Path 18 | 19 | from msl.examples.loadlib import EXAMPLES_DIR, Cpp64 20 | from msl.loadlib import LoadLibrary 21 | 22 | if Path(sys.executable).name != "pythonw.exe": 23 | raise RuntimeError("Must run this script using,\n pythonw.exe " + __file__) 24 | 25 | sys.stdout = open(f"{__file__[:-3]}.txt", mode="w") # noqa: PTH123, SIM115 26 | sys.stderr = sys.stdout 27 | 28 | with LoadLibrary(EXAMPLES_DIR / "Trig.class") as java: 29 | print(java) # noqa: T201 30 | 31 | with Cpp64() as cpp: 32 | print(cpp) # noqa: T201 33 | 34 | print("You should delete this file") # noqa: T201 35 | -------------------------------------------------------------------------------- /tests/dotnet_config/invalid_xml.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MSLNZ/msl-loadlib/c3f9e7e6f4d0c739e6af608bbbe865504030f503/tests/dotnet_config/invalid_xml.exe -------------------------------------------------------------------------------- /tests/dotnet_config/invalid_xml.exe.config: -------------------------------------------------------------------------------- 1 | this is not an xml file! -------------------------------------------------------------------------------- /tests/dotnet_config/legacy_v2_runtime.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from pathlib import Path 3 | 4 | import clr # type: ignore[import-untyped] # pyright: ignore[reportUnusedImport,reportMissingTypeStubs] # noqa: F401 5 | 6 | from msl.loadlib import IS_PYTHON_64BIT, LoadLibrary 7 | 8 | bitness = "x64" if IS_PYTHON_64BIT else "x86" 9 | filename = f"legacy_v2_runtime_{bitness}.dll" 10 | path = Path(__file__).parent / "legacy_v2_runtime" / filename 11 | 12 | net = LoadLibrary(path, libtype="net") 13 | 14 | expected = "Microsoft Visual Studio 2005 (Version 8.0.50727.42); Microsoft .NET Framework (Version 2.0.50727)" 15 | environment = net.lib.legacy.Build().Environment() 16 | if environment != expected: 17 | sys.exit(f"{environment!r} != {expected!r}") 18 | 19 | print("SUCCESS") # noqa: T201 20 | -------------------------------------------------------------------------------- /tests/dotnet_config/legacy_v2_runtime/AssemblyInfo.cpp: -------------------------------------------------------------------------------- 1 | using namespace System; 2 | using namespace System::Reflection; 3 | using namespace System::Runtime::CompilerServices; 4 | using namespace System::Runtime::InteropServices; 5 | using namespace System::Security::Permissions; 6 | 7 | [assembly:AssemblyTitleAttribute("legacy_v2_runtime")]; 8 | [assembly:AssemblyDescriptionAttribute("")]; 9 | [assembly:AssemblyConfigurationAttribute("")]; 10 | [assembly:AssemblyCompanyAttribute("")]; 11 | [assembly:AssemblyProductAttribute("legacy_v2_runtime")]; 12 | [assembly:AssemblyCopyrightAttribute("")]; 13 | [assembly:AssemblyTrademarkAttribute("")]; 14 | [assembly:AssemblyCultureAttribute("")]; 15 | [assembly:AssemblyVersionAttribute("1.0.0.0")]; 16 | [assembly:ComVisible(false)]; 17 | [assembly:CLSCompliantAttribute(true)]; 18 | [assembly:SecurityPermission(SecurityAction::RequestMinimum, UnmanagedCode = true)]; 19 | -------------------------------------------------------------------------------- /tests/dotnet_config/legacy_v2_runtime/legacy_v2_runtime.cpp: -------------------------------------------------------------------------------- 1 | using namespace System; 2 | 3 | namespace legacy { 4 | 5 | public ref class Build 6 | { 7 | public: 8 | System::String^ Environment() 9 | { 10 | return "Microsoft Visual Studio 2005 (Version 8.0.50727.42); Microsoft .NET Framework (Version 2.0.50727)"; 11 | } 12 | }; 13 | } 14 | -------------------------------------------------------------------------------- /tests/dotnet_config/legacy_v2_runtime/legacy_v2_runtime.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 9.00 3 | # Visual Studio 2005 4 | Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "legacy_v2_runtime", "legacy_v2_runtime.vcproj", "{74A98B97-FD5C-43AD-952C-213B56ADE55E}" 5 | EndProject 6 | Global 7 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 8 | Debug|Win32 = Debug|Win32 9 | Debug|x64 = Debug|x64 10 | Release|Win32 = Release|Win32 11 | Release|x64 = Release|x64 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {74A98B97-FD5C-43AD-952C-213B56ADE55E}.Debug|Win32.ActiveCfg = Debug|Win32 15 | {74A98B97-FD5C-43AD-952C-213B56ADE55E}.Debug|Win32.Build.0 = Debug|Win32 16 | {74A98B97-FD5C-43AD-952C-213B56ADE55E}.Debug|x64.ActiveCfg = Debug|x64 17 | {74A98B97-FD5C-43AD-952C-213B56ADE55E}.Debug|x64.Build.0 = Debug|x64 18 | {74A98B97-FD5C-43AD-952C-213B56ADE55E}.Release|Win32.ActiveCfg = Release|Win32 19 | {74A98B97-FD5C-43AD-952C-213B56ADE55E}.Release|Win32.Build.0 = Release|Win32 20 | {74A98B97-FD5C-43AD-952C-213B56ADE55E}.Release|x64.ActiveCfg = Release|x64 21 | {74A98B97-FD5C-43AD-952C-213B56ADE55E}.Release|x64.Build.0 = Release|x64 22 | EndGlobalSection 23 | GlobalSection(SolutionProperties) = preSolution 24 | HideSolutionNode = FALSE 25 | EndGlobalSection 26 | EndGlobal 27 | -------------------------------------------------------------------------------- /tests/dotnet_config/legacy_v2_runtime/legacy_v2_runtime_x64.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MSLNZ/msl-loadlib/c3f9e7e6f4d0c739e6af608bbbe865504030f503/tests/dotnet_config/legacy_v2_runtime/legacy_v2_runtime_x64.dll -------------------------------------------------------------------------------- /tests/dotnet_config/legacy_v2_runtime/legacy_v2_runtime_x86.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MSLNZ/msl-loadlib/c3f9e7e6f4d0c739e6af608bbbe865504030f503/tests/dotnet_config/legacy_v2_runtime/legacy_v2_runtime_x86.dll -------------------------------------------------------------------------------- /tests/dotnet_config/no_config_exists.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MSLNZ/msl-loadlib/c3f9e7e6f4d0c739e6af608bbbe865504030f503/tests/dotnet_config/no_config_exists.exe -------------------------------------------------------------------------------- /tests/dotnet_config/root_tag_is_not_configuration.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MSLNZ/msl-loadlib/c3f9e7e6f4d0c739e6af608bbbe865504030f503/tests/dotnet_config/root_tag_is_not_configuration.exe -------------------------------------------------------------------------------- /tests/dotnet_config/root_tag_is_not_configuration.exe.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /tests/dotnet_config/set_to_false.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MSLNZ/msl-loadlib/c3f9e7e6f4d0c739e6af608bbbe865504030f503/tests/dotnet_config/set_to_false.exe -------------------------------------------------------------------------------- /tests/dotnet_config/set_to_false.exe.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /tests/dotnet_config/startup_element_does_not_exist.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MSLNZ/msl-loadlib/c3f9e7e6f4d0c739e6af608bbbe865504030f503/tests/dotnet_config/startup_element_does_not_exist.exe -------------------------------------------------------------------------------- /tests/namespace_with_dots/Namespace.With.Dots.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | // Test case for Issue #7 8 | namespace Namespace.With.Dots 9 | { 10 | public class Checker 11 | { 12 | public bool IsSuccess() 13 | { 14 | return true; 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /tests/namespace_with_dots/Namespace.With.Dots.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {046D6BE4-95F6-43B3-B883-9823B67A2D1A} 8 | Library 9 | Properties 10 | Namespace.With.Dots 11 | Namespace.With.Dots 12 | v4.6.1 13 | 512 14 | 15 | 16 | true 17 | full 18 | false 19 | bin\Debug\ 20 | DEBUG;TRACE 21 | prompt 22 | 4 23 | 24 | 25 | pdbonly 26 | true 27 | bin\Release\ 28 | TRACE 29 | prompt 30 | 4 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /tests/namespace_with_dots/Namespace.With.Dots.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MSLNZ/msl-loadlib/c3f9e7e6f4d0c739e6af608bbbe865504030f503/tests/namespace_with_dots/Namespace.With.Dots.dll -------------------------------------------------------------------------------- /tests/nested_namespaces/nested_namespaces.cs: -------------------------------------------------------------------------------- 1 | namespace A 2 | { 3 | namespace B 4 | { 5 | namespace C 6 | { 7 | public class Klass 8 | { 9 | public string Message() 10 | { 11 | return "Hello from A.B.C.Klass().Message()"; 12 | } 13 | } 14 | 15 | public enum ErrorCode : ushort 16 | { 17 | Unknown, 18 | ConnectionLost = 100, 19 | OutlierReading = 200 20 | } 21 | 22 | public class Subtracter 23 | { 24 | public int x; 25 | public int y; 26 | 27 | public Subtracter(int x, int y) 28 | { 29 | this.x = x; 30 | this.y = y; 31 | } 32 | 33 | public int Subtract() 34 | { 35 | return x - y; 36 | } 37 | } 38 | 39 | public struct Point 40 | { 41 | public int X, Y; 42 | 43 | public Point(int x, int y) 44 | { 45 | X = x; 46 | Y = y; 47 | } 48 | 49 | public override string ToString() => $"Point"; 50 | } 51 | } 52 | public class Klass 53 | { 54 | public string Message() 55 | { 56 | return "Hello from A.B.Klass().Message()"; 57 | } 58 | } 59 | } 60 | 61 | public class Klass 62 | { 63 | public string Message() 64 | { 65 | return "Hello from A.Klass().Message()"; 66 | } 67 | } 68 | 69 | } 70 | 71 | public class Messenger 72 | { 73 | public string Message() 74 | { 75 | return "Hello from Messenger.Message()"; 76 | } 77 | } 78 | 79 | class Adder 80 | { 81 | public int x; 82 | public int y; 83 | 84 | public Adder(int x, int y) 85 | { 86 | this.x = x; 87 | this.y = y; 88 | } 89 | 90 | public int Add() 91 | { 92 | return x + y; 93 | } 94 | 95 | } 96 | 97 | namespace Foo.Bar 98 | { 99 | public class Baz 100 | { 101 | public string message; 102 | 103 | public Baz(string msg) 104 | { 105 | message = msg; 106 | } 107 | 108 | public string Message() 109 | { 110 | return message; 111 | } 112 | } 113 | } 114 | 115 | public enum Season 116 | { 117 | Winter, 118 | Spring, 119 | Summer, 120 | Autumn 121 | } 122 | 123 | public struct Point 124 | { 125 | 126 | public Point(int x, int y) 127 | { 128 | X = x; 129 | Y = y; 130 | } 131 | 132 | public int X { get; } 133 | public int Y { get; } 134 | 135 | } 136 | 137 | public struct StructWithoutConstructor 138 | { 139 | public int X; 140 | public int Y; 141 | } 142 | -------------------------------------------------------------------------------- /tests/nested_namespaces/nested_namespaces.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard1.4 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /tests/nested_namespaces/nested_namespaces.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MSLNZ/msl-loadlib/c3f9e7e6f4d0c739e6af608bbbe865504030f503/tests/nested_namespaces/nested_namespaces.dll -------------------------------------------------------------------------------- /tests/server32_comtypes/activex_media_player.py: -------------------------------------------------------------------------------- 1 | from msl.loadlib import LoadLibrary, Server32 2 | 3 | # comtypes on the 32-bit server will try to import numpy 4 | # remove site-packages before importing msl.loadlib.activex 5 | if Server32.is_interpreter(): 6 | _ = Server32.remove_site_packages_64bit() 7 | 8 | from msl.loadlib.activex import Application 9 | 10 | prog_id = "MediaPlayer.MediaPlayer.1" 11 | 12 | 13 | class ActiveX(Server32): 14 | def __init__(self, host: str, port: int) -> None: 15 | super().__init__(prog_id, "activex", host, port) 16 | self._a: Application = Application() 17 | 18 | def this(self) -> bool: 19 | enabled: bool = self.lib.IsSoundCardEnabled() 20 | return enabled 21 | 22 | def reload(self) -> bool: 23 | enabled: bool = self._a.load(prog_id).IsSoundCardEnabled() 24 | return enabled 25 | 26 | @staticmethod 27 | def load_library() -> bool: 28 | enabled: bool = LoadLibrary(prog_id, "activex").lib.IsSoundCardEnabled() 29 | return enabled 30 | 31 | def error1(self) -> str: 32 | try: 33 | self._a.load("ABC.DEF.GHI") 34 | except OSError as e: 35 | return str(e) 36 | else: 37 | msg = "Did not raise OSError" 38 | raise OSError(msg) 39 | 40 | def error2(self) -> str: 41 | try: 42 | _ = LoadLibrary("ABC.DEF.GHI", "activex") 43 | except OSError as e: 44 | return str(e) 45 | else: 46 | msg = "Did not raise OSError" 47 | raise OSError(msg) 48 | -------------------------------------------------------------------------------- /tests/server32_comtypes/ctypes_union_error.py: -------------------------------------------------------------------------------- 1 | # Changes to ctypes in Python 3.7.6 and 3.8.1 caused the following exception 2 | # TypeError: item 1 in _argtypes_ passes a union by value, which is unsupported. 3 | # when loading some COM objects, see https://bugs.python.org/issue16575 4 | # 5 | # Want to make sure that the Python interpreter that the server32-windows.exe 6 | # is running on does not raise this TypeError 7 | import os 8 | import sys 9 | import tempfile 10 | 11 | from msl.loadlib import Server32 12 | 13 | 14 | class FileSystemObjectServer(Server32): 15 | def __init__(self, host: str, port: int) -> None: 16 | # comtypes will try to import numpy to see if it is available. 17 | # Since Client64 passes its sys.path to Server32 the modules that 18 | # are available to Client64 to import are also available to Server32. 19 | # Therefore, we don't want this test to fail because the Python 20 | # environment that is running Client64 has numpy installed. 21 | # (This only appeared to be an issue when Client64 runs on Python 3.5) 22 | path = Server32.remove_site_packages_64bit() 23 | 24 | super().__init__("Scripting.FileSystemObject", "com", host, port) 25 | 26 | # put 'site-packages' back in 27 | if path: 28 | sys.path.append(path) 29 | 30 | self.temp_file: str = os.path.join(tempfile.gettempdir(), "msl-loadlib-FileSystemObject.txt") # noqa: PTH118 31 | 32 | def get_temp_file(self) -> str: 33 | return self.temp_file 34 | 35 | def create_and_write(self, text: str) -> None: 36 | fp = self.lib.CreateTextFile(self.temp_file) 37 | fp.WriteLine(text) 38 | fp.Close() 39 | -------------------------------------------------------------------------------- /tests/server32_comtypes/shell32.py: -------------------------------------------------------------------------------- 1 | # When the 32-bit server was running in Python 3.9.1 loading 2 | # Scripting.FileSystemObject (in ctypes_union_error.py) raised 3 | # errors from comtypes that were not related to the ctypes error. 4 | # 5 | # This test class can be used to replace ctypes_union_error.py 6 | # to test that comtypes can load a library on the 32-bit server. 7 | # We don't want to test the code of comtypes just MSL-LoadLib 8 | # 9 | 10 | from __future__ import annotations 11 | 12 | from typing import Callable 13 | 14 | from msl.loadlib import Server32 15 | 16 | 17 | class Shell32(Server32): 18 | def __init__(self, host: str, port: int) -> None: 19 | # comtypes will try to import numpy to see if it is available. 20 | # Since Client64 passes its sys.path to Server32 the modules that 21 | # are available to Client64 to import are also available to Server32. 22 | # Therefore, we don't want this test to fail because the Python 23 | # environment that is running Client64 has numpy installed. 24 | # (This only appeared to be an issue when Client64 runs on Python 3.5) 25 | _ = Server32.remove_site_packages_64bit() 26 | 27 | super().__init__("WScript.Shell", "com", host, port) 28 | 29 | self._environ: Callable[[str], str] = self.lib.Environment("System") 30 | 31 | def environ(self, name: str) -> str: 32 | value: str = self._environ(name) 33 | return value 34 | -------------------------------------------------------------------------------- /tests/sew_eurodrive/FirstDll.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MSLNZ/msl-loadlib/c3f9e7e6f4d0c739e6af608bbbe865504030f503/tests/sew_eurodrive/FirstDll.dll -------------------------------------------------------------------------------- /tests/sew_eurodrive/SecondDll.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MSLNZ/msl-loadlib/c3f9e7e6f4d0c739e6af608bbbe865504030f503/tests/sew_eurodrive/SecondDll.dll -------------------------------------------------------------------------------- /tests/sew_eurodrive/sew32.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | from msl.loadlib import Server32 4 | 5 | 6 | class SEWEuroDrive32(Server32): 7 | def __init__(self, host: str, port: int) -> None: 8 | path = Path(__file__).parent / "FirstDll.dll" 9 | super().__init__(path, "clr", host, port) 10 | self.first_class = self.lib.FirstDll.FirstClass() # pyright: ignore[reportUnannotatedClassAttribute] 11 | 12 | def do_something(self, value: float) -> float: 13 | # returns "value/3/2" 14 | result: float = self.first_class.DoSometing(value) # cSpell: ignore Someting 15 | return result 16 | -------------------------------------------------------------------------------- /tests/test_application.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | import pytest 4 | 5 | from conftest import IS_WINDOWS, skipif_not_windows 6 | from msl.loadlib import activex 7 | 8 | 9 | def test_menu_item() -> None: 10 | mi = activex.MenuItem(hmenu=123, id=7, text="hello world", callback=None, flags=1, data=[-1, 0, 1]) 11 | assert mi.hmenu == 123 12 | assert mi.id == 7 13 | assert mi.checked is False 14 | assert mi.text == "hello world" 15 | assert mi.callback is None 16 | assert mi.flags == 1 17 | assert mi == mi # noqa: PLR0124 18 | assert mi is mi # noqa: PLR0124 19 | assert str(mi) == "" 20 | assert mi.data == [-1, 0, 1] 21 | 22 | mi2 = activex.MenuItem(hmenu=123, id=8, text="hello world", callback=None, flags=1, data=[-1, 0, 1]) 23 | assert mi != mi2 # IDs do not match 24 | 25 | mi3 = activex.MenuItem(hmenu=-1, id=1, text="a", callback=None, flags=0, data=None) 26 | 27 | with pytest.raises(ValueError, match="A MenuItem must first be added to a Menu"): 28 | mi3.checked = True 29 | 30 | 31 | def test_menu_group() -> None: 32 | mg = activex.MenuGroup() 33 | assert mg.name == "" 34 | assert mg.checked is None 35 | assert str(mg) == "" 36 | 37 | mg = activex.MenuGroup("click") 38 | a = mg.append("a") 39 | assert isinstance(a, activex.MenuItem) 40 | _ = mg.append("b", data=8, flags=activex.MenuFlag.POPUP) 41 | mg.append_separator() 42 | _ = mg.append("c", callback=None) 43 | assert mg.name == "click" 44 | assert mg.checked is None 45 | assert str(mg) == "" 46 | 47 | for item in mg: 48 | assert isinstance(item, activex.MenuItem) 49 | assert item.checked is False 50 | 51 | for item in [a, None]: # type: ignore[assignment] 52 | with pytest.raises(ValueError, match="A MenuGroup must first be added to a Menu"): 53 | mg.checked = item 54 | 55 | 56 | @skipif_not_windows 57 | def test_menu() -> None: # noqa: PLR0915 58 | m = activex.Menu() 59 | assert m.hmenu > 0 60 | 61 | h_file = m.create("File") 62 | assert h_file > 0 63 | 64 | new = m.append(h_file, "New", data=-1) 65 | assert isinstance(new, activex.MenuItem) 66 | assert m[new.id] is new 67 | assert new.id == 1 68 | assert new.text == "New" 69 | assert new.flags == activex.MenuFlag.STRING 70 | assert new.callback is None 71 | assert new.data == -1 72 | assert new.checked is False 73 | 74 | m.append_separator(h_file) 75 | 76 | h_settings = m.create("Settings") 77 | 78 | group = activex.MenuGroup("Alphabet") 79 | a = group.append("A", data="a") 80 | assert isinstance(a, activex.MenuItem) 81 | b = group.append("B") 82 | group.append_separator() 83 | c = group.append("C") 84 | 85 | m.append_group(h_settings, group) 86 | 87 | assert m[a.id] is a 88 | assert a.id == 3 # adding a separator (before creating 'Settings') increments the ID counter 89 | assert a.text == "A" 90 | assert a.flags == activex.MenuFlag.STRING 91 | assert a.callback is None 92 | assert a.data == "a" 93 | assert a.checked is False 94 | 95 | assert m[b.id] is b 96 | assert b.id == 4 97 | assert b.text == "B" 98 | assert b.data is None 99 | 100 | assert m[c.id] is c 101 | assert c.id == 6 # adding a separator (in the group) increments the ID counter 102 | assert c.text == "C" 103 | 104 | with pytest.raises(KeyError): 105 | _ = m[c.id + 1] 106 | 107 | assert group.checked is None 108 | assert a.checked is False 109 | assert b.checked is False 110 | assert c.checked is False 111 | 112 | group.checked = a 113 | assert group.checked is a 114 | assert a.checked is True 115 | assert b.checked is False # type: ignore[unreachable] 116 | assert c.checked is False 117 | 118 | group.checked = b 119 | assert group.checked is b 120 | assert a.checked is False 121 | assert b.checked is True 122 | assert c.checked is False 123 | 124 | group.checked = c 125 | assert group.checked is c 126 | assert a.checked is False 127 | assert b.checked is False 128 | assert c.checked is True 129 | 130 | group.checked = None 131 | assert group.checked is None 132 | assert a.checked is False 133 | assert b.checked is False 134 | assert c.checked is False 135 | 136 | 137 | @pytest.mark.filterwarnings(pytest.PytestUnhandledThreadExceptionWarning) # type: ignore[arg-type] # pyright: ignore[reportArgumentType] 138 | @skipif_not_windows 139 | def test_application() -> None: 140 | app = activex.Application() 141 | assert app.hwnd > 0 142 | assert app.thread_id > 0 143 | assert isinstance(app.menu, activex.Menu) 144 | app.set_window_position(10, 0, 100, 250) 145 | app.set_window_size(100, 100) 146 | app.set_window_title("new title") 147 | 148 | # ok to call close() multiple times 149 | app.close() 150 | app.close() 151 | app.close() 152 | app.close() 153 | 154 | 155 | @pytest.mark.skipif(IS_WINDOWS, reason="do not test on Windows") 156 | def test_application_raises() -> None: 157 | with pytest.raises(OSError, match="not supported on this platform"): 158 | _ = activex.Application() 159 | 160 | 161 | @skipif_not_windows 162 | def test_icon() -> None: 163 | icon = activex.Icon("does not exist") 164 | assert str(icon) == "" 165 | assert icon.hicon is None 166 | 167 | # ok to call destroy() multiple times 168 | icon.destroy() 169 | icon.destroy() 170 | icon.destroy() 171 | icon.destroy() 172 | 173 | with pytest.raises(ValueError, match="negative index"): 174 | _ = activex.Icon("", index=-1) 175 | 176 | icon = activex.Icon(sys.executable) 177 | assert icon.hicon is not None 178 | assert icon.hicon > 0 179 | icon.destroy() 180 | -------------------------------------------------------------------------------- /tests/test_client64.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import gc 4 | import os.path 5 | from datetime import datetime 6 | from pathlib import Path 7 | 8 | import pytest 9 | 10 | from conftest import skipif_no_server32, skipif_not_windows 11 | from msl.examples.loadlib import EXAMPLES_DIR, Cpp64 12 | from msl.loadlib import Client64 13 | from msl.loadlib._constants import server_filename 14 | from msl.loadlib.client64 import _build_paths # pyright: ignore[reportPrivateUsage] 15 | 16 | 17 | @skipif_no_server32 18 | def test_unclosed_warnings_1(recwarn: pytest.WarningsRecorder) -> None: 19 | # recwarn is a built-in pytest fixture that records all warnings emitted by test functions 20 | 21 | # The following warnings should not be written to stderr for the unclosed subprocess PIPE's 22 | # sys:1: ResourceWarning: unclosed file <_io.BufferedReader name=3> 23 | # sys:1: ResourceWarning: unclosed file <_io.BufferedReader name=4> 24 | # nor for unclosed sockets 25 | # ResourceWarning: unclosed 26 | Cpp64() # pyright: ignore[reportUnusedCallResult] 27 | _ = gc.collect() 28 | assert len(recwarn) == 0 29 | 30 | 31 | @skipif_no_server32 32 | def test_unclosed_warnings_2(recwarn: pytest.WarningsRecorder) -> None: 33 | for _ in range(3): 34 | cpp = Cpp64() 35 | out, err = cpp.shutdown_server32() 36 | for _ in range(10): 37 | out.close() 38 | err.close() 39 | del cpp 40 | _ = gc.collect() 41 | assert len(recwarn) == 0 42 | 43 | 44 | def test_bad_del() -> None: 45 | # Make sure that the following exception is not raised in Client64.__del__ 46 | # AttributeError: 'BadDel' object has no attribute '_client' 47 | 48 | class BadDel(Client64): 49 | def __init__(self) -> None: # pyright: ignore[reportMissingSuperCall] 50 | pass 51 | 52 | b = BadDel() 53 | b.__del__() 54 | del b 55 | 56 | with BadDel(): 57 | pass 58 | 59 | # this should raise AttributeError because super() was not called in BadDel 60 | with pytest.raises(AttributeError, match="_client"): 61 | BadDel().request32("request") 62 | 63 | 64 | def test_invalid_server32_dir() -> None: 65 | with pytest.raises(OSError, match=rf"^Cannot find {server_filename}$"): 66 | _ = Client64(__file__, server32_dir="") 67 | 68 | 69 | @skipif_no_server32 70 | @skipif_not_windows 71 | def test_module32_as_name() -> None: 72 | # Load the kernel32 module because it uses an absolute path to kernel32.dll 73 | # Trying to load another module will look for the shared library in the tmp/_MEI* 74 | # directory since Path(__file__).parent is used to locate the shared library 75 | with Client64("msl.examples.loadlib.kernel32") as c: 76 | assert isinstance(c.request32("get_time"), datetime) 77 | 78 | 79 | @skipif_no_server32 80 | @skipif_not_windows 81 | def test_module32_as_path() -> None: 82 | # Load the kernel32 file because it uses an absolute path to kernel32.dll 83 | # Trying to load another module will look for the shared library in the tmp/_MEI* 84 | # directory since Path(__file__).parent is used to locate the shared library 85 | path = EXAMPLES_DIR / "kernel32.py" 86 | assert path.is_file() 87 | client = Client64(path) 88 | assert isinstance(client.request32("get_local_time"), datetime) 89 | 90 | 91 | def test_build_paths_none() -> None: 92 | paths = _build_paths(None) 93 | assert isinstance(paths, list) 94 | assert len(paths) == 0 95 | 96 | 97 | class BytesPath: 98 | def __init__(self, path: bytes) -> None: 99 | self._path: bytes = path 100 | 101 | def __fspath__(self) -> bytes: 102 | return self._path 103 | 104 | 105 | @pytest.mark.parametrize("path", ["here", b"here", Path("here"), BytesPath(b"here")]) 106 | def test_build_paths_single(path: str | bytes | Path | BytesPath) -> None: 107 | assert _build_paths(path) == [os.path.join(os.getcwd(), "here")] # noqa: PTH109, PTH118 108 | 109 | 110 | def test_build_paths_iterable() -> None: 111 | cwd = os.getcwd() # noqa: PTH109 112 | paths: list[str | bytes | Path | BytesPath] = ["a", b"b", Path("c"), BytesPath(b"d")] 113 | assert _build_paths(paths) == [ 114 | os.path.join(cwd, "a"), # noqa: PTH118 115 | os.path.join(cwd, "b"), # noqa: PTH118 116 | os.path.join(cwd, "c"), # noqa: PTH118 117 | os.path.join(cwd, "d"), # noqa: PTH118 118 | ] 119 | 120 | 121 | def test_build_paths_ignore() -> None: 122 | cwd = os.getcwd() # noqa: PTH109 123 | paths: list[str | bytes | Path | BytesPath] = ["a", b"b", Path("c"), BytesPath(b"d")] 124 | assert _build_paths(paths, ignore=[os.path.join(cwd, "c")]) == [ # noqa: PTH118 125 | os.path.join(cwd, "a"), # noqa: PTH118 126 | os.path.join(cwd, "b"), # noqa: PTH118 127 | os.path.join(cwd, "d"), # noqa: PTH118 128 | ] 129 | -------------------------------------------------------------------------------- /tests/test_context_manager.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from ctypes import CDLL 3 | 4 | import pytest 5 | from py4j.java_gateway import ( # type: ignore[import-untyped] # pyright: ignore[reportMissingTypeStubs] 6 | JavaGateway, # pyright: ignore[reportUnknownVariableType] 7 | JVMView, # pyright: ignore[reportUnknownVariableType] 8 | ) 9 | 10 | from conftest import IS_MAC_ARM64, skipif_no_pythonnet, skipif_no_server32 11 | from msl.examples.loadlib import EXAMPLES_DIR, Cpp64 12 | from msl.loadlib import IS_PYTHON_64BIT, LoadLibrary 13 | from msl.loadlib._constants import default_extension 14 | from msl.loadlib.load_library import DotNet 15 | from msl.loadlib.utils import logger 16 | 17 | suffix = "arm64" if IS_MAC_ARM64 else "64" if IS_PYTHON_64BIT else "32" 18 | 19 | 20 | def test_raises() -> None: 21 | with pytest.raises(OSError, match=r"^Cannot find"): # noqa: SIM117 22 | with LoadLibrary("doesnotexist"): 23 | pass 24 | 25 | with pytest.raises(ValueError, match=r"^Invalid libtype"): # noqa: SIM117 26 | with LoadLibrary(EXAMPLES_DIR / "Trig.class", libtype="invalid"): # type: ignore[arg-type] # pyright: ignore[reportArgumentType] 27 | pass 28 | 29 | 30 | def test_cpp() -> None: 31 | path = str(EXAMPLES_DIR / f"cpp_lib{suffix}{default_extension}") 32 | with LoadLibrary(path) as library: 33 | assert library.assembly is None 34 | assert library.gateway is None 35 | assert library.path == path 36 | assert isinstance(library.lib, CDLL) 37 | assert library.lib.add(1, 2) == 3 38 | assert library.path == path 39 | assert library.assembly is None 40 | assert library.gateway is None 41 | assert library.lib is None 42 | 43 | # can still call this (even multiple times) 44 | for _ in range(10): # type: ignore[unreachable] 45 | library.cleanup() 46 | 47 | assert "libtype=NoneType" in str(library) 48 | assert "libtype=NoneType" in repr(library) 49 | 50 | 51 | @skipif_no_pythonnet 52 | def test_dotnet() -> None: 53 | path = str(EXAMPLES_DIR / f"dotnet_lib{suffix}.dll") 54 | with LoadLibrary(path, libtype="net") as library: 55 | assert isinstance(library.assembly, library.lib.System.Reflection.Assembly) 56 | assert library.assembly is not None 57 | assert library.gateway is None 58 | assert library.path == path 59 | assert isinstance(library.lib, DotNet) 60 | assert library.lib.DotNetMSL.BasicMath().add_integers(1, 2) == 3 # type: ignore[attr-defined] # pyright: ignore[reportUnknownMemberType,reportAttributeAccessIssue] 61 | assert library.path == path 62 | assert library.assembly is None 63 | assert library.gateway is None 64 | assert library.lib is None 65 | 66 | # can still call this (even multiple times) 67 | for _ in range(10): # type: ignore[unreachable] 68 | library.cleanup() 69 | 70 | assert "libtype=NoneType" in str(library) 71 | assert "libtype=NoneType" in repr(library) 72 | 73 | 74 | def test_java(caplog: pytest.LogCaptureFixture) -> None: 75 | caplog.set_level(logging.DEBUG, logger.name) 76 | path = str(EXAMPLES_DIR / "Trig.class") 77 | with LoadLibrary(path) as library: 78 | assert library.assembly is None 79 | assert isinstance(library.gateway, JavaGateway) 80 | assert library.path == path 81 | assert isinstance(library.lib, JVMView) 82 | assert library.lib.Trig.cos(0.0) == 1.0 83 | assert library.path == path 84 | assert library.assembly is None 85 | assert library.gateway is None 86 | assert library.lib is None 87 | 88 | record = caplog.records[0] 89 | assert record.levelname == "DEBUG" 90 | assert record.msg == "Loaded %s" 91 | 92 | record = caplog.records[1] 93 | assert record.levelname == "DEBUG" 94 | assert record.msg == "shutdown Py4J.GatewayServer" 95 | 96 | # can still call this (even multiple times) 97 | for _ in range(10): 98 | library.cleanup() 99 | 100 | assert "libtype=NoneType" in str(library) 101 | assert "libtype=NoneType" in repr(library) 102 | 103 | 104 | @skipif_no_server32 105 | def test_client() -> None: 106 | with Cpp64() as cpp: 107 | assert cpp.connection is not None 108 | assert cpp.add(1, -1) == 0 109 | assert cpp.connection is None 110 | 111 | # can still call this (even multiple times) 112 | for _ in range(10): # type: ignore[unreachable] 113 | out, err = cpp.shutdown_server32() 114 | assert out.closed 115 | assert err.closed 116 | 117 | with Cpp64() as cpp: 118 | out, err = cpp.shutdown_server32() 119 | assert not out.closed 120 | assert not err.closed 121 | assert out.read() == b"" 122 | assert err.read() == b"" 123 | out, err = cpp.shutdown_server32() 124 | assert out.closed 125 | assert err.closed 126 | 127 | assert str(cpp).endswith("address=None (closed)>") 128 | assert repr(cpp).endswith("address=None (closed)>") 129 | -------------------------------------------------------------------------------- /tests/test_dotnet_legacy_v2_runtime.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from pathlib import Path 3 | from subprocess import PIPE, Popen 4 | 5 | import pytest 6 | 7 | from conftest import skipif_no_pythonnet, skipif_not_windows 8 | from msl.loadlib import LoadLibrary 9 | 10 | config_path = Path(sys.base_prefix) / f"{Path(sys.executable).name}.config" 11 | 12 | 13 | def teardown_module() -> None: 14 | config_path.unlink(missing_ok=True) 15 | 16 | 17 | @skipif_not_windows 18 | @skipif_no_pythonnet 19 | def test_framework_v2() -> None: 20 | # The python.exe.config file must exist before the Python interpreter 21 | # starts in order for pythonnet to load a library from .NET <4.0. That 22 | # is why we must use subprocess for this test. The 'legacy_v2_runtime.py' 23 | # script must be run in a different Python process which depends on 24 | # whether python.exe.config exists before the subprocess runs. 25 | # 26 | # This test also requires that the .NET Framework 3.5 is installed. 27 | # If it is not, this test will fail with a missing-dependency error: 28 | # 29 | # System.IO.FileLoadException: Could not load file or assembly 30 | # 'legacy_v2_runtime_x64.dll' or one of its dependencies. 31 | # 32 | 33 | root_dir = Path(__file__).parent / "dotnet_config" 34 | script = root_dir / "legacy_v2_runtime.py" 35 | 36 | assert script.is_file() 37 | 38 | config_path.unlink(missing_ok=True) 39 | 40 | # the python.exe.config file gets created 41 | assert not config_path.is_file() 42 | p = Popen([sys.executable, script], stdout=PIPE, stderr=PIPE) # noqa: S603 43 | stdout, stderr = p.communicate() 44 | assert not stdout 45 | assert b"useLegacyV2RuntimeActivationPolicy property was added" in stderr 46 | assert config_path.is_file() 47 | 48 | # the script now runs without error 49 | p = Popen([sys.executable, script], stdout=PIPE, stderr=PIPE) # noqa: S603 50 | stdout, stderr = p.communicate() 51 | assert not stderr 52 | assert stdout.rstrip() == b"SUCCESS" 53 | 54 | # the above tests also depend on LoadLibrary raising OSError 55 | # if the DLL file does not exist 56 | with pytest.raises(OSError, match=r"Cannot find '.*legacy_v2_runtime.dll' for libtype='clr'"): 57 | _ = LoadLibrary(root_dir / "legacy_v2_runtime.dll", libtype="clr") 58 | -------------------------------------------------------------------------------- /tests/test_mock.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import os 4 | import platform 5 | import sys 6 | from ctypes import c_int 7 | from http.client import HTTPConnection 8 | from pathlib import Path 9 | from typing import Any 10 | 11 | from msl.loadlib import Client64, Server32, Server32Error 12 | 13 | if Server32.is_interpreter(): 14 | from unittest.mock import Mock 15 | 16 | pytest = Mock() 17 | skipif_no_server32 = Mock() 18 | default_extension = "" 19 | else: 20 | import pytest # type: ignore[assignment] 21 | 22 | from conftest import skipif_no_server32 # type: ignore[assignment] 23 | from msl.loadlib._constants import default_extension 24 | 25 | 26 | IS_MACOS_ARM64 = sys.platform == "darwin" and platform.machine() == "arm64" 27 | 28 | 29 | class Server(Server32): 30 | def __init__(self, host: str, port: int, **kwargs: str) -> None: 31 | arch = "arm64" if IS_MACOS_ARM64 else "64" if sys.maxsize > 2**32 else "32" 32 | path = os.path.join(Server32.examples_dir(), f"cpp_lib{arch}") # noqa: PTH118 33 | super().__init__(path, "cdll", host, port) 34 | 35 | self.kwargs: dict[str, str] = kwargs 36 | 37 | self.lib.add.argtypes = [c_int, c_int] 38 | self.lib.add.restype = c_int 39 | 40 | def add(self, a: int, b: int) -> int: 41 | out: int = self.lib.add(a, b) 42 | return out 43 | 44 | def get_kwargs(self) -> dict[str, str]: 45 | return self.kwargs 46 | 47 | 48 | class Client(Client64): 49 | def __init__(self, module32: str | Path = __file__, host: str | None = None, **kwargs: Any) -> None: 50 | super().__init__(module32, host=host, **kwargs) 51 | 52 | def add(self, a: int, b: int) -> int: 53 | reply: int = self.request32("add", a, b) 54 | return reply 55 | 56 | def get_kwargs(self) -> dict[str, str]: 57 | # calls the method 58 | reply: dict[str, str] = self.request32("get_kwargs") 59 | return reply 60 | 61 | def kwargs(self) -> dict[str, str]: 62 | # calls the attribute 63 | reply: dict[str, str] = self.request32("kwargs") 64 | return reply 65 | 66 | 67 | @skipif_no_server32 68 | @pytest.mark.parametrize("host", [None, "127.0.0.1"]) # type: ignore[misc] 69 | def test_equivalent(host: None | str) -> None: # type: ignore[misc] 70 | c = Client(host=host, x=1.2) 71 | basename = Path(c.lib32_path).name 72 | if host is None: 73 | assert c.connection is None 74 | assert c.host is None 75 | if sys.maxsize > 2**32: 76 | # client and server are running in 64-bit Python 77 | assert c.lib32_path.endswith(f"cpp_lib64{default_extension}") 78 | else: 79 | # client and server are running in 32-bit Python 80 | assert c.lib32_path.endswith(f"cpp_lib32{default_extension}") 81 | assert c.port == -1 82 | assert str(c) == f"" 83 | else: 84 | assert isinstance(c.connection, HTTPConnection) 85 | assert c.host == host 86 | assert c.lib32_path.endswith(f"cpp_lib32{default_extension}") 87 | assert isinstance(c.port, int) 88 | assert c.port > 0 89 | assert str(c) == f"" 90 | 91 | assert c.add(1, 2) == 3 92 | assert c.get_kwargs() == {"x": "1.2"} # kwarg values are cast to str 93 | assert c.kwargs() == {"x": "1.2"} # access attribute value 94 | 95 | # calling shutdown_server32 multiple times is okay 96 | for _ in range(10): 97 | stdout, stderr = c.shutdown_server32() 98 | assert len(stdout.read()) == 0 99 | assert len(stderr.read()) == 0 100 | 101 | 102 | def test_attributes() -> None: 103 | c = Client(x=0, y="hello", z=None) 104 | assert c.connection is None 105 | assert c.host is None 106 | if sys.maxsize > 2**32: 107 | # client and server are running in 64-bit Python 108 | if IS_MACOS_ARM64: 109 | assert c.lib32_path.endswith(f"cpp_libarm64{default_extension}") 110 | else: 111 | assert c.lib32_path.endswith(f"cpp_lib64{default_extension}") 112 | else: 113 | # client and server are running in 32-bit Python 114 | assert c.lib32_path.endswith(f"cpp_lib32{default_extension}") 115 | assert c.port == -1 116 | assert str(c) == f"" 117 | assert c.add(1, 2) == 3 118 | # kwarg values are cast to str 119 | assert c.get_kwargs() == {"x": "0", "y": "hello", "z": "None"} 120 | # access attribute value 121 | assert c.kwargs() == {"x": "0", "y": "hello", "z": "None"} 122 | 123 | # calling shutdown_server32 multiple times is okay 124 | for _ in range(10): 125 | stdout, stderr = c.shutdown_server32() 126 | assert len(stdout.read()) == 0 127 | assert len(stderr.read()) == 0 128 | stdout.close() 129 | stderr.close() 130 | 131 | 132 | def test_context_manager() -> None: 133 | with Client(host=None) as c: 134 | assert c.add(1, 2) == 3 135 | assert not c.kwargs() 136 | 137 | 138 | def test_raises_server32_error() -> None: 139 | c = Client(host=None) 140 | with pytest.raises(Server32Error, match=r"\(see above for more details\)$"): 141 | assert c.add(1, [2]) == 3 # type: ignore[arg-type] # pyright: ignore[reportArgumentType] 142 | 143 | stdout, stderr = c.shutdown_server32() 144 | assert len(stdout.read()) == 0 145 | assert len(stderr.read()) == 0 146 | stdout.close() 147 | stderr.close() 148 | 149 | 150 | def test_no_server32_subclass() -> None: 151 | mod = Path(__file__).parent / "bad_servers" / "no_server32_subclass.py" 152 | match = r"no_server32_subclass.py' does not contain a class that is a subclass of Server32$" 153 | with pytest.raises(AttributeError, match=match): 154 | _ = Client(module32=mod) 155 | 156 | 157 | def test_module_not_found() -> None: 158 | with pytest.raises(ModuleNotFoundError): 159 | _ = Client(module32="does_not_exist") 160 | -------------------------------------------------------------------------------- /tests/test_server32_argparse.py: -------------------------------------------------------------------------------- 1 | """Tests the CLI argparse which updates sys.path, os.environ['PATH'] and **kwargs for the 32-bit server.""" 2 | 3 | from __future__ import annotations 4 | 5 | import os 6 | import sys 7 | from pathlib import Path 8 | from typing import Any 9 | 10 | from msl.loadlib import Client64, ConnectionTimeoutError, Server32 11 | 12 | if Server32.is_interpreter(): 13 | from unittest.mock import Mock 14 | 15 | pytest = Mock() 16 | skipif_no_server32 = Mock() 17 | skipif_not_windows = Mock() 18 | else: 19 | import pytest # type: ignore[assignment] 20 | 21 | from conftest import skipif_no_server32, skipif_not_windows # type: ignore[assignment] 22 | 23 | IS_WINDOWS: bool = sys.platform == "win32" 24 | 25 | 26 | class BytesPath: 27 | def __init__(self, path: bytes) -> None: 28 | self._path: bytes = path 29 | 30 | def __fspath__(self) -> bytes: 31 | return self._path 32 | 33 | 34 | class ArgParse32(Server32): 35 | def __init__(self, host: str, port: int, **kwargs: str) -> None: 36 | # load any dll since it won't be called 37 | path = os.path.join(Server32.examples_dir(), "cpp_lib32") # noqa: PTH118 38 | super().__init__(path, "cdll", host, port) 39 | self.kwargs: dict[str, str] = kwargs 40 | 41 | self.env_paths: list[str] = os.environ["PATH"].split(os.pathsep) 42 | 43 | # the server creates the os.added_dll_directories attribute (Windows only) 44 | self.dll_dirs: str | list[str] 45 | try: 46 | dll_dirs = os.added_dll_directories # type: ignore[attr-defined] # pyright: ignore[reportAttributeAccessIssue,reportUnknownMemberType,reportUnknownVariableType] 47 | except AttributeError: 48 | self.dll_dirs = "os.added_dll_directories does not exist" 49 | else: 50 | self.dll_dirs = [p.path for p in dll_dirs] # pyright: ignore[reportUnknownMemberType,reportUnknownVariableType] 51 | 52 | @staticmethod 53 | def is_in_sys_path(path: str) -> bool: 54 | return path in sys.path 55 | 56 | def is_in_environ_path(self, path: str) -> bool: 57 | return path in self.env_paths 58 | 59 | def is_in_dll_dirs(self, path: str) -> bool: 60 | return path in self.dll_dirs 61 | 62 | def get_kwarg(self, key: str) -> str: 63 | value: str = self.kwargs[key] 64 | return value 65 | 66 | 67 | class ArgParse64(Client64): 68 | def __init__(self, **kwargs: Any) -> None: 69 | super().__init__(__file__, **kwargs) 70 | 71 | def is_in_sys_path(self, path: str | bytes | Path | BytesPath) -> bool: 72 | p = os.path.abspath(os.fsdecode(path)) # noqa: PTH100 73 | reply: bool = self.request32("is_in_sys_path", p) 74 | return reply 75 | 76 | def is_in_environ_path(self, path: str | bytes | Path | BytesPath) -> bool: 77 | p = os.path.abspath(os.fsdecode(path)) # noqa: PTH100 78 | reply: bool = self.request32("is_in_environ_path", p) 79 | return reply 80 | 81 | def is_in_dll_dirs(self, path: str | bytes | Path | BytesPath) -> bool: 82 | p = os.path.abspath(os.fsdecode(path)) # noqa: PTH100 83 | reply: bool = self.request32("is_in_dll_dirs", p) 84 | return reply 85 | 86 | def get_kwarg(self, key: str) -> str: 87 | reply: str = self.request32("get_kwarg", key) 88 | return reply 89 | 90 | 91 | @skipif_no_server32 92 | def test_arg_parser_iterable() -> None: # type: ignore[misc] 93 | sys_path: list[bytes | str | Path | BytesPath] 94 | env_path: list[bytes | str | Path | BytesPath] 95 | if IS_WINDOWS: 96 | sys_path = [ 97 | b"C:/home/joe/code", 98 | "C:\\Program Files (x86)\\Whatever", 99 | Path(r"C:\Users\username"), 100 | BytesPath(b"C:/users"), 101 | ] 102 | env_path = [ 103 | b"D:/ends/in/slash/", 104 | "D:/path/to/lib", 105 | Path(r"D:\path\with space"), 106 | BytesPath(b"C:/a/b/c"), 107 | ] 108 | dll_dir = os.path.dirname(__file__) # noqa: PTH120 109 | else: 110 | sys_path = [ 111 | b"/ends/in/slash/", 112 | "/usr/local/custom/path", 113 | Path("/home/username"), 114 | BytesPath(b"/a/b/c"), 115 | ] 116 | env_path = [ 117 | b"/home/my/folder", 118 | "/a/path/for/environ/slash/", 119 | Path("/home/my/username"), 120 | BytesPath(b"/a/b/c/d"), 121 | ] 122 | dll_dir = None 123 | 124 | kwargs = {"a": -11, "b": 3.1415926, "c": "abcd efghi jk", "d": [1, 2, 3], "e": {1: "val"}} # cSpell: ignore efghi 125 | 126 | client = ArgParse64(add_dll_directory=dll_dir, append_sys_path=sys_path, append_environ_path=env_path, **kwargs) 127 | 128 | for path in sys_path: 129 | assert client.is_in_sys_path(path) 130 | assert client.is_in_sys_path(Path(__file__).parent) 131 | 132 | for path in env_path: 133 | assert client.is_in_environ_path(path) 134 | assert client.is_in_environ_path(Path.cwd()) 135 | 136 | if dll_dir is not None: 137 | assert client.is_in_dll_dirs(dll_dir) 138 | 139 | assert client.get_kwarg("a") == "-11" 140 | assert client.get_kwarg("b") == "3.1415926" 141 | assert client.get_kwarg("c") == "abcd efghi jk" 142 | assert client.get_kwarg("d") == "[1, 2, 3]" 143 | assert client.get_kwarg("e") == "{1: 'val'}" 144 | 145 | 146 | @skipif_not_windows 147 | def test_add_dll_directory_win32() -> None: # type: ignore[misc] 148 | with ArgParse64() as a: 149 | assert a.request32("dll_dirs") == "os.added_dll_directories does not exist" 150 | 151 | path = str(Path(__file__).parent) 152 | with ArgParse64(add_dll_directory=path) as a: 153 | assert a.is_in_dll_dirs(path) 154 | 155 | dll_dirs: list[bytes | str | Path | BytesPath] = [ 156 | path.encode(), 157 | os.path.abspath(os.path.join(path, "..")), # noqa: PTH100, PTH118 158 | Path(os.path.abspath(os.path.join(path, "..", ".."))), # noqa: PTH100, PTH118 159 | BytesPath(os.path.join(path, "bad_servers").encode()), # noqa: PTH118 160 | ] 161 | with ArgParse64(add_dll_directory=dll_dirs) as a: 162 | for item in dll_dirs: 163 | assert a.is_in_dll_dirs(item) 164 | 165 | with pytest.raises(ConnectionTimeoutError, match=r"FileNotFoundError"): # noqa: SIM117 166 | with ArgParse64(add_dll_directory="C:\\does\\not\\exist", timeout=5): 167 | pass 168 | 169 | 170 | @pytest.mark.skipif(IS_WINDOWS, reason="do not test on Windows") # type: ignore[misc] 171 | @skipif_no_server32 172 | def test_add_dll_directory_linux() -> None: # type: ignore[misc] 173 | with pytest.raises(ConnectionTimeoutError, match=r"not supported"): # noqa: SIM117 174 | with ArgParse64(add_dll_directory="/home", timeout=2): 175 | pass 176 | -------------------------------------------------------------------------------- /tests/test_server32_bad_modules.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | import pytest 4 | 5 | from conftest import skipif_no_server32 6 | from msl.loadlib import Client64, ConnectionTimeoutError 7 | 8 | 9 | class Client(Client64): 10 | def __init__(self, module32: str) -> None: 11 | super().__init__(module32, append_sys_path=Path(__file__).parent / "bad_servers", timeout=5) 12 | 13 | 14 | def check(module_name: str, match: str) -> None: 15 | # This test module is a bit buggy. 16 | # Sometimes we get a ConnectionRefusedError that we want to ignore. 17 | attempts = 1 18 | 19 | max_attempts = 3 20 | while True: 21 | try: 22 | with pytest.raises(ConnectionTimeoutError, match=match): 23 | _ = Client(module_name) 24 | except ConnectionRefusedError: # noqa: PERF203 25 | if attempts == max_attempts: 26 | raise 27 | attempts += 1 28 | else: 29 | break # then this test was successful 30 | 31 | 32 | @skipif_no_server32 33 | def test_no_module() -> None: 34 | check("", r"specify a Python module") 35 | 36 | 37 | @skipif_no_server32 38 | def test_relative_import() -> None: 39 | check(".relative", r"Cannot perform relative imports") 40 | 41 | 42 | @skipif_no_server32 43 | def test_import_error() -> None: 44 | # the module import_error.py exists, but the module imports a package that does not exist 45 | check("import_error", r"No module named 'missing'") 46 | 47 | 48 | @skipif_no_server32 49 | def test_import_error2() -> None: 50 | check("doesnotexist", r"module must be in sys.path") 51 | 52 | 53 | @skipif_no_server32 54 | def test_no_server32_subclass() -> None: 55 | check("no_server32_subclass", r"Module does not contain a class that is a subclass of Server32") 56 | 57 | 58 | @skipif_no_server32 59 | def test_no_init() -> None: 60 | check("no_init", r"class NoInit\(Server32\):") 61 | 62 | 63 | @skipif_no_server32 64 | def test_bad_init_args() -> None: 65 | check("bad_init_args", r"class BadInitArgs\(Server32\):") 66 | 67 | 68 | @skipif_no_server32 69 | def test_bad_init_args2() -> None: 70 | check("bad_init_args2", r"missing 1 required positional argument: 'extra'") 71 | 72 | 73 | @skipif_no_server32 74 | def test_no_super() -> None: 75 | check("no_super", r"class NoSuper\(Server32\):") 76 | 77 | 78 | @skipif_no_server32 79 | def test_bad_super_init() -> None: 80 | check("bad_super_init", r"class BadSuperInit\(Server32\):") 81 | 82 | 83 | @skipif_no_server32 84 | def test_bad_lib_path() -> None: 85 | check("bad_lib_path", r"Cannot find 'doesnotexist' for libtype='cdll'") 86 | 87 | 88 | @skipif_no_server32 89 | def test_bad_lib_type() -> None: 90 | check("bad_lib_type", r"ValueError: Invalid libtype 'invalid'") 91 | 92 | 93 | @skipif_no_server32 94 | def test_unexpected_error() -> None: 95 | check("unexpected_error", r"ZeroDivisionError") 96 | 97 | 98 | @skipif_no_server32 99 | def test_unexpected_error2() -> None: 100 | check("unexpected_error2", r"TypeError: unsupported operand") 101 | 102 | 103 | @skipif_no_server32 104 | def test_unexpected_error3() -> None: 105 | check("unexpected_error3", r"NameError: name 'y' is not defined") 106 | 107 | 108 | @skipif_no_server32 109 | def test_wrong_bitness() -> None: 110 | check("wrong_bitness", r"Failed to load") 111 | -------------------------------------------------------------------------------- /tests/test_server32_examples_dir.py: -------------------------------------------------------------------------------- 1 | import os 2 | from pathlib import Path 3 | 4 | from msl.examples.loadlib import EXAMPLES_DIR 5 | from msl.loadlib import Client64, Server32 6 | 7 | if Server32.is_interpreter(): 8 | from unittest.mock import Mock 9 | 10 | skipif_no_server32 = Mock() 11 | else: 12 | from conftest import skipif_no_server32 # type: ignore[assignment] 13 | 14 | 15 | class Ex32(Server32): 16 | def __init__(self, host: str, port: int) -> None: 17 | # this class would not instantiate if Server32.examples_dir() 18 | # was incorrect, so the fact that the server starts already 19 | # demonstrates that this test passes 20 | path = os.path.join(Server32.examples_dir(), "cpp_lib32") # noqa: PTH118 21 | super().__init__(path, "cdll", host, port) 22 | 23 | def ex_dir(self) -> Path: 24 | return self.examples_dir() 25 | 26 | 27 | class Ex64(Client64): 28 | def __init__(self) -> None: 29 | super().__init__(__file__) 30 | 31 | def examples_dir(self) -> str: 32 | reply: str = self.request32("examples_dir") 33 | return reply 34 | 35 | def ex_dir(self) -> str: 36 | reply: str = self.request32("ex_dir") 37 | return reply 38 | 39 | 40 | @skipif_no_server32 41 | def test_examples_dir() -> None: # type: ignore[misc] 42 | assert Server32.examples_dir() == EXAMPLES_DIR 43 | 44 | with Ex64() as e: 45 | assert e.examples_dir() == str(EXAMPLES_DIR) 46 | assert e.ex_dir() == str(EXAMPLES_DIR) 47 | -------------------------------------------------------------------------------- /tests/test_server32_is_interpreter.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from msl.loadlib import Client64, Server32 4 | 5 | if Server32.is_interpreter(): 6 | from unittest.mock import Mock 7 | 8 | skipif_no_server32 = Mock() 9 | else: 10 | from conftest import skipif_no_server32 # type: ignore[assignment] 11 | 12 | 13 | class Running32(Server32): 14 | def __init__(self, host: str, port: int) -> None: 15 | path = os.path.join(Server32.examples_dir(), "cpp_lib32") # noqa: PTH118 16 | super().__init__(path, "cdll", host, port) 17 | 18 | def interpreter(self) -> bool: 19 | return self.is_interpreter() 20 | 21 | 22 | class Running64(Client64): 23 | def __init__(self) -> None: 24 | super().__init__(__file__) 25 | 26 | def interpreter(self) -> bool: 27 | reply: bool = self.request32("interpreter") 28 | return reply 29 | 30 | def is_interpreter(self) -> bool: 31 | reply: bool = self.request32("is_interpreter") 32 | return reply 33 | 34 | 35 | @skipif_no_server32 36 | def test_is_interpreter() -> None: # type: ignore[misc] 37 | with Running64() as r: 38 | interpreter = r.interpreter() 39 | assert isinstance(interpreter, bool) 40 | assert interpreter 41 | 42 | is_interpreter = r.is_interpreter() 43 | assert isinstance(is_interpreter, bool) 44 | assert is_interpreter 45 | 46 | assert not Server32.is_interpreter() 47 | -------------------------------------------------------------------------------- /tests/test_server32_issue24a.py: -------------------------------------------------------------------------------- 1 | from msl.loadlib import Client64, ConnectionTimeoutError, Server32 2 | 3 | if Server32.is_interpreter(): 4 | # Simulate the case where importing this module on the 32-bit server hangs 5 | print("importing time") # noqa: T201 6 | import time 7 | 8 | print("sleeping for 999 seconds") # noqa: T201 9 | for _ in range(999): 10 | time.sleep(1) 11 | 12 | import pytest 13 | 14 | from conftest import skipif_no_server32 15 | 16 | 17 | @skipif_no_server32 18 | def test_importing() -> None: 19 | class Issue24(Client64): 20 | def __init__(self) -> None: 21 | super().__init__(__file__, timeout=2) 22 | 23 | with pytest.warns(UserWarning, match=r"killed the 32-bit server using brute force"): # noqa: SIM117 24 | with pytest.raises(ConnectionTimeoutError, match=r"importing time\s+sleeping for 999 seconds"): 25 | with Issue24(): 26 | pass 27 | -------------------------------------------------------------------------------- /tests/test_server32_issue24b.py: -------------------------------------------------------------------------------- 1 | from msl.loadlib import Client64, ConnectionTimeoutError, Server32 2 | 3 | if Server32.is_interpreter(): 4 | from unittest.mock import Mock 5 | 6 | skipif_no_server32 = Mock() 7 | print("mock skipif_no_server32") # noqa: T201 8 | else: 9 | from conftest import skipif_no_server32 # type: ignore[assignment] 10 | 11 | 12 | class HangsForever(Server32): 13 | def __init__(self, host: str, port: int) -> None: 14 | # Simulate the case where instantiating this class on the 32-bit server hangs 15 | print("import time") # noqa: T201 16 | import time 17 | 18 | print("now go to sleep") # noqa: T201 19 | for _ in range(999): 20 | time.sleep(1) 21 | 22 | super().__init__("whatever", "cdll", host, port) 23 | 24 | 25 | @skipif_no_server32 26 | def test_instantiating() -> None: # type: ignore[misc] 27 | import pytest 28 | 29 | class Issue24(Client64): 30 | def __init__(self) -> None: 31 | super().__init__(__file__, timeout=2) 32 | 33 | with pytest.warns(UserWarning, match=r"killed the 32-bit server using brute force") as warn_info: # noqa: SIM117 34 | with pytest.raises(ConnectionTimeoutError, match=r"mock skipif_no_server32\s+import time\s+now go to sleep"): 35 | with Issue24(): # this line is what the lineno test should equal 36 | pass 37 | 38 | assert len(warn_info.list) == 3 39 | 40 | assert warn_info.list[0].filename == __file__ 41 | assert warn_info.list[0].lineno == 35 # occurs at "with Issue24():" above 42 | assert str(warn_info.list[0].message) == "killed the 32-bit server using brute force" 43 | 44 | assert warn_info.list[1].filename == __file__ 45 | assert warn_info.list[1].category is ResourceWarning 46 | assert str(warn_info.list[1].message).startswith("unclosed file") 47 | 48 | assert warn_info.list[2].filename == __file__ 49 | assert warn_info.list[2].category is ResourceWarning 50 | assert str(warn_info.list[2].message).startswith("unclosed file") 51 | -------------------------------------------------------------------------------- /tests/test_server32_property.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import os 4 | from ctypes import c_float 5 | 6 | from msl.loadlib import Client64, Server32, Server32Error 7 | 8 | if Server32.is_interpreter(): 9 | from unittest.mock import Mock 10 | 11 | skipif_no_server32 = Mock() 12 | else: 13 | from conftest import skipif_no_server32 # type: ignore[assignment] 14 | 15 | 16 | class Property32(Server32): 17 | CONSTANT: int = 2 18 | 19 | def __init__(self, host: str, port: int) -> None: 20 | path = os.path.join(Server32.examples_dir(), "cpp_lib32") # noqa: PTH118 21 | super().__init__(path, "cdll", host, port) 22 | 23 | self.three: int = self.lib.add(1, 2) 24 | 25 | def subtract(self, a: float, b: float) -> float: 26 | self.lib.subtract.restype = c_float 27 | result: float = self.lib.subtract(c_float(a), c_float(b)) 28 | return result 29 | 30 | @property 31 | def seven(self) -> int: 32 | return 7 33 | 34 | @property 35 | def parameters(self) -> dict[str, int | str | complex]: 36 | return {"one": 1, "string": "hey", "complex": 7j} 37 | 38 | @property 39 | def multiple(self) -> tuple[int, str, complex]: 40 | return 1, "hey", 7j 41 | 42 | 43 | class Property64(Client64): 44 | def __init__(self) -> None: 45 | super().__init__(__file__) 46 | 47 | self.CONSTANT: int = self.request32("CONSTANT") 48 | self.three: int = self.request32("three") 49 | 50 | def add(self, a: int, b: int) -> int: 51 | reply: int = self.request32("add", a, b) 52 | return reply 53 | 54 | def subtract(self, a: float, b: float) -> float: 55 | reply: float = self.request32("subtract", a, b) 56 | return reply 57 | 58 | @property 59 | def seven(self) -> int: 60 | reply: int = self.request32("seven") 61 | return reply 62 | 63 | @property 64 | def parameters(self) -> dict[str, int | str | complex]: 65 | reply: dict[str, int | str | complex] = self.request32("parameters") 66 | return reply 67 | 68 | @property 69 | def foo(self) -> int: 70 | reply: int = self.request32("foo") 71 | return reply 72 | 73 | def multiple(self) -> tuple[int, str, complex]: 74 | reply: tuple[int, str, complex] = self.request32("multiple") 75 | return reply 76 | 77 | 78 | @skipif_no_server32 79 | def test_request_property() -> None: # type: ignore[misc] 80 | import pytest 81 | 82 | with Property64() as p: 83 | assert p.subtract(100, 100) == 0 84 | assert p.CONSTANT == 2 85 | assert p.three == 3 86 | assert p.seven == 7 87 | assert p.parameters == {"one": 1, "string": "hey", "complex": 7j} 88 | assert p.multiple() == (1, "hey", 7j) 89 | 90 | with pytest.raises(Server32Error, match=r"'Property32' object has no attribute 'add'"): 91 | _ = p.add(1, 2) 92 | 93 | with pytest.raises(Server32Error, match=r"'Property32' object has no attribute 'foo'"): 94 | _ = p.foo 95 | -------------------------------------------------------------------------------- /tests/test_server32_protocol.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import os 4 | from typing import Any 5 | 6 | from msl.loadlib import Client64, Server32 7 | 8 | if Server32.is_interpreter(): 9 | from unittest.mock import Mock 10 | 11 | skipif_no_server32 = Mock() 12 | else: 13 | from conftest import skipif_no_server32 # type: ignore[assignment] 14 | 15 | 16 | class Bounce32(Server32): 17 | def __init__(self, host: str, port: int) -> None: 18 | path = os.path.join(Server32.examples_dir(), "cpp_lib32") # noqa: PTH118 19 | super().__init__(path, "cdll", host, port) 20 | 21 | def bounce(self, *args: Any, **kwargs: Any) -> Any: 22 | return args, kwargs 23 | 24 | 25 | class Bounce64(Client64): 26 | def __init__(self, protocol: int) -> None: 27 | super().__init__(__file__, protocol=protocol) 28 | 29 | def bounce(self, *args: Any, **kwargs: Any) -> Any: 30 | return self.request32("bounce", *args, **kwargs) 31 | 32 | 33 | @skipif_no_server32 34 | def test_protocol() -> None: # type: ignore[misc] 35 | import pytest 36 | 37 | args = (None, True, False, 1, -2.0, 5 - 6j, [1, [2.0, "hello"]], {"one": "1", "two": 2}) 38 | kwargs = { 39 | "None": None, 40 | "True": True, 41 | "False": False, 42 | "Int": 1, 43 | "Float": -2.0, 44 | "Complex": 5 - 6j, 45 | "List": [1, [2.0, "hello"]], 46 | "Dict": {"one": "1", "two": 2}, 47 | } 48 | 49 | for protocol in [0, 1, 2, 3, 4, 5]: 50 | with Bounce64(protocol) as b: 51 | a, k = b.bounce(*args, **kwargs) 52 | assert a == args 53 | assert k == kwargs 54 | 55 | with pytest.raises(ValueError, match=r"pickle protocol"): # noqa: SIM117 56 | with Bounce64(6) as b: 57 | pass 58 | -------------------------------------------------------------------------------- /tests/test_server32_remove_site_packages.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | from msl.loadlib import Client64, Server32 5 | 6 | if Server32.is_interpreter(): 7 | from unittest.mock import Mock 8 | 9 | skipif_no_server32 = Mock() 10 | else: 11 | from conftest import skipif_no_server32 # type: ignore[assignment] 12 | 13 | 14 | class Site32(Server32): 15 | def __init__(self, host: str, port: int) -> None: 16 | path = os.path.join(Server32.examples_dir(), "cpp_lib32") # noqa: PTH118 17 | super().__init__(path, "cdll", host, port) 18 | 19 | def remove(self) -> str: 20 | return self.remove_site_packages_64bit() 21 | 22 | @staticmethod 23 | def contains(path: str) -> bool: 24 | return path in sys.path 25 | 26 | 27 | class Site64(Client64): 28 | def __init__(self) -> None: 29 | super().__init__(__file__) 30 | 31 | def remove(self) -> str: 32 | reply: str = self.request32("remove") 33 | return reply 34 | 35 | def contains(self, path: str) -> bool: 36 | reply: bool = self.request32("contains", path) 37 | return reply 38 | 39 | 40 | @skipif_no_server32 41 | def test_remove() -> None: # type: ignore[misc] 42 | with Site64() as s: 43 | path = s.remove() 44 | assert path 45 | assert path in sys.path 46 | assert not s.contains(path) 47 | -------------------------------------------------------------------------------- /tests/test_server32_rpc_timeout.py: -------------------------------------------------------------------------------- 1 | import os 2 | import time 3 | 4 | from msl.loadlib import Client64, ResponseTimeoutError, Server32 5 | 6 | if Server32.is_interpreter(): 7 | from unittest.mock import Mock 8 | 9 | skipif_no_server32 = Mock() 10 | else: 11 | from conftest import skipif_no_server32 # type: ignore[assignment] 12 | 13 | 14 | RPC_TIMEOUT = 5.0 15 | 16 | 17 | class RPCServer(Server32): 18 | def __init__(self, host: str, port: int) -> None: 19 | path = os.path.join(Server32.examples_dir(), "cpp_lib32") # noqa: PTH118 20 | super().__init__(path, "cdll", host, port) 21 | 22 | def no_delay(self, a: int, b: int) -> int: 23 | result: int = self.lib.add(a, b) 24 | return result 25 | 26 | def short_delay(self, a: int, b: int) -> int: 27 | time.sleep(RPC_TIMEOUT / 2.0) 28 | result: int = self.lib.add(a, b) 29 | return result 30 | 31 | def long_delay(self, a: int, b: int) -> int: 32 | time.sleep(RPC_TIMEOUT * 2.0) 33 | result: int = self.lib.add(a, b) 34 | return result 35 | 36 | 37 | class RPCClient(Client64): 38 | def __init__(self) -> None: 39 | super().__init__(__file__, rpc_timeout=RPC_TIMEOUT) 40 | 41 | def no_delay(self, a: int, b: int) -> int: 42 | reply: int = self.request32("no_delay", a, b) 43 | return reply 44 | 45 | def short_delay(self, a: int, b: int) -> int: 46 | reply: int = self.request32("short_delay", a, b) 47 | return reply 48 | 49 | def long_delay(self, a: int, b: int) -> int: 50 | reply: int = self.request32("long_delay", a, b) 51 | return reply 52 | 53 | 54 | @skipif_no_server32 55 | def test_rpc_timeout() -> None: # type: ignore[misc] 56 | import pytest 57 | 58 | with RPCClient() as c: 59 | assert c.no_delay(8, 3) == 11 60 | assert c.short_delay(-4, 2) == -2 61 | with pytest.raises(ResponseTimeoutError): 62 | _ = c.long_delay(1, 2) 63 | -------------------------------------------------------------------------------- /tests/test_server32_sew_eurodrive.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | from conftest import skipif_not_windows, xfail_windows_ga 4 | from msl.loadlib import Client64 5 | 6 | 7 | @xfail_windows_ga 8 | @skipif_not_windows 9 | def test_sew_eurodrive() -> None: 10 | """The reason for creating this test case was discussed through email. 11 | 12 | The LoadLibrary class has been modified so that clr.AddReference is 13 | called before clr.System.Reflection.Assembly.LoadFile is called. 14 | The order of execution was important for the example .NET DLL 15 | provided by an engineer at SEW-EURODRIVE. 16 | """ 17 | 18 | class SEWEuroDrive64(Client64): 19 | def __init__(self) -> None: 20 | super().__init__(Path(__file__).parent / "sew_eurodrive" / "sew32.py") 21 | 22 | def do_something(self, value: float) -> float: 23 | result: float = self.request32("do_something", value) 24 | return result 25 | 26 | with SEWEuroDrive64() as sew: 27 | assert sew.do_something(600) == 100.0 28 | -------------------------------------------------------------------------------- /tests/test_server32_shutdown.py: -------------------------------------------------------------------------------- 1 | import os 2 | import time 3 | 4 | from msl.loadlib import Client64, Server32 5 | 6 | if Server32.is_interpreter(): 7 | from unittest.mock import Mock 8 | 9 | skipif_no_server32 = Mock() 10 | else: 11 | from conftest import skipif_no_server32 # type: ignore[assignment] 12 | 13 | 14 | class ShutdownHangs(Server32): 15 | def __init__(self, host: str, port: int) -> None: 16 | path = os.path.join(Server32.examples_dir(), "cpp_lib32") # noqa: PTH118 17 | super().__init__(path, "cdll", host, port) 18 | 19 | def add(self, x: int, y: int) -> int: 20 | result: int = self.lib.add(x, y) 21 | return result 22 | 23 | def shutdown_handler(self) -> None: 24 | time.sleep(999) 25 | 26 | 27 | @skipif_no_server32 28 | def test_killed() -> None: # type: ignore[misc] 29 | import pytest 30 | 31 | class Hangs(Client64): 32 | def __init__(self) -> None: 33 | super().__init__(__file__) 34 | 35 | def add(self, x: int, y: int) -> int: 36 | reply: int = self.request32("add", x, y) 37 | return reply 38 | 39 | with Hangs() as hangs: 40 | assert hangs.add(1, 1) == 2 41 | with pytest.warns(UserWarning, match=r"killed the 32-bit server using brute force") as warn_info: 42 | _ = hangs.shutdown_server32(kill_timeout=2) 43 | 44 | assert len(warn_info.list) == 1 45 | assert warn_info.list[0].filename == __file__ 46 | assert warn_info.list[0].lineno == 42 # occurs at hangs.shutdown_server32 above 47 | -------------------------------------------------------------------------------- /tests/test_server32_stdout_stderr.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | from msl.loadlib import Client64, Server32 5 | 6 | if Server32.is_interpreter(): 7 | from unittest.mock import Mock 8 | 9 | skipif_no_server32 = Mock() 10 | else: 11 | from conftest import skipif_no_server32 # type: ignore[assignment] 12 | 13 | 14 | class Print32(Server32): 15 | def __init__(self, host: str, port: int, **kwargs: str) -> None: 16 | path = os.path.join(Server32.examples_dir(), "cpp_lib32") # noqa: PTH118 17 | super().__init__(path, "cdll", host, port) 18 | 19 | if kwargs["show"] == "True": 20 | print("this is a message") # noqa: T201 21 | print("there is a problem", file=sys.stderr) # noqa: T201 22 | 23 | def write(self, n: int, *, stdout: bool) -> bool: 24 | stream = sys.stdout if stdout else sys.stderr 25 | print("x" * n, end="", file=stream) 26 | return True 27 | 28 | 29 | class Print64(Client64): 30 | def __init__(self, *, show: bool) -> None: 31 | super().__init__(__file__, show=show) 32 | 33 | def write(self, n: int, *, stdout: bool) -> bool: 34 | reply: bool = self.request32("write", n, stdout=stdout) 35 | return reply 36 | 37 | 38 | @skipif_no_server32 39 | def test_shutdown_server32() -> None: # type: ignore[misc] 40 | p = Print64(show=True) 41 | stdout, stderr = p.shutdown_server32() 42 | assert stdout.read().rstrip() == b"this is a message" 43 | assert stderr.read().rstrip() == b"there is a problem" 44 | 45 | # calling shutdown_server32 multiple times is okay 46 | # but the buffer has been read already 47 | for _ in range(10): 48 | stdout, stderr = p.shutdown_server32() 49 | assert not stdout.read() 50 | assert not stderr.read() 51 | 52 | 53 | @skipif_no_server32 54 | def test_buffer_size_4096() -> None: # type: ignore[misc] 55 | # 4096 is the maximum buffer size for a PIPE before the 32-bit server blocks 56 | n = 4096 57 | p = Print64(show=False) 58 | assert p.write(n, stdout=True) 59 | assert p.write(n, stdout=False) 60 | stdout, stderr = p.shutdown_server32() 61 | assert stdout.read() == b"x" * n 62 | assert stderr.read() == b"x" * n 63 | 64 | # calling shutdown_server32 multiple times is okay 65 | # but the buffer has been read already 66 | for _ in range(10): 67 | stdout, stderr = p.shutdown_server32() 68 | assert not stdout.read() 69 | assert not stderr.read() 70 | -------------------------------------------------------------------------------- /tests/uñicödé/Namespace.With.Dots-uñicödé.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MSLNZ/msl-loadlib/c3f9e7e6f4d0c739e6af608bbbe865504030f503/tests/uñicödé/Namespace.With.Dots-uñicödé.dll -------------------------------------------------------------------------------- /tests/uñicödé/Trig.class: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MSLNZ/msl-loadlib/c3f9e7e6f4d0c739e6af608bbbe865504030f503/tests/uñicödé/Trig.class -------------------------------------------------------------------------------- /tests/uñicödé/cpp32unicode.py: -------------------------------------------------------------------------------- 1 | import ctypes 2 | 3 | from msl.loadlib import Server32 4 | 5 | 6 | class Cpp32(Server32): 7 | def __init__(self, host: str, port: int) -> None: 8 | super().__init__("cpp_lib32-uñicödé", "cdll", host, port) 9 | 10 | def add(self, a: int, b: int) -> int: 11 | result: int = self.lib.add(ctypes.c_int32(a), ctypes.c_int32(b)) 12 | return result 13 | -------------------------------------------------------------------------------- /tests/uñicödé/cpp_lib32-uñicödé.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MSLNZ/msl-loadlib/c3f9e7e6f4d0c739e6af608bbbe865504030f503/tests/uñicödé/cpp_lib32-uñicödé.dll -------------------------------------------------------------------------------- /tests/uñicödé/cpp_lib32-uñicödé.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MSLNZ/msl-loadlib/c3f9e7e6f4d0c739e6af608bbbe865504030f503/tests/uñicödé/cpp_lib32-uñicödé.so -------------------------------------------------------------------------------- /tests/uñicödé/cpp_lib64-uñicödé.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MSLNZ/msl-loadlib/c3f9e7e6f4d0c739e6af608bbbe865504030f503/tests/uñicödé/cpp_lib64-uñicödé.dll -------------------------------------------------------------------------------- /tests/uñicödé/cpp_lib64-uñicödé.dylib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MSLNZ/msl-loadlib/c3f9e7e6f4d0c739e6af608bbbe865504030f503/tests/uñicödé/cpp_lib64-uñicödé.dylib -------------------------------------------------------------------------------- /tests/uñicödé/cpp_lib64-uñicödé.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MSLNZ/msl-loadlib/c3f9e7e6f4d0c739e6af608bbbe865504030f503/tests/uñicödé/cpp_lib64-uñicödé.so -------------------------------------------------------------------------------- /tests/uñicödé/cpp_libarm64-uñicödé.dylib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MSLNZ/msl-loadlib/c3f9e7e6f4d0c739e6af608bbbe865504030f503/tests/uñicödé/cpp_libarm64-uñicödé.dylib --------------------------------------------------------------------------------