├── docs ├── _static │ └── keep_dir_alive ├── snippets │ ├── reqrep_sync.py │ ├── pair0_sync.py │ ├── pair0_async.py │ ├── pair1_sync.py │ ├── pushpull_sync.py │ ├── bus0_sync.py │ ├── pair1_async.py │ ├── surveyor_sync.py │ └── pubsub_sync.py ├── Makefile ├── requirements.txt ├── make.bat ├── developing.rst ├── index.rst ├── core.rst ├── conf.py └── exceptions.rst ├── pynng ├── _version.py ├── __init__.py ├── exceptions.py ├── tls.py ├── sockaddr.py ├── options.py └── _aio.py ├── .github ├── rmstuff.sh └── workflows │ ├── smoketest.yml │ └── cibuildwheel.yml ├── setup.cfg ├── .gitignore ├── MANIFEST.in ├── .readthedocs.yaml ├── test ├── _test_util.py ├── test_options.py ├── test_aio.py ├── test_msg.py ├── test_tls.py ├── test_api.py ├── test_pipe.py ├── test_protocols.py └── test_abstract.py ├── LICENSE.txt ├── new_release.sh ├── examples ├── pubsub.py ├── pair0.py ├── pipeline.py ├── reqprep.py ├── survey.py ├── README.md ├── bus.py ├── pair1_async.py └── abstract.py ├── generate_api.sh ├── pyproject.toml ├── README.md ├── setup.py └── nng_api.h /docs/_static/keep_dir_alive: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pynng/_version.py: -------------------------------------------------------------------------------- 1 | __version__ = "0.8.1+dev" 2 | -------------------------------------------------------------------------------- /.github/rmstuff.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | rm -rf build nng/build* mbedtls/build* 4 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [build_nng] 2 | repo=https://github.com/nanomsg/nng 3 | rev=v1.6.0 4 | 5 | [build_mbedtls] 6 | repo=https://github.com/ARMmbed/mbedtls.git 7 | rev=v3.5.1 8 | 9 | [build_ext] 10 | inplace = 1 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *~ 3 | *.o 4 | *.so 5 | _nng_py.c 6 | Release 7 | _nng.c 8 | *.pyd 9 | __pycache__ 10 | *.swp 11 | .pytest_cache 12 | dist 13 | *.egg-info 14 | build 15 | nng/ 16 | mbedtls/ 17 | .eggs 18 | .venv/ 19 | .vscode/ 20 | .lvimrc 21 | .ycm_extra_conf.py 22 | *.whl 23 | -------------------------------------------------------------------------------- /docs/snippets/reqrep_sync.py: -------------------------------------------------------------------------------- 1 | from pynng import Req0, Rep0 2 | 3 | address = "tcp://127.0.0.1:13131" 4 | 5 | with Rep0(listen=address) as rep, Req0(dial=address) as req: 6 | req.send(b"random.random()") 7 | question = rep.recv() 8 | answer = b"4" # guaranteed to be random 9 | rep.send(answer) 10 | print(req.recv()) # prints b'4' 11 | -------------------------------------------------------------------------------- /docs/snippets/pair0_sync.py: -------------------------------------------------------------------------------- 1 | from pynng import Pair0 2 | 3 | address = "tcp://127.0.0.1:13131" 4 | # in real code you should also pass recv_timeout and/or send_timeout 5 | with Pair0(listen=address) as s0, Pair0(dial=address) as s1: 6 | s0.send(b"hello s1") 7 | print(s1.recv()) # prints b'hello s1' 8 | s1.send(b"hi old buddy s0, great to see ya") 9 | print(s0.recv()) # prints b'hi old buddy s0, great to see ya 10 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include *.sh *.py *.bat 8.txt *.md 2 | include *.h 3 | include LICENSE.txt 4 | recursive-include nng * 5 | recursive-include mbedtls * 6 | recursive-include test *.py 7 | recursive-include pynng *.py 8 | prune nng/build 9 | prune nng/.git 10 | prune nng/.circleci 11 | exclude nng/.gitignore 12 | prune mbedtls/build 13 | prune mbedtls/.git 14 | prune mbedtls/.github 15 | prune mbedtls/.travis.yml 16 | prune mbedtls/prefix 17 | exclude mbedtls/.gitignore 18 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # Read the Docs configuration file for Sphinx projects 2 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 3 | 4 | # Required 5 | version: 2 6 | 7 | # Set the OS, Python version and other tools you might need 8 | build: 9 | os: ubuntu-22.04 10 | tools: 11 | python: "3.11" 12 | 13 | # Build documentation in the "docs/" directory with Sphinx 14 | sphinx: 15 | configuration: docs/conf.py 16 | 17 | python: 18 | install: 19 | - requirements: docs/requirements.txt 20 | - method: pip 21 | path: . 22 | -------------------------------------------------------------------------------- /docs/snippets/pair0_async.py: -------------------------------------------------------------------------------- 1 | from trio import run 2 | from pynng import Pair0 3 | 4 | 5 | async def send_and_recv(): 6 | address = "tcp://127.0.0.1:13131" 7 | # in real code you should also pass recv_timeout and/or send_timeout 8 | with Pair0(listen=address) as s0, Pair0(dial=address) as s1: 9 | await s0.asend(b"hello s1") 10 | print(await s1.arecv()) # prints b'hello s1' 11 | await s1.asend(b"hi old buddy s0, great to see ya") 12 | print(await s0.arecv()) # prints b'hi old buddy s0, great to see ya 13 | 14 | 15 | run(send_and_recv) 16 | -------------------------------------------------------------------------------- /.github/workflows/smoketest.yml: -------------------------------------------------------------------------------- 1 | name: smoketest 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | matrix: 10 | python-version: [ '3.8' ] 11 | steps: 12 | - uses: actions/checkout@v2 13 | - name: Setup python 14 | uses: actions/setup-python@v2 15 | with: 16 | python-version: ${{ matrix.python-version }} 17 | architecture: x64 18 | - run: sudo apt update 19 | - run: sudo apt install -y ninja-build 20 | - run: pip install -v -e '.[dev]' 21 | - run: pytest -v test 22 | -------------------------------------------------------------------------------- /docs/snippets/pair1_sync.py: -------------------------------------------------------------------------------- 1 | from pynng import Pair1 2 | 3 | address = "tcp://127.0.0.1:12343" 4 | with Pair1(listen=address, polyamorous=True) as s0, Pair1( 5 | dial=address, polyamorous=True 6 | ) as s1, Pair1(dial=address, polyamorous=True) as s2: 7 | s1.send(b"hello from s1") 8 | s2.send(b"hello from s2") 9 | msg1 = s0.recv_msg() 10 | msg2 = s0.recv_msg() 11 | print(msg1.bytes) # prints b'hello from s1' 12 | print(msg2.bytes) # prints b'hello from s2' 13 | msg1.pipe.send(b"hey s1") 14 | msg2.pipe.send(b"hey s2") 15 | print(s2.recv()) # prints b'hey s2' 16 | print(s1.recv()) # prints b'hey s1' 17 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | SPHINXPROJ = pynng 8 | SOURCEDIR = . 9 | BUILDDIR = build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | alabaster==0.7.13 2 | Babel==2.14.0 3 | certifi==2023.11.17 4 | cffi==1.16.0 5 | charset-normalizer==3.3.2 6 | docutils==0.20.1 7 | idna==3.6 8 | imagesize==1.4.1 9 | Jinja2==3.1.2 10 | MarkupSafe==2.1.3 11 | packaging==23.2 12 | pycparser==2.21 13 | Pygments==2.17.2 14 | requests==2.31.0 15 | sniffio==1.3.0 16 | snowballstemmer==2.2.0 17 | Sphinx==7.2.6 18 | sphinx-rtd-theme==2.0.0 19 | sphinxcontrib-applehelp==1.0.7 20 | sphinxcontrib-devhelp==1.0.5 21 | sphinxcontrib-htmlhelp==2.0.4 22 | sphinxcontrib-jquery==4.1 23 | sphinxcontrib-jsmath==1.0.1 24 | sphinxcontrib-qthelp==1.0.6 25 | sphinxcontrib-serializinghtml==1.1.9 26 | sphinxcontrib-trio==1.1.2 27 | urllib3==2.1.0 28 | -------------------------------------------------------------------------------- /docs/snippets/pushpull_sync.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | from pynng import Push0, Pull0, Timeout 4 | 5 | addr = "tcp://127.0.0.1:31313" 6 | with Push0(listen=addr) as push, Pull0(dial=addr, recv_timeout=100) as pull0, Pull0( 7 | dial=addr, recv_timeout=100 8 | ) as pull1: 9 | pass 10 | # give some time to connect 11 | time.sleep(0.01) 12 | push.send(b"hi some node") 13 | push.send(b"hi some other node") 14 | print(pull0.recv()) # prints b'hi some node' 15 | print(pull1.recv()) # prints b'hi some other node' 16 | try: 17 | pull0.recv() 18 | assert False, "Cannot get here, since messages are sent round robin" 19 | except Timeout: 20 | pass 21 | -------------------------------------------------------------------------------- /test/_test_util.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | 4 | def wait_pipe_len(sock, expected, timeout=10): 5 | """ 6 | Wait up to ``timeout`` seconds for the length of sock.pipes to become 7 | ``expected`` value. This prevents hardcoding sleep times, which should be 8 | pretty small for local development but potentially pretty large for CI 9 | runs. 10 | 11 | """ 12 | now = time.time() 13 | later = now + timeout 14 | while time.time() < later and len(sock.pipes) != expected: 15 | time.sleep(0.0005) 16 | if len(sock.pipes) != expected: 17 | raise TimeoutError( 18 | f"Waiting for {expected} pipes, but have {len(sock.pipes)} pipes" 19 | ) 20 | -------------------------------------------------------------------------------- /docs/snippets/bus0_sync.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | from pynng import Bus0, Timeout 4 | 5 | address = "tcp://127.0.0.1:13131" 6 | with Bus0(listen=address, recv_timeout=100) as s0, Bus0( 7 | dial=address, recv_timeout=100 8 | ) as s1, Bus0(dial=address, recv_timeout=100) as s2: 9 | # let all connections be established 10 | time.sleep(0.05) 11 | s0.send(b"hello buddies") 12 | s1.recv() # prints b'hello buddies' 13 | s2.recv() # prints b'hello buddies' 14 | s1.send(b"hi s0") 15 | print(s0.recv()) # prints b'hi s0' 16 | # s2 is not directly connected to s1. 17 | try: 18 | s2.recv() 19 | assert False, "this is never reached" 20 | except Timeout: 21 | print("s2 is not connected directly to s1!") 22 | -------------------------------------------------------------------------------- /docs/snippets/pair1_async.py: -------------------------------------------------------------------------------- 1 | from pynng import Pair1 2 | import trio 3 | 4 | 5 | async def polyamorous_send_and_recv(): 6 | address = "tcp://127.0.0.1:12343" 7 | with Pair1(listen=address, polyamorous=True) as s0, Pair1( 8 | dial=address, polyamorous=True 9 | ) as s1, Pair1(dial=address, polyamorous=True) as s2: 10 | await s1.asend(b"hello from s1") 11 | await s2.asend(b"hello from s2") 12 | msg1 = await s0.arecv_msg() 13 | msg2 = await s0.arecv_msg() 14 | print(msg1.bytes) # prints b'hello from s1' 15 | print(msg2.bytes) # prints b'hello from s2' 16 | await msg1.pipe.asend(b"hey s1") 17 | await msg2.pipe.asend(b"hey s2") 18 | print(await s2.arecv()) # prints b'hey s2' 19 | print(await s1.arecv()) # prints b'hey s1' 20 | 21 | 22 | trio.run(polyamorous_send_and_recv) 23 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=source 11 | set BUILDDIR=build 12 | set SPHINXPROJ=pynng 13 | 14 | if "%1" == "" goto help 15 | 16 | %SPHINXBUILD% >NUL 2>NUL 17 | if errorlevel 9009 ( 18 | echo. 19 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 20 | echo.installed, then set the SPHINXBUILD environment variable to point 21 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 22 | echo.may add the Sphinx directory to PATH. 23 | echo. 24 | echo.If you don't have Sphinx installed, grab it from 25 | echo.http://sphinx-doc.org/ 26 | exit /b 1 27 | ) 28 | 29 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 30 | goto end 31 | 32 | :help 33 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 34 | 35 | :end 36 | popd 37 | -------------------------------------------------------------------------------- /docs/snippets/surveyor_sync.py: -------------------------------------------------------------------------------- 1 | from pynng import Surveyor0, Respondent0, Timeout 2 | 3 | import time 4 | 5 | address = "tcp://127.0.0.1:13131" 6 | 7 | with Surveyor0(listen=address) as surveyor, Respondent0( 8 | dial=address 9 | ) as responder1, Respondent0(dial=address) as responder2: 10 | # give time for connections to happen 11 | time.sleep(0.1) 12 | surveyor.survey_time = 500 13 | surveyor.send(b"who wants to party?") 14 | # usually these would be in another thread or process, ya know? 15 | responder1.recv() 16 | responder2.recv() 17 | responder1.send(b"me me me!!!") 18 | responder2.send(b"I need to sit this one out.") 19 | 20 | # accept responses until the survey is finished. 21 | while True: 22 | try: 23 | response = surveyor.recv() 24 | if response == b"me me me!!!": 25 | print("all right, someone is ready to party!") 26 | elif response == b"I need to sit this one out.": 27 | print("Too bad, someone is not ready to party.") 28 | except Timeout: 29 | print("survey is OVER! Time for bed.") 30 | break 31 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright 2018-2019 Cody Piersall and other contributors 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"), 7 | to deal in the Software without restriction, including without limitation 8 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 9 | and/or sell copies of the Software, and to permit persons to whom 10 | the Software is furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included 13 | in all 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 18 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 21 | IN THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /pynng/__init__.py: -------------------------------------------------------------------------------- 1 | # provide the API 2 | from ._version import __version__ 3 | from ._nng import lib, ffi 4 | from .nng import ( 5 | Bus0, 6 | Pair0, 7 | Pair1, 8 | Pull0, 9 | Push0, 10 | Pub0, 11 | Sub0, 12 | Req0, 13 | Rep0, 14 | Surveyor0, 15 | Respondent0, 16 | Context, 17 | Socket, 18 | Listener, 19 | Dialer, 20 | Pipe, 21 | Message, 22 | ) 23 | 24 | from .tls import TLSConfig 25 | 26 | from .exceptions import ( 27 | NNGException, 28 | Interrupted, 29 | NoMemory, 30 | InvalidOperation, 31 | Busy, 32 | Timeout, 33 | ConnectionRefused, 34 | Closed, 35 | TryAgain, 36 | NotSupported, 37 | AddressInUse, 38 | BadState, 39 | NoEntry, 40 | ProtocolError, 41 | DestinationUnreachable, 42 | AddressInvalid, 43 | PermissionDenied, 44 | MessageTooLarge, 45 | ConnectionReset, 46 | ConnectionAborted, 47 | Canceled, 48 | OutOfFiles, 49 | OutOfSpace, 50 | AlreadyExists, 51 | ReadOnly, 52 | WriteOnly, 53 | CryptoError, 54 | AuthenticationError, 55 | NoArgument, 56 | Ambiguous, 57 | BadType, 58 | Internal, 59 | check_err, 60 | MessageStateError, 61 | ) 62 | -------------------------------------------------------------------------------- /new_release.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/bash 2 | 3 | # Create a new release of pynng. 4 | # This script ensures that the version in pynng/_version.py is consistent with the 5 | # repository tag. It then adds "+dev" the revision in pynng/_version.py, so that the 6 | # tagged release is on *exactly one* revision. 7 | # 8 | # This script changes the state of your repository, so be careful with it! 9 | function usage() { 10 | echo >&2 "$0 RELEASE" 11 | echo >&2 " RELEASE must be of the form major.minor.micro, e.g. 0.12.4" 12 | exit 13 | } 14 | 15 | if [ "$#" -lt "1" ]; then 16 | echo >&2 "Not enough args: Must provide a new release version" 17 | usage 18 | exit 19 | fi 20 | 21 | VERSION_FILE=pynng/_version.py 22 | 23 | new_version="$1" 24 | 25 | # make sure the new version actually has the right pattern 26 | if ! echo "$new_version" | grep -E "^[0-9]+\.[0-9]+\.[0-9]+$" > /dev/null; then 27 | echo >&2 "RELEASE must match pattern MAJOR.MINOR.MICRO" 28 | usage 29 | fi 30 | 31 | echo "__version__ = \"$new_version\"" > $VERSION_FILE 32 | git add $VERSION_FILE 33 | git commit -m "Version bump to v$new_version" 34 | git tag -a v"$new_version" -m "Release version $new_version." 35 | echo "__version__ = \"$new_version+dev\"" > $VERSION_FILE 36 | git add $VERSION_FILE 37 | git commit -m "Add +dev to version ($new_version+dev)" 38 | -------------------------------------------------------------------------------- /docs/snippets/pubsub_sync.py: -------------------------------------------------------------------------------- 1 | import time 2 | from pynng import Pub0, Sub0, Timeout 3 | 4 | address = "tcp://127.0.0.1:31313" 5 | with Pub0(listen=address) as pub, Sub0(dial=address, recv_timeout=100) as sub0, Sub0( 6 | dial=address, recv_timeout=100 7 | ) as sub1, Sub0(dial=address, recv_timeout=100) as sub2, Sub0( 8 | dial=address, recv_timeout=100 9 | ) as sub3: 10 | sub0.subscribe(b"wolf") 11 | sub1.subscribe(b"puppy") 12 | # The empty string matches everything! 13 | sub2.subscribe(b"") 14 | # we're going to send two messages before receiving anything, and this is 15 | # the only socket that needs to receive both messages. 16 | sub2.recv_buffer_size = 2 17 | # sub3 is not subscribed to anything 18 | # make sure everyone is connected 19 | time.sleep(0.05) 20 | 21 | pub.send(b"puppy: that is a cute dog") 22 | pub.send(b"wolf: that is a big dog") 23 | 24 | print(sub0.recv()) # prints b'wolf...' since that is the matching message 25 | print(sub1.recv()) # prints b'puppy...' since that is the matching message 26 | 27 | # sub2 will receive all messages (since empty string matches everything) 28 | print(sub2.recv()) # prints b'puppy...' since it was sent first 29 | print(sub2.recv()) # prints b'wolf...' since it was sent second 30 | 31 | try: 32 | sub3.recv() 33 | assert False, "never gets here since sub3 is not subscribed" 34 | except Timeout: 35 | print("got a Timeout since sub3 had no subscriptions") 36 | -------------------------------------------------------------------------------- /examples/pubsub.py: -------------------------------------------------------------------------------- 1 | """ 2 | This pattern is used to allow a single broadcaster to publish messages to many subscribers, 3 | which may choose to limit which messages they receive. 4 | """ 5 | 6 | import datetime 7 | import pynng 8 | import trio 9 | 10 | address = "ipc:///tmp/pubsub.ipc" 11 | 12 | 13 | def get_current_date(): 14 | return str(datetime.datetime.now()) 15 | 16 | 17 | async def server(sock): 18 | while True: 19 | date = get_current_date() 20 | print(f"SERVER: PUBLISHING DATE {date}") 21 | await sock.asend(date.encode()) 22 | await trio.sleep(1) 23 | 24 | 25 | async def client(name, max_msg=2): 26 | with pynng.Sub0() as sock: 27 | sock.subscribe("") 28 | sock.dial(address) 29 | 30 | while max_msg: 31 | msg = await sock.arecv_msg() 32 | print(f"CLIENT ({name}): RECEIVED {msg.bytes.decode()}") 33 | max_msg -= 1 34 | 35 | 36 | async def main(): 37 | # publisher publishes the date forever, once per second, and clients print as they 38 | # receive the date 39 | with pynng.Pub0(listen=address) as pub: 40 | async with trio.open_nursery() as nursery: 41 | nursery.start_soon(server, pub) 42 | nursery.start_soon(client, "client0", 2) 43 | nursery.start_soon(client, "client1", 3) 44 | nursery.start_soon(client, "client2", 4) 45 | 46 | 47 | if __name__ == "__main__": 48 | try: 49 | trio.run(main) 50 | except KeyboardInterrupt: 51 | # that's the way the program *should* end 52 | pass 53 | -------------------------------------------------------------------------------- /examples/pair0.py: -------------------------------------------------------------------------------- 1 | """ 2 | The simplest, most straightforward socket is the Pair0. It represents a 3 | pipe for one-to-one bidrectional communication. 4 | 5 | To demonstrate the script, run the following commands from two separate 6 | terminals: 7 | 8 | python pair0.py node0 ipc:///tmp/pair0_example # from terminal 1 9 | python pair0.py node1 ipc:///tmp/pair0_example # from terminal 2 10 | 11 | It can be fun to kill (just ^C) each node and restart it, to demonstrate how 12 | the sockets will automatically reconnect. 13 | 14 | """ 15 | 16 | 17 | import sys 18 | import time 19 | 20 | import pynng 21 | 22 | 23 | def usage(): 24 | """ 25 | print usage message and exit. 26 | 27 | """ 28 | print("Usage: {} node0|node1 URL".format(sys.argv[0])) 29 | sys.exit(1) 30 | 31 | 32 | def main(): 33 | if len(sys.argv) < 3: 34 | usage() 35 | node = sys.argv[1] 36 | if node not in ("node0", "node1"): 37 | usage() 38 | addr = sys.argv[2] 39 | with pynng.Pair0(recv_timeout=100, send_timeout=100) as sock: 40 | if node == "node0": 41 | sock.listen(addr) 42 | else: 43 | sock.dial(addr) 44 | while True: 45 | try: 46 | msg = sock.recv() 47 | print("got message from", msg.decode()) 48 | except pynng.Timeout: 49 | pass 50 | time.sleep(0.5) 51 | try: 52 | sock.send(node.encode()) 53 | except pynng.Timeout: 54 | pass 55 | 56 | 57 | if __name__ == "__main__": 58 | main() 59 | -------------------------------------------------------------------------------- /examples/pipeline.py: -------------------------------------------------------------------------------- 1 | """ 2 | Demonstrate how to use a pipeline socket. 3 | 4 | This pattern is useful for solving producer/consumer problems, including load-balancing. 5 | Messages flow from the push side to the pull side. 6 | If multiple peers are connected, the pattern attempts to distribute fairly. 7 | 8 | """ 9 | 10 | import pynng 11 | import trio 12 | 13 | address = "ipc:///tmp/pipeline.ipc" 14 | 15 | 16 | async def node0(sock): 17 | async def recv_eternally(): 18 | while True: 19 | try: 20 | msg = await sock.arecv_msg() 21 | except pynng.Timeout: 22 | break 23 | content = msg.bytes.decode() 24 | print(f'NODE0: RECEIVED "{content}"') 25 | 26 | return await recv_eternally() 27 | 28 | 29 | async def node1(message): 30 | with pynng.Push0() as sock: 31 | sock.dial(address) 32 | print(f'NODE1: SENDING "{message}"') 33 | await sock.asend(message.encode()) 34 | await trio.sleep(1) # wait for messages to flush before shutting down 35 | 36 | 37 | async def main(): 38 | # open a pull socket, and then open multiple Push sockets to push to them. 39 | with pynng.Pull0(listen=address, recv_timeout=200) as pull: 40 | async with trio.open_nursery() as nursery: 41 | nursery.start_soon(node0, pull) 42 | for msg in ["A", "B", "C", "D"]: 43 | nursery.start_soon(node1, msg) 44 | 45 | 46 | if __name__ == "__main__": 47 | try: 48 | trio.run(main) 49 | except KeyboardInterrupt: 50 | # that's the way the program *should* end 51 | pass 52 | -------------------------------------------------------------------------------- /examples/reqprep.py: -------------------------------------------------------------------------------- 1 | """ 2 | Request/Reply is used for synchronous communications where each question is responded with a single answer, 3 | for example remote procedure calls (RPCs). 4 | Like Pipeline, it also can perform load-balancing. 5 | This is the only reliable messaging pattern in the suite, as it automatically will retry if a request is not matched with a response. 6 | 7 | """ 8 | 9 | import datetime 10 | import pynng 11 | import trio 12 | 13 | DATE = "DATE" 14 | 15 | address = "ipc:///tmp/reqrep.ipc" 16 | 17 | 18 | async def node0(sock): 19 | while True: 20 | try: 21 | msg = await sock.arecv_msg() 22 | except pynng.Timeout: 23 | break 24 | content = msg.bytes.decode() 25 | if DATE == content: 26 | print("NODE0: RECEIVED DATE REQUEST") 27 | date = str(datetime.datetime.now()) 28 | await sock.asend(date.encode()) 29 | 30 | 31 | async def node1(): 32 | with pynng.Req0() as sock: 33 | sock.dial(address) 34 | print(f"NODE1: SENDING DATE REQUEST") 35 | await sock.asend(DATE.encode()) 36 | msg = await sock.arecv_msg() 37 | print(f"NODE1: RECEIVED DATE {msg.bytes.decode()}") 38 | 39 | 40 | async def main(): 41 | with pynng.Rep0(listen=address, recv_timeout=300) as rep: 42 | async with trio.open_nursery() as nursery: 43 | nursery.start_soon(node0, rep) 44 | nursery.start_soon(node1) 45 | 46 | 47 | if __name__ == "__main__": 48 | try: 49 | trio.run(main) 50 | except KeyboardInterrupt: 51 | # that's the way the program *should* end 52 | pass 53 | -------------------------------------------------------------------------------- /.github/workflows/cibuildwheel.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build_wheels: 7 | name: Build wheels on ${{ matrix.os }} 8 | runs-on: ${{ matrix.os }} 9 | strategy: 10 | matrix: 11 | os: [ 12 | ubuntu-24.04, 13 | ubuntu-24.04-arm, 14 | # windows-11-arm, 15 | windows-2025, 16 | macos-15, 17 | ] 18 | # don't let one failing job cancel all the others. 19 | fail-fast: false 20 | 21 | steps: 22 | - uses: actions/checkout@v5 23 | 24 | # Used to host cibuildwheel 25 | - uses: actions/setup-python@v5 26 | with: 27 | python-version: '3.13' 28 | 29 | - name: Install cibuildwheel 30 | run: python -m pip install cibuildwheel==3.1.4 31 | 32 | - name: Build wheels 33 | run: python -m cibuildwheel --output-dir wheelhouse 34 | 35 | - uses: actions/upload-artifact@v4 36 | with: 37 | name: cibw-wheels-${{ matrix.os }}-${{ strategy.job-index }} 38 | path: ./wheelhouse/*.whl 39 | 40 | # create the source distribution 41 | make_sdist: 42 | name: Make SDist 43 | runs-on: ubuntu-24.04 44 | steps: 45 | - uses: actions/checkout@v4 46 | with: 47 | fetch-depth: 0 # Optional, use if you use setuptools_scm 48 | submodules: true # Optional, use if you have submodules 49 | 50 | - name: Build SDist 51 | run: | 52 | pip install build 53 | pip install -e . 54 | python -m build --sdist 55 | 56 | - uses: actions/upload-artifact@v4 57 | with: 58 | path: dist/*.tar.gz 59 | -------------------------------------------------------------------------------- /generate_api.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # Uses sed and cpp to get nng.h into a form cffi can swallow. 4 | # removes #includes, 5 | echo "// THIS FILE WAS AUTOMATICALLY GENERATED BY $0" > nng_api.h 6 | process_header() { 7 | # remove includes; otherwise cpp chokes 8 | sed '/^#include/d' "$1" | \ 9 | # cpp, since cffi can't do includes 10 | cpp | \ 11 | # Strip all preprocessor directives 12 | sed 's/^#.*$//g' | \ 13 | # remove blank lines 14 | sed '/^$/d' | \ 15 | # remove NNG_DECL since we don't need it 16 | sed 's/^NNG_DECL *//g' 17 | } 18 | 19 | process_header nng/include/nng/nng.h | awk '1;/extern int nng_msg_getopt/{exit}'| head -n -1 >> nng_api.h 20 | process_header nng/include/nng/protocol/bus0/bus.h >> nng_api.h 21 | process_header nng/include/nng/protocol/pair0/pair.h >> nng_api.h 22 | process_header nng/include/nng/protocol/pair1/pair.h >> nng_api.h 23 | process_header nng/include/nng/protocol/pipeline0/push.h >> nng_api.h 24 | process_header nng/include/nng/protocol/pipeline0/pull.h >> nng_api.h 25 | process_header nng/include/nng/protocol/pubsub0/pub.h >> nng_api.h 26 | process_header nng/include/nng/protocol/pubsub0/sub.h >> nng_api.h 27 | process_header nng/include/nng/protocol/reqrep0/req.h >> nng_api.h 28 | process_header nng/include/nng/protocol/reqrep0/rep.h >> nng_api.h 29 | process_header nng/include/nng/protocol/survey0/survey.h >> nng_api.h 30 | process_header nng/include/nng/protocol/survey0/respond.h >> nng_api.h 31 | # nng_tls_config_{pass,key} have only stub implementations, and are 32 | # undefined when building with mbedtls. so we explicitly exclude them 33 | process_header nng/include/nng/supplemental/tls/tls.h | egrep -v "nng_tls_config_(pass|key)" >> nng_api.h 34 | process_header nng/include/nng/transport/tls/tls.h >> nng_api.h 35 | 36 | grep '^#define NNG_FLAG' nng/include/nng/nng.h >> nng_api.h 37 | grep '^#define NNG_.*_VERSION' nng/include/nng/nng.h >> nng_api.h 38 | -------------------------------------------------------------------------------- /examples/survey.py: -------------------------------------------------------------------------------- 1 | """ 2 | The surveyor pattern is used to send a timed survey out, 3 | responses are individually returned until the survey has expired. 4 | This pattern is useful for service discovery and voting algorithms. 5 | """ 6 | 7 | import datetime 8 | import pynng 9 | import trio 10 | 11 | 12 | DATE = "DATE" 13 | address = "ipc:///tmp/survey.ipc" 14 | 15 | 16 | def get_current_date(): 17 | return str(datetime.datetime.now()) 18 | 19 | 20 | async def server(sock, max_survey_request=10): 21 | while max_survey_request: 22 | print(f"SERVER: SENDING DATE SURVEY REQUEST") 23 | await sock.asend(DATE.encode()) 24 | while True: 25 | try: 26 | msg = await sock.arecv_msg() 27 | print(f'SERVER: RECEIVED "{msg.bytes.decode()}" SURVEY RESPONSE') 28 | except pynng.Timeout: 29 | break 30 | print("SERVER: SURVEY COMPLETE") 31 | max_survey_request -= 1 32 | 33 | 34 | async def client(name, max_survey=3): 35 | with pynng.Respondent0() as sock: 36 | sock.dial(address) 37 | while max_survey: 38 | await sock.arecv_msg() 39 | print(f'CLIENT ({name}): RECEIVED SURVEY REQUEST"') 40 | print(f"CLIENT ({name}): SENDING DATE SURVEY RESPONSE") 41 | await sock.asend(get_current_date().encode()) 42 | max_survey -= 1 43 | 44 | 45 | async def main(): 46 | with pynng.Surveyor0(listen=address) as surveyor: 47 | async with trio.open_nursery() as nursery: 48 | nursery.start_soon(server, surveyor) 49 | nursery.start_soon(client, "client0", 3) 50 | nursery.start_soon(client, "client1", 3) 51 | nursery.start_soon(client, "client2", 4) 52 | 53 | 54 | if __name__ == "__main__": 55 | try: 56 | trio.run(main) 57 | except KeyboardInterrupt: 58 | # that's the way the program *should* end 59 | pass 60 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | Examples 2 | ======== 3 | 4 | This directory contains examples on using pynng for different tasks. 5 | 6 | * Pair (Two Way Radio) [`pair0.py`](./pair0.py): 7 | Demonstrates the basic bi-directional connection, pair0. 8 | Adapted from [nng pair example](https://nanomsg.org/gettingstarted/nng/pair.html). 9 | * Polyamorous pair1 connection : 10 | - [`pair1_async.py`](./pair1_async.py): Demonstrates using trio. 11 | - [`pair1_async_curio.py`](./pair1_async.py): Demonstrates using curio. 12 | * Pipeline (A One-Way Pipe) : 13 | [`pipeline.py`](./pair1_async.py): Push/Pull adapted from [nng example](https://nanomsg.org/gettingstarted/nng/pipeline.html) 14 | * Request/Reply (I ask, you answer) : 15 | [`reqprep.py`](./reqprep.py): Rep0/Req0 adapted from [nng example](https://nanomsg.org/gettingstarted/nng/pipeline.html) 16 | * Pub/Sub (Topics & Broadcast): 17 | [`pubsub.py`](./pubsub.py): Pub0/Sub0 adapted from [nng example](https://nanomsg.org/gettingstarted/nng/pubsub.html) 18 | * Survey (Everybody Votes): 19 | [`survey.py`](./survey.py): Surveyor0/Respondent0 adapted from [nng example](https://nanomsg.org/gettingstarted/nng/survey.html) 20 | * Bus (Routing): 21 | [`bus.py`](./bus.py): Bus0 adapted from [nng example](https://nanomsg.org/gettingstarted/nng/bus.html) 22 | 23 | 24 | 25 | Adding an Example 26 | ----------------- 27 | 28 | More examples are welcome. To add an example: 29 | 1. create a new file in this directory that demonstrates what needs to be 30 | demonstrated 31 | 2. Make sure there is a docstring in the file that describes what it is 32 | demonstrating. Add as much detail as is needed. Show example in the 33 | docstring as well. 34 | 3. Add a short description of what the example does to the list of descriptions 35 | in this file. Keep the list in alphabetical order. 36 | 37 | Don't call out to third-party code if you can help it. Keep examples simple. 38 | If you are adding an async example, use 39 | [trio](https://trio.readthedocs.io/en/latest/). 40 | -------------------------------------------------------------------------------- /examples/bus.py: -------------------------------------------------------------------------------- 1 | """ 2 | The bus protocol is useful for routing applications, or for building fully interconnected mesh networks. 3 | In this pattern, messages are sent to every directly connected peer. 4 | """ 5 | 6 | import pynng 7 | import trio 8 | 9 | trio = trio 10 | 11 | 12 | async def node(name, listen_address, contacts): 13 | with pynng.Bus0(listen=listen_address, recv_timeout=300) as sock: 14 | await trio.sleep(0.2) # wait for peers to bind 15 | for contact in contacts: 16 | sock.dial(contact) 17 | 18 | await trio.sleep(0.2) 19 | # wait for connects to establish 20 | print(f"{name}: SENDING '{listen_address}' ONTO BUS") 21 | await sock.asend(listen_address.encode()) 22 | 23 | while True: 24 | try: 25 | msg = await sock.arecv_msg() 26 | print(f'{name}: RECEIVED "{msg.bytes.decode()}" FROM BUS') 27 | except pynng.Timeout: 28 | print(f"{name}: Timeout") 29 | break 30 | 31 | 32 | async def main(): 33 | async with trio.open_nursery() as nursery: 34 | nursery.start_soon( 35 | node, 36 | "node0", 37 | "ipc:///tmp/node0.ipc", 38 | ["ipc:///tmp/node1.ipc", "ipc:///tmp/node2.ipc"], 39 | ) 40 | nursery.start_soon( 41 | node, 42 | "node1", 43 | "ipc:///tmp/node1.ipc", 44 | ["ipc:///tmp/node2.ipc", "ipc:///tmp/node3.ipc"], 45 | ) 46 | nursery.start_soon( 47 | node, 48 | "node2", 49 | "ipc:///tmp/node2.ipc", 50 | ["ipc:///tmp/node3.ipc"], 51 | ) 52 | nursery.start_soon( 53 | node, 54 | "node3", 55 | "ipc:///tmp/node3.ipc", 56 | ["ipc:///tmp/node0.ipc"], 57 | ) 58 | 59 | 60 | if __name__ == "__main__": 61 | try: 62 | trio.run(main) 63 | except KeyboardInterrupt: 64 | # that's the way the program *should* end 65 | pass 66 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "pynng" 3 | authors = [ 4 | {name = "Cody Piersall", email = "cody.piersall@gmail.com"}, 5 | ] 6 | classifiers = [ 7 | "Development Status :: 3 - Alpha", 8 | "Framework :: AsyncIO", 9 | "Framework :: Trio", 10 | "License :: OSI Approved :: MIT License", 11 | "Operating System :: OS Independent", 12 | "Programming Language :: Python :: 3 :: Only", 13 | "Programming Language :: Python :: 3.6", 14 | "Programming Language :: Python :: 3.7", 15 | "Programming Language :: Python :: 3.8", 16 | "Programming Language :: Python :: 3.9", 17 | "Programming Language :: Python :: 3.10", 18 | "Topic :: Software Development :: Libraries", 19 | "Topic :: System :: Networking", 20 | ] 21 | dependencies = [ 22 | "cffi", 23 | "sniffio", 24 | ] 25 | description = "Networking made simply using nng" 26 | # Note: when we drop Python 3.8 we can use the new license version and CI won't yell at 27 | # setuptools 3.8 requires this version. 28 | # uncomment when dropping python3.8 29 | # license = "MIT" 30 | license = {file = "LICENSE.txt"} 31 | dynamic = ["version"] 32 | keywords = [ 33 | "networking", 34 | "nng", 35 | "nanomsg", 36 | "zmq", 37 | "messaging", 38 | "message", 39 | "trio", 40 | "asyncio", 41 | ] 42 | readme = "README.md" 43 | 44 | [project.optional-dependencies] 45 | dev = [ 46 | "pytest", 47 | "pytest-asyncio", 48 | "pytest-trio", 49 | "trio", 50 | ] 51 | 52 | docs = [ 53 | "sphinx", 54 | "sphinx-rtd-theme", 55 | "sphinxcontrib-trio", 56 | ] 57 | 58 | [project.urls] 59 | Homepage = "https://github.com/codypiersall/pynng" 60 | Source = "https://github.com/codypiersall/pynng" 61 | Documentation = "https://pynng.readthedocs.io/en/latest/" 62 | 63 | [tool.setuptools.dynamic] 64 | version = {attr = "pynng._version.__version__"} 65 | [tool.setuptools.packages.find] 66 | include = ["pynng"] 67 | 68 | [build-system] 69 | build-backend = "setuptools.build_meta" 70 | requires = [ 71 | "setuptools==75.3.2", 72 | "setuptools-scm", 73 | "cffi", 74 | "cmake", 75 | "build", 76 | "wheel", 77 | ] 78 | 79 | [tool.cibuildwheel] 80 | test-requires = [ 81 | "pytest", 82 | "pytest-asyncio", 83 | "pytest-trio", 84 | "trio", 85 | ] 86 | 87 | test-command = "pytest {project}/test" 88 | 89 | # 3.14 does not have cffi support yet (Sep. 2025) 90 | skip = "cp314*" 91 | 92 | build-verbosity = 1 93 | 94 | [tool.cibuildwheel.windows] 95 | # Windows builds fail because they try to use the same directory for different 96 | # architectures, and that's no good. 97 | before-build = "bash .github/rmstuff.sh" 98 | 99 | [tool.cibuildwheel.macos] 100 | # Specify universal2 building for macOS 101 | archs = ["universal2"] 102 | environment = { CMAKE_OSX_ARCHITECTURES="x86_64;arm64" } 103 | 104 | [tool.black] 105 | # Just a placeholder because I have a plugin that won't run without a section in 106 | # pyproject.toml 107 | target-version = ["py38"] 108 | 109 | # pytest configuration 110 | [tool.pytest.ini_options] 111 | addopts = "--capture=no --verbose" 112 | testpaths = "test" 113 | -------------------------------------------------------------------------------- /examples/pair1_async.py: -------------------------------------------------------------------------------- 1 | """ 2 | Demonstrate how to use a pair1 socket. 3 | 4 | Pair1 sockets are similar to pair0 sockets. The difference is that while pair0 5 | supports only a single connection, pair1 sockets support _n_ one-to-one 6 | connections. 7 | 8 | This program demonstrates how to use pair1 sockets. The key differentiator is 9 | that with pair1 sockets, you must always specify the *pipe* that you want to 10 | use for the message. 11 | 12 | To use this program, you must start several nodes. One node will be the 13 | listener, and all other nodes will be dialers. In one terminal, you must start 14 | a listener: 15 | 16 | python pair1_async.py listen tcp://127.0.0.1:12345 17 | 18 | And in as many separate terminals as you would like, start some dialers: 19 | 20 | # run in as many separate windows as you wish 21 | python pair1_async.py dial tcp://127.0.0.1:12345 22 | 23 | Whenever you type into the dialer processes, whatever you type is received on 24 | the listening process. Whatever you type into the listening process is sent to 25 | *all* the dialing processes. 26 | 27 | """ 28 | 29 | import argparse 30 | 31 | import pynng 32 | import trio 33 | 34 | 35 | try: 36 | run_sync = trio.to_thread.run_sync 37 | except AttributeError: 38 | # versions of trio prior to 0.12.0 used this method 39 | run_sync = trio.run_sync_in_worker_thread 40 | 41 | 42 | async def send_eternally(sock): 43 | """ 44 | Eternally listen for user input in the terminal and send it on all 45 | available pipes. 46 | """ 47 | while True: 48 | stuff = await run_sync(input, cancellable=True) 49 | for pipe in sock.pipes: 50 | await pipe.asend(stuff.encode()) 51 | 52 | 53 | async def recv_eternally(sock): 54 | while True: 55 | msg = await sock.arecv_msg() 56 | source_addr = str(msg.pipe.remote_address) 57 | content = msg.bytes.decode() 58 | print("{} says: {}".format(source_addr, content)) 59 | 60 | 61 | async def main(): 62 | p = argparse.ArgumentParser(description=__doc__) 63 | p.add_argument( 64 | "mode", 65 | help='Whether the socket should "listen" or "dial"', 66 | choices=["listen", "dial"], 67 | ) 68 | p.add_argument( 69 | "addr", 70 | help="Address to listen or dial; e.g. tcp://127.0.0.1:13134", 71 | ) 72 | args = p.parse_args() 73 | 74 | with pynng.Pair1(polyamorous=True) as sock: 75 | async with trio.open_nursery() as n: 76 | if args.mode == "listen": 77 | # the listening socket can get dialled by any number of dialers. 78 | # add a couple callbacks to see when the socket is receiving 79 | # connections. 80 | def pre_connect_cb(pipe): 81 | addr = str(pipe.remote_address) 82 | print("~~~~got connection from {}".format(addr)) 83 | 84 | def post_remove_cb(pipe): 85 | addr = str(pipe.remote_address) 86 | print("~~~~goodbye for now from {}".format(addr)) 87 | 88 | sock.add_pre_pipe_connect_cb(pre_connect_cb) 89 | sock.add_post_pipe_remove_cb(post_remove_cb) 90 | sock.listen(args.addr) 91 | else: 92 | sock.dial(args.addr) 93 | n.start_soon(recv_eternally, sock) 94 | n.start_soon(send_eternally, sock) 95 | 96 | 97 | if __name__ == "__main__": 98 | try: 99 | trio.run(main) 100 | except KeyboardInterrupt: 101 | # that's the way the program *should* end 102 | pass 103 | -------------------------------------------------------------------------------- /docs/developing.rst: -------------------------------------------------------------------------------- 1 | Pynng Developer Notes 2 | ===================== 3 | 4 | A list of notes, useful only to developers of the library, and not for users. 5 | 6 | Testing without pulling dependencies from GitHub 7 | ------------------------------------------------ 8 | 9 | It can lower feedback speed dramatically when testing pynng, and needing to clone 10 | `nng`_ and `mbedtls`_ from GitHub. If you are just doing this 11 | one time, it is not terrible, but when you're working on *how pynng is built* it is 12 | tedious and painful to wait for a slow internet connection. This can be mitigated by 13 | cloning nng and mbedtls outside of this repository, running a git server, and pointing 14 | the ``setup.cfg`` script to 15 | 16 | .. code-block:: bash 17 | 18 | # clone *once* locally 19 | git clone https://github.com/nanomsg/nng ~/pynng-deps/nanomsg/nng 20 | git clone https://github.com/Mbed-TLS/mbedtls ~/pynng-deps/Mbed-TLS/mbedtls 21 | # start a git daemon in the parent directory 22 | git daemon --reuseaddr --base-path="$HOME/pynng-deps" --export-all 23 | 24 | Then change the ``setup.cfg`` to point to the local git server. Change the ``repo`` 25 | lines from ``repo=https://github.com/nanomsg/nng`` to 26 | ``repo=git://127.0.0.1:/nanomsg/nng``. The relevant sections of the file will look like 27 | this: 28 | 29 | .. code-block:: cfg 30 | 31 | [build_nng] 32 | repo=git://127.0.0.1:/nanomsg/nng 33 | 34 | [build_mbedtls] 35 | repo=git://127.0.0.1:/Mbed-TLS/mbedtls 36 | 37 | Testing CI changes 38 | ------------------ 39 | 40 | When testing CI changes, it can be painful, embarrassing, and tedious, to push changes 41 | just to see how CI does. Sometimes this is necessary, for example for architectures or 42 | operating systems you do not own so cannot test on. However, you *can* test CI somewhat 43 | using the incredible `nektos/act`_ tool. It enables running Github Actions locally. We 44 | do need to pass some flags to make the tool do what we want. 45 | 46 | If you have single failing tests, you can narrow down the build by setting specific 47 | `cibuildwheel options`_ in the ``pyproject.toml`` file, to skip certain Python versions 48 | or architectures. 49 | 50 | Running CI Locally 51 | ################## 52 | 53 | Use this command to run Github Actions locally using the `nektos/act`_ tool 54 | 55 | .. code-block:: bash 56 | 57 | # run cibuildwheel, using ubuntu-20.04 image 58 | # This is how you test on Linux 59 | # Needs --container-options='-u root' so cibuildwheel can launch its own docker containers 60 | act --container-options='-u root' \ 61 | -W .github/workflows/cibuildwheel.yml \ 62 | --matrix os:ubuntu-20.04 \ 63 | --pull=false \ 64 | --artifact-server-path=artifacts 65 | 66 | ``--pull=false`` prevents downloading the latest runner docker image. 67 | ``--artifact-server-path=artifacts`` enables an artifact server, and lets you look at 68 | the built artifacts afterwards. 69 | 70 | Making a new release 71 | -------------------- 72 | 73 | Making a new release is a potentially error-prone process; the file 74 | ``pynng/_version.py`` must be increased, the change must be committed, the commit must 75 | be tagged, and the following commit should append "+dev" to the version. CI should run, 76 | then the artifacts need to be collected and pushed to PyPI. The ``sdist`` is built 77 | manually. 78 | 79 | Anyway, there is a script to automate this: 80 | 81 | .. literalinclude:: ../new_release.sh 82 | :language: bash 83 | 84 | .. _cibuildwheel options: https://cibuildwheel.readthedocs.io/en/stable/options/ 85 | .. _mbedtls: https://github.com/Mbed-TLS/mbedtls 86 | .. _nektos/act: https://github.com/nektos/act 87 | .. _nng: https://github.com/nanomsg/nng 88 | 89 | -------------------------------------------------------------------------------- /test/test_options.py: -------------------------------------------------------------------------------- 1 | import pynng.options 2 | import pytest 3 | from pathlib import Path 4 | 5 | PORT = 13131 6 | IP = "127.0.0.1" 7 | tcp_addr = "tcp://{}:{}".format(IP, PORT) 8 | addr = "inproc://test-addr" 9 | 10 | 11 | def test_timeout_works(): 12 | with pynng.Pair0(listen=addr) as s0: 13 | # default is -1 14 | assert s0.recv_timeout == -1 15 | s0.recv_timeout = 1 # 1 ms, not too long 16 | with pytest.raises(pynng.Timeout): 17 | s0.recv() 18 | 19 | 20 | def test_can_set_socket_name(): 21 | with pynng.Pair0() as p: 22 | assert p.name != "this" 23 | p.name = "this" 24 | assert p.name == "this" 25 | # make sure we're actually testing the right thing, not just setting an 26 | # attribute on the socket 27 | assert pynng.options._getopt_string(p, "socket-name") == "this" 28 | 29 | 30 | def test_can_read_sock_raw(): 31 | with pynng.Pair0() as cooked, pynng.Pair0( 32 | opener=pynng.lib.nng_pair0_open_raw 33 | ) as raw: 34 | assert not cooked.raw 35 | assert raw.raw 36 | 37 | 38 | def test_dial_blocking_behavior(): 39 | # the default dial is different than the pynng library; it will log in the 40 | # event of a failure, but then continue. 41 | with pynng.Pair1() as s0, pynng.Pair1() as s1: 42 | with pytest.raises(pynng.ConnectionRefused): 43 | s0.dial(addr, block=True) 44 | 45 | # default is to attempt 46 | s0.dial(addr) 47 | s1.listen(addr) 48 | s0.send(b"what a message") 49 | assert s1.recv() == b"what a message" 50 | 51 | 52 | def test_can_set_recvmaxsize(): 53 | with pynng.Pair1( 54 | recv_timeout=50, recv_max_size=100, listen=tcp_addr 55 | ) as s0, pynng.Pair1(dial=tcp_addr) as s1: 56 | listener = s0.listeners[0] 57 | msg = b"\0" * 101 58 | assert listener.recv_max_size == s0.recv_max_size 59 | s1.send(msg) 60 | with pytest.raises(pynng.Timeout): 61 | s0.recv() 62 | 63 | 64 | def test_nng_sockaddr(): 65 | with pynng.Pair1(recv_timeout=50, listen=tcp_addr) as s0: 66 | sa = s0.listeners[0].local_address 67 | assert isinstance(sa, pynng.sockaddr.InAddr) 68 | # big-endian 69 | expected_port = (PORT >> 8) | ((PORT & 0xFF) << 8) 70 | assert sa.port == expected_port 71 | # big-endian 72 | ip_parts = [int(x) for x in IP.split(".")] 73 | expected_addr = ( 74 | ip_parts[0] | ip_parts[1] << 8 | ip_parts[2] << 16 | ip_parts[3] << 24 75 | ) 76 | assert expected_addr == sa.addr 77 | assert str(sa) == tcp_addr.replace("tcp://", "") 78 | 79 | path = "/tmp/thisisipc" 80 | with pynng.Pair1(recv_timeout=50, listen="ipc://{}".format(path)) as s0: 81 | sa = s0.listeners[0].local_address 82 | assert isinstance(sa, pynng.sockaddr.IPCAddr) 83 | assert sa.path == path 84 | assert str(sa) == path 85 | 86 | url = "inproc://thisisinproc" 87 | with pynng.Pair1(recv_timeout=50, listen=url) as s0: 88 | sa = s0.listeners[0].local_address 89 | assert str(sa) == url 90 | 91 | # skip ipv6 test when running in Docker 92 | if Path("/.dockerenv").exists(): 93 | return 94 | 95 | ipv6 = "tcp://[::1]:13131" 96 | with pynng.Pair1(recv_timeout=50, listen=ipv6) as s0: 97 | sa = s0.listeners[0].local_address 98 | assert isinstance(sa, pynng.sockaddr.In6Addr) 99 | assert sa.addr == b"\x00" * 15 + b"\x01" 100 | assert str(sa) == ipv6.replace("tcp://", "") 101 | 102 | 103 | def test_resend_time(): 104 | # test req/rep resend time 105 | with pynng.Rep0(listen=addr, recv_timeout=3000) as rep, pynng.Req0( 106 | dial=addr, recv_timeout=3000, resend_time=100 107 | ) as req: 108 | req.send(b"hey i have a question for you") 109 | rep.recv() 110 | # if it doesn't resend we'll never receive the second time 111 | rep.recv() 112 | rep.send(b"well i have an answer") 113 | req.recv() 114 | -------------------------------------------------------------------------------- /test/test_aio.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import itertools 3 | 4 | import pytest 5 | import trio 6 | 7 | import pynng 8 | 9 | addr = "inproc://test-addr" 10 | 11 | 12 | @pytest.mark.trio 13 | async def test_arecv_asend_asyncio(): 14 | with pynng.Pair0(listen=addr, recv_timeout=1000) as listener, pynng.Pair0( 15 | dial=addr 16 | ) as dialer: 17 | await dialer.asend(b"hello there buddy") 18 | assert (await listener.arecv()) == b"hello there buddy" 19 | 20 | 21 | @pytest.mark.trio 22 | async def test_asend_arecv_trio(): 23 | with pynng.Pair0(listen=addr, recv_timeout=2000) as listener, pynng.Pair0( 24 | dial=addr, send_timeout=2000 25 | ) as dialer: 26 | await dialer.asend(b"hello there") 27 | assert (await listener.arecv()) == b"hello there" 28 | 29 | 30 | @pytest.mark.trio 31 | async def test_arecv_trio_cancel(): 32 | with pynng.Pair0(listen=addr, recv_timeout=5000) as p0: 33 | with pytest.raises(trio.TooSlowError): 34 | with trio.fail_after(0.001): 35 | await p0.arecv() 36 | 37 | 38 | @pytest.mark.asyncio 39 | async def test_arecv_asyncio_cancel(): 40 | async def cancel_soon(fut, sleep_time=0.05): 41 | # need to sleep for some amount of time to ensure the arecv actually 42 | # had time to start. 43 | await asyncio.sleep(sleep_time) 44 | fut.cancel() 45 | 46 | with pynng.Pair0(listen=addr, recv_timeout=5000) as p0: 47 | arecv = p0.arecv() 48 | fut = asyncio.ensure_future(arecv) 49 | with pytest.raises(asyncio.CancelledError): 50 | await asyncio.gather(fut, cancel_soon(fut)) 51 | 52 | 53 | @pytest.mark.asyncio 54 | async def test_asend_asyncio_send_timeout(): 55 | with pytest.raises(pynng.exceptions.Timeout): 56 | with pynng.Pair0(listen=addr, send_timeout=1) as p0: 57 | await p0.asend(b"foo") 58 | 59 | 60 | @pytest.mark.trio 61 | async def test_asend_trio_send_timeout(): 62 | with pytest.raises(pynng.exceptions.Timeout): 63 | with pynng.Pair0(listen=addr, send_timeout=1) as p0: 64 | await p0.asend(b"foo") 65 | 66 | 67 | @pytest.mark.trio 68 | async def test_pub_sub_trio(): 69 | """Demonstrate pub-sub protocol use with ``trio``. 70 | 71 | Start a publisher which publishes 1000 integers and marks each value 72 | as *even* or *odd* (its parity). Spawn 4 subscribers (2 for consuming 73 | the evens and 2 for consuming the odds) in separate tasks and have each 74 | one retreive values and verify the parity. 75 | """ 76 | sentinel_received = {} 77 | 78 | def is_even(i): 79 | return i % 2 == 0 80 | 81 | async def pub(): 82 | with pynng.Pub0(listen=addr) as pubber: 83 | for i in range(20): 84 | prefix = "even" if is_even(i) else "odd" 85 | msg = "{}:{}".format(prefix, i) 86 | await pubber.asend(msg.encode("ascii")) 87 | 88 | while not all(sentinel_received.values()): 89 | # signal completion 90 | await pubber.asend(b"odd:None") 91 | await pubber.asend(b"even:None") 92 | 93 | async def subs(which): 94 | if which == "even": 95 | pred = is_even 96 | else: 97 | pred = lambda i: not is_even(i) 98 | 99 | with pynng.Sub0(dial=addr, recv_timeout=5000) as subber: 100 | subber.subscribe(which + ":") 101 | 102 | while True: 103 | val = await subber.arecv() 104 | 105 | lot, _, i = val.partition(b":") 106 | 107 | if i == b"None": 108 | break 109 | 110 | assert pred(int(i)) 111 | 112 | # mark subscriber as having received None sentinel 113 | sentinel_received[which] = True 114 | 115 | async with trio.open_nursery() as n: 116 | # whip up the subs 117 | for _, lot in itertools.product(range(1), ("even", "odd")): 118 | sentinel_received[lot] = False 119 | n.start_soon(subs, lot) 120 | 121 | # head over to the pub 122 | await pub() 123 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. pynng documentation master file, created by 2 | sphinx-quickstart on Mon Aug 20 21:41:14 2018. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | This is Pynng's Documentation. 7 | ============================== 8 | 9 | pynng is Python bindings to `Nanomsg Next Generation`_ (nng). It provides a 10 | nice Pythonic interface to the nng library. The goal is that pynng's interface 11 | feels natural enough to use that you don't think of it as a wrapper, while 12 | still exposing the power of the underlying library. It is installable with 13 | pip on all major platforms (Linux, Windows, macOS). It has first class support 14 | for `Trio`_ and :mod:`asyncio`, in addition to being able to be used 15 | synchronously. 16 | 17 | nng is an 18 | implementation of the `Scalability Protocols`_; it is the spiritual successor 19 | to `ZeroMQ`_. There are a couple of distributed systems problems that the 20 | scalability protocols aim to solve: 21 | 22 | 1. There are a few communication patterns that are implemented over and over 23 | and over and over and over again. The wheel is continuously reinvented, but 24 | no implementations are compatible with each other. 25 | 2. Not only is the wheel continuosly reinvented, it is reinvented for every 26 | combination of *transport* and *protocol*. A *transport* is how data gets 27 | from one place to another; things like TCP/IP, HTTP, Unix sockets, carrier 28 | pigeons. A *protocol* is the way that both sides have agreed to communicate 29 | with each other (some protocols are ad-hoc, and some are more formal). 30 | 31 | The scalability protocols are the basic tools you need to build a distributed 32 | system. The following **protocols** are available: 33 | 34 | * **pair** - simple one-to-one communication. (:class:`~pynng.Pair0`, 35 | :class:`~pynng.Pair1`.) 36 | * **request/response** - I ask, you answer. (:class:`~pynng.Req0`, 37 | :class:`~pynng.Rep0`) 38 | * **pub/sub** - subscribers are notified of topics they are interested in. 39 | (:class:`~pynng.Pub0`, :class:`~pynng.Sub0`) 40 | * **pipeline**, aka **push/pull** - load balancing. 41 | (:class:`~pynng.Push0`, :class:`~pynng.Pull0`) 42 | * **survey** - query the state of multiple applications. 43 | (:class:`~pynng.Surveyor0`, :class:`~pynng.Respondent0`) 44 | * **bus** - messages are sent to all connected sockets (:class:`~pynng.Bus0`) 45 | 46 | The following **transports** are available: 47 | 48 | * **inproc**: communication within a single process. 49 | * **ipc**: communication across processes on a single machine. 50 | * **tcp**: communication over networks via tcp. 51 | * **ws**: communication over networks with websockets. (Probably only useful if 52 | one end is on a browser.) 53 | * **tls+tcp**: Encrypted `TLS`_ communication over networks. 54 | * **carrier pigeons**: communication via World War 1-style `carrier pigeons`_. 55 | The latency is pretty high on this one. 56 | 57 | These protocols are language-agnostic, and `implementations exist for many 58 | languages `_. 59 | 60 | This library is available under the `MIT License`_ and the source is available 61 | on `GitHub`_. 62 | 63 | If you need two processes to talk to each other—either locally or remotely—you 64 | should be using the scalability protocols. You never need to open another plain 65 | `socket`_ again. 66 | 67 | Okay, that was a little hyperbolic. But give pynng a chance; you might like 68 | it. 69 | 70 | Installing pynng 71 | ---------------- 72 | 73 | On Linux, Windows, and macOS, a quick 74 | 75 | .. code-block:: python 76 | 77 | pip3 install pynng 78 | 79 | should do the trick. pynng works on Python 3.6+. 80 | 81 | 82 | Getting Started 83 | --------------- 84 | 85 | .. toctree:: 86 | :maxdepth: 2 87 | 88 | core 89 | exceptions 90 | developing 91 | 92 | 93 | Indices and tables 94 | ================== 95 | 96 | * :ref:`genindex` 97 | * :ref:`modindex` 98 | * :ref:`search` 99 | 100 | .. _Nanomsg Next Generation: https://github.com/nanomsg/nng 101 | .. _Scalability Protocols: https://nanomsg.org 102 | .. _ZeroMQ: https://zeromq.org 103 | .. _Unix sockets: http://man7.org/linux/man-pages/man7/unix.7.html 104 | .. _carrier pigeons: https://en.wikipedia.org/wiki/IP_over_Avian_Carriers 105 | .. _socket: http://man7.org/linux/man-pages/man2/socket.2.html 106 | .. _TLS: https://en.wikipedia.org/wiki/Transport_Layer_Security 107 | .. _MIT License: https://github.com/codypiersall/pynng/blob/master/LICENSE.txt 108 | .. _GitHub: https://github.com/codypiersall/pynng 109 | .. _Trio: https://trio.readthedocs.io 110 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This is pynng. 2 | ============== 3 | 4 | [![MIT License](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/codypiersall/pynng/blob/master/LICENSE.txt) 5 | [![PyPI Version](https://img.shields.io/pypi/v/pynng.svg)](https://pypi.org/project/pynng) 6 | [![smoketest](https://github.com/codypiersall/pynng/actions/workflows/smoketest.yml/badge.svg?branch=master)](https://github.com/codypiersall/pynng/actions/workflows/smoketest.yml) 7 | [![Build](https://github.com/codypiersall/pynng/actions/workflows/cibuildwheel.yml/badge.svg?branch=master)](https://github.com/codypiersall/pynng/actions/workflows/cibuildwheel.yml) 8 | [![docs](https://img.shields.io/readthedocs/pynng.svg)](https://pynng.readthedocs.io) 9 | 10 | Ergonomic bindings for [nanomsg next generation] \(nng), in Python. 11 | pynng provides a nice interface on top of the full power of nng. nng, and 12 | therefore pynng, make it easy to communicate between processes on a single 13 | computer or computers across a network. This library is compatible with Python 14 | ≥ 3.6. nng is the [rewriting](https://nanomsg.github.io/nng/RATIONALE.html) of 15 | [Nanomsg](https://nanomsg.org/), which is the spiritual successor to [ZeroMQ](http://zeromq.org/). 16 | 17 | Goals 18 | ----- 19 | 20 | Provide a Pythonic, works-out-of-the box library on Windows and Unix-y 21 | platforms. Like nng itself, the license is MIT, so it can be used without 22 | restriction. 23 | 24 | Installation 25 | ------------ 26 | 27 | On Windows, MacOS, and Linux, the usual 28 | 29 | pip3 install pynng 30 | 31 | should suffice. Note that on 32-bit Linux and on macOS no binary distributions 32 | are available, so [CMake](https://cmake.org/) is also required. 33 | 34 | Building from the GitHub repo works as well, natch: 35 | 36 | git clone https://github.com/codypiersall/pynng 37 | cd pynng 38 | pip3 install -e . 39 | 40 | (If you want to run tests, you also need to `pip3 install trio curio pytest pytest-asyncio pytest-trio pytest-curio`, 41 | then just run `pytest test`.) 42 | 43 | pynng might work on the BSDs as well. Who knows! 44 | 45 | Using pynng 46 | ----------- 47 | 48 | Using pynng is easy peasy: 49 | 50 | ```python 51 | from pynng import Pair0 52 | 53 | s1 = Pair0() 54 | s1.listen('tcp://127.0.0.1:54321') 55 | s2 = Pair0() 56 | s2.dial('tcp://127.0.0.1:54321') 57 | s1.send(b'Well hello there') 58 | print(s2.recv()) 59 | s1.close() 60 | s2.close() 61 | ``` 62 | 63 | Since pynng sockets support setting most parameters in the socket's `__init__` 64 | method and is a context manager, the above code can be written much shorter: 65 | 66 | ```python 67 | from pynng import Pair0 68 | 69 | with Pair0(listen='tcp://127.0.0.1:54321') as s1, \ 70 | Pair0(dial='tcp://127.0.0.1:54321') as s2: 71 | s1.send(b'Well hello there') 72 | print(s2.recv()) 73 | ``` 74 | 75 | ### Using pynng with an async framework 76 | 77 | Asynchronous sending also works with 78 | 79 | [curio](https://github.com/dabeaz/curio), [trio](https://trio.readthedocs.io/en/latest/) and 80 | [asyncio](https://docs.python.org/3/library/asyncio.html). Here is an example 81 | using trio: 82 | 83 | 84 | ```python 85 | import pynng 86 | import trio 87 | 88 | async def send_and_recv(sender, receiver, message): 89 | await sender.asend(message) 90 | return await receiver.arecv() 91 | 92 | with pynng.Pair0(listen='tcp://127.0.0.1:54321') as s1, \ 93 | pynng.Pair0(dial='tcp://127.0.0.1:54321') as s2: 94 | received = trio.run(send_and_recv, s1, s2, b'hello there old pal!') 95 | assert received == b'hello there old pal!' 96 | ``` 97 | 98 | Many other protocols are available as well: 99 | 100 | * `Pair0`: one-to-one, bidirectional communication. 101 | * `Pair1`: one-to-one, bidirectional communication, but also supporting 102 | polyamorous sockets 103 | * `Pub0`, `Sub0`: publish/subscribe sockets. 104 | * `Surveyor0`, `Respondent0`: Broadcast a survey to respondents, e.g. to find 105 | out what services are available. 106 | * `Req0`, `Rep0`: request/response pattern. 107 | * `Push0`, `Pull0`: Aggregate messages from multiple sources and load balance 108 | among many destinations. 109 | 110 | Examples 111 | -------- 112 | 113 | Some examples (okay, just two examples) are available in the 114 | [examples](https://github.com/codypiersall/pynng/tree/master/examples) 115 | directory. 116 | 117 | Git Branch Policy 118 | ----------------- 119 | 120 | The **only** stable branch is `master`. There will *never* be a `git push -f` 121 | on master. On the other hand, all other branches are not considered stable; 122 | they may be deleted, rebased, force-pushed, and any other manner of funky 123 | business. 124 | 125 | [nanomsg next generation]: https://nanomsg.github.io/nng/index.html 126 | -------------------------------------------------------------------------------- /examples/abstract.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Example demonstrating the use of abstract sockets in pynng. 4 | 5 | Abstract sockets are a Linux-specific feature that allows for socket communication 6 | without creating files in the filesystem. They are identified by names in an 7 | abstract namespace. 8 | 9 | To run this example (on Linux only): 10 | 11 | # Terminal 1 12 | python abstract.py node0 abstract://my_test_socket 13 | 14 | # Terminal 2 15 | python abstract.py node1 abstract://my_test_socket 16 | 17 | You can also test with special characters: 18 | 19 | # Terminal 1 20 | python abstract.py node0 "abstract://test%00socket" 21 | 22 | # Terminal 2 23 | python abstract.py node1 "abstract://test%00socket" 24 | 25 | Or test auto-bind functionality: 26 | 27 | # Terminal 1 28 | python abstract.py node0 abstract:// 29 | 30 | # Terminal 2 31 | python abstract.py node1 abstract:// 32 | """ 33 | 34 | import sys 35 | import time 36 | import platform 37 | import pynng 38 | 39 | 40 | def usage(): 41 | """Print usage message and exit.""" 42 | print("Usage: {} node0|node1 URL".format(sys.argv[0])) 43 | print("") 44 | print("Example URLs:") 45 | print(" abstract://my_test_socket") 46 | print(" abstract://test%00socket (with NUL byte)") 47 | print(" abstract:// (auto-bind)") 48 | print("") 49 | print( 50 | "Note: Abstract sockets are Linux-specific and will not work on other platforms." 51 | ) 52 | sys.exit(1) 53 | 54 | 55 | def main(): 56 | if len(sys.argv) < 3: 57 | usage() 58 | 59 | node = sys.argv[1] 60 | if node not in ("node0", "node1"): 61 | usage() 62 | 63 | url = sys.argv[2] 64 | 65 | # Check if we're on Linux 66 | if platform.system() != "Linux": 67 | print("Error: Abstract sockets are only supported on Linux.") 68 | print(f"Current platform: {platform.system()}") 69 | sys.exit(1) 70 | 71 | # Check if URL starts with abstract:// 72 | if not url.startswith("abstract://"): 73 | print("Error: URL must start with 'abstract://'") 74 | usage() 75 | 76 | print(f"Starting {node} with URL: {url}") 77 | 78 | try: 79 | with pynng.Pair0(recv_timeout=100, send_timeout=100) as sock: 80 | if node == "node0": 81 | # Node 0 listens 82 | print(f"Listening on {url}") 83 | listener = sock.listen(url) 84 | 85 | # Print information about the listener 86 | local_addr = listener.local_address 87 | print(f"Local address: {local_addr}") 88 | print(f"Address family: {local_addr.family_as_str}") 89 | print(f"Address name: {local_addr.name}") 90 | 91 | # Wait for connections and messages 92 | while True: 93 | try: 94 | msg = sock.recv() 95 | print(f"Received message: {msg.decode()}") 96 | except pynng.Timeout: 97 | pass 98 | 99 | # Send a message periodically 100 | try: 101 | sock.send(f"Hello from {node} at {time.time()}".encode()) 102 | except pynng.Timeout: 103 | pass 104 | 105 | time.sleep(0.5) 106 | 107 | else: 108 | # Node 1 dials 109 | print(f"Dialing {url}") 110 | dialer = sock.dial(url) 111 | 112 | # Print information about the dialer 113 | try: 114 | local_addr = dialer.local_address 115 | print(f"Local address: {local_addr}") 116 | print(f"Address family: {local_addr.family_as_str}") 117 | except pynng.NotSupported: 118 | print( 119 | "Local address not supported for dialer with abstract sockets" 120 | ) 121 | 122 | # Wait for connections and messages 123 | while True: 124 | try: 125 | msg = sock.recv() 126 | print(f"Received message: {msg.decode()}") 127 | except pynng.Timeout: 128 | pass 129 | 130 | # Send a message periodically 131 | try: 132 | sock.send(f"Hello from {node} at {time.time()}".encode()) 133 | except pynng.Timeout: 134 | pass 135 | 136 | time.sleep(0.5) 137 | 138 | except pynng.NNGException as e: 139 | print(f"Error: {e}") 140 | sys.exit(1) 141 | except KeyboardInterrupt: 142 | print("\nExiting...") 143 | sys.exit(0) 144 | 145 | 146 | if __name__ == "__main__": 147 | main() 148 | -------------------------------------------------------------------------------- /test/test_msg.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import pynng 3 | from _test_util import wait_pipe_len 4 | 5 | addr = "inproc://test-addr" 6 | 7 | # timeout, ms 8 | to = 1000 9 | 10 | 11 | def test_socket_send_recv_msg_from_pipe(): 12 | with pynng.Pair0(listen=addr, recv_timeout=to) as s1, pynng.Pair0( 13 | dial=addr, recv_timeout=to 14 | ) as s2: 15 | wait_pipe_len(s1, 1) 16 | pipe = s1.pipes[0] 17 | msg = pynng.Message(b"oh hello friend", pipe) 18 | assert isinstance(msg, pynng.Message) 19 | assert msg.bytes == b"oh hello friend" 20 | s1.send_msg(msg) 21 | assert msg.pipe is pipe 22 | msg2 = s2.recv_msg() 23 | assert isinstance(msg2, pynng.Message) 24 | assert msg2.bytes == b"oh hello friend" 25 | assert msg2.pipe is s2.pipes[0] 26 | 27 | 28 | def test_socket_send_recv_msg(): 29 | with pynng.Pair0(listen=addr, recv_timeout=to) as s1, pynng.Pair0( 30 | dial=addr, recv_timeout=to 31 | ) as s2: 32 | wait_pipe_len(s1, 1) 33 | msg = pynng.Message(b"we are friends, old buddy") 34 | s1.send_msg(msg) 35 | msg2 = s2.recv_msg() 36 | assert msg2.bytes == b"we are friends, old buddy" 37 | 38 | 39 | @pytest.mark.trio 40 | async def test_socket_arecv_asend_msg(): 41 | with pynng.Pair0(listen=addr, recv_timeout=to) as s1, pynng.Pair0( 42 | dial=addr, recv_timeout=to 43 | ) as s2: 44 | wait_pipe_len(s1, 1) 45 | msg = pynng.Message(b"you truly are a pal") 46 | await s1.asend_msg(msg) 47 | msg2 = await s2.arecv_msg() 48 | assert msg2.bytes == b"you truly are a pal" 49 | assert msg2.pipe is s2.pipes[0] 50 | 51 | 52 | @pytest.mark.trio 53 | async def test_context_arecv_asend_msg(): 54 | with pynng.Req0(listen=addr, recv_timeout=to) as s1, pynng.Rep0( 55 | dial=addr, recv_timeout=to 56 | ) as s2: 57 | with s1.new_context() as ctx1, s2.new_context() as ctx2: 58 | wait_pipe_len(s1, 1) 59 | msg = pynng.Message(b"do i even know you") 60 | await ctx1.asend_msg(msg) 61 | msg2 = await ctx2.arecv_msg() 62 | assert msg2.pipe is s2.pipes[0] 63 | assert msg2.bytes == b"do i even know you" 64 | msg3 = pynng.Message(b"yes of course i am your favorite platypus") 65 | await ctx2.asend_msg(msg3) 66 | msg4 = await ctx1.arecv_msg() 67 | assert msg4.pipe is s1.pipes[0] 68 | assert msg4.bytes == b"yes of course i am your favorite platypus" 69 | 70 | 71 | def test_context_recv_send_msg(): 72 | with pynng.Req0(listen=addr, recv_timeout=to) as s1, pynng.Rep0( 73 | dial=addr, recv_timeout=to 74 | ) as s2: 75 | with s1.new_context() as ctx1, s2.new_context() as ctx2: 76 | wait_pipe_len(s1, 1) 77 | msg = pynng.Message(b"do i even know you") 78 | ctx1.send_msg(msg) 79 | msg2 = ctx2.recv_msg() 80 | assert msg2.pipe is s2.pipes[0] 81 | assert msg2.bytes == b"do i even know you" 82 | msg3 = pynng.Message(b"yes of course i am your favorite platypus") 83 | ctx2.send_msg(msg3) 84 | msg4 = ctx1.recv_msg() 85 | assert msg4.pipe is s1.pipes[0] 86 | assert msg4.bytes == b"yes of course i am your favorite platypus" 87 | 88 | 89 | def test_cannot_double_send(): 90 | # double send would cause a SEGFAULT!!! That's no good 91 | with pynng.Req0(listen=addr, recv_timeout=to) as s1, pynng.Rep0( 92 | dial=addr, recv_timeout=to 93 | ) as s2: 94 | msg = pynng.Message(b"this is great") 95 | s1.send_msg(msg) 96 | with pytest.raises(pynng.MessageStateError): 97 | s1.send_msg(msg) 98 | 99 | with s1.new_context() as ctx: 100 | msg = pynng.Message(b"this also is great") 101 | ctx.send_msg(msg) 102 | with pytest.raises(pynng.MessageStateError): 103 | ctx.send_msg(msg) 104 | 105 | # don't really need to receive, but linters hate not using s2 106 | s2.recv_msg() 107 | 108 | 109 | @pytest.mark.trio 110 | async def test_cannot_double_asend(): 111 | # double send would cause a SEGFAULT!!! That's no good 112 | with pynng.Req0(listen=addr, recv_timeout=to) as s1, pynng.Rep0( 113 | dial=addr, recv_timeout=to 114 | ) as s2: 115 | msg = pynng.Message(b"this is great") 116 | await s1.asend_msg(msg) 117 | with pytest.raises(pynng.MessageStateError): 118 | await s1.asend_msg(msg) 119 | 120 | with s1.new_context() as ctx: 121 | msg = pynng.Message(b"this also is great") 122 | await ctx.asend_msg(msg) 123 | with pytest.raises(pynng.MessageStateError): 124 | await ctx.asend_msg(msg) 125 | 126 | # don't really need to receive, but linters hate not using s2 127 | await s2.arecv_msg() 128 | -------------------------------------------------------------------------------- /pynng/exceptions.py: -------------------------------------------------------------------------------- 1 | """ 2 | Exception hierarchy for pynng. The base of the hierarchy is NNGException. 3 | Every exception has a corresponding errno attribute which can be checked. 4 | 5 | Generally, each number in nng_errno_enum corresponds with an Exception type. 6 | 7 | 8 | """ 9 | 10 | from ._nng import ffi, lib as nng 11 | 12 | 13 | class NNGException(Exception): 14 | """The base exception for any exceptional condition in the nng bindings.""" 15 | 16 | def __init__(self, msg, errno): 17 | super().__init__(msg) 18 | self.errno = errno 19 | 20 | 21 | class Interrupted(NNGException): # NNG_EINTR 22 | pass 23 | 24 | 25 | class NoMemory(NNGException): # NNG_ENOMEM 26 | pass 27 | 28 | 29 | class InvalidOperation(NNGException): # NNG_EINVAL 30 | pass 31 | 32 | 33 | class Busy(NNGException): # NNG_EBUSY 34 | pass 35 | 36 | 37 | class Timeout(NNGException): # NNG_ETIMEDOUT 38 | pass 39 | 40 | 41 | class ConnectionRefused(NNGException): # NNG_ECONNREFUSED 42 | pass 43 | 44 | 45 | class Closed(NNGException): # NNG_ECLOSED 46 | pass 47 | 48 | 49 | class TryAgain(NNGException): # NNG_EAGAIN 50 | pass 51 | 52 | 53 | class NotSupported(NNGException): # NNG_ENOTSUP 54 | pass 55 | 56 | 57 | class AddressInUse(NNGException): # NNG_EADDRINUSE 58 | pass 59 | 60 | 61 | class BadState(NNGException): # NNG_ESTATE 62 | pass 63 | 64 | 65 | class NoEntry(NNGException): # NNG_ENOENT 66 | pass 67 | 68 | 69 | class ProtocolError(NNGException): # NNG_EPROTO 70 | pass 71 | 72 | 73 | class DestinationUnreachable(NNGException): # NNG_EUNREACHABLE 74 | pass 75 | 76 | 77 | class AddressInvalid(NNGException): # NNG_EADDRINVAL 78 | pass 79 | 80 | 81 | class PermissionDenied(NNGException): # NNG_EPERM 82 | pass 83 | 84 | 85 | class MessageTooLarge(NNGException): # NNG_EMSGSiZE 86 | pass 87 | 88 | 89 | class ConnectionReset(NNGException): # NNG_ECONNRESET 90 | pass 91 | 92 | 93 | class ConnectionAborted(NNGException): # NNG_ECONNABORTED 94 | pass 95 | 96 | 97 | class Canceled(NNGException): # NNG_ECANCELED 98 | pass 99 | 100 | 101 | class OutOfFiles(NNGException): # NNG_ENOFILES 102 | pass 103 | 104 | 105 | class OutOfSpace(NNGException): # NNG_ENOSPC 106 | pass 107 | 108 | 109 | class AlreadyExists(NNGException): # NNG_EEXIST 110 | pass 111 | 112 | 113 | class ReadOnly(NNGException): # NNG_EREADONLY 114 | pass 115 | 116 | 117 | class WriteOnly(NNGException): # NNG_EWRITEONLY 118 | pass 119 | 120 | 121 | class CryptoError(NNGException): # NNG_ECRYPTO 122 | pass 123 | 124 | 125 | class AuthenticationError(NNGException): # NNG_EPEERAUTH 126 | pass 127 | 128 | 129 | class NoArgument(NNGException): # NNG_ENOARG 130 | pass 131 | 132 | 133 | class Ambiguous(NNGException): # NNG_EAMBIGUOUS 134 | pass 135 | 136 | 137 | class BadType(NNGException): # NNG_EBADTYPE 138 | pass 139 | 140 | 141 | class Internal(NNGException): # NNG_EINTERNAL 142 | pass 143 | 144 | 145 | # maps exceptions from the enum nng_errno_enum to Exception classes 146 | EXCEPTION_MAP = { 147 | nng.NNG_EINTR: Interrupted, 148 | nng.NNG_ENOMEM: NoMemory, 149 | nng.NNG_EINVAL: InvalidOperation, 150 | nng.NNG_EBUSY: Busy, 151 | nng.NNG_ETIMEDOUT: Timeout, 152 | nng.NNG_ECONNREFUSED: ConnectionRefused, 153 | nng.NNG_ECLOSED: Closed, 154 | nng.NNG_EAGAIN: TryAgain, 155 | nng.NNG_ENOTSUP: NotSupported, 156 | nng.NNG_EADDRINUSE: AddressInUse, 157 | nng.NNG_ESTATE: BadState, 158 | nng.NNG_ENOENT: NoEntry, 159 | nng.NNG_EPROTO: ProtocolError, 160 | nng.NNG_EUNREACHABLE: DestinationUnreachable, 161 | nng.NNG_EADDRINVAL: AddressInvalid, 162 | nng.NNG_EPERM: PermissionDenied, 163 | nng.NNG_EMSGSIZE: MessageTooLarge, 164 | nng.NNG_ECONNRESET: ConnectionReset, 165 | nng.NNG_ECONNABORTED: ConnectionAborted, 166 | nng.NNG_ECANCELED: Canceled, 167 | nng.NNG_ENOFILES: OutOfFiles, 168 | nng.NNG_ENOSPC: OutOfSpace, 169 | nng.NNG_EEXIST: AlreadyExists, 170 | nng.NNG_EREADONLY: ReadOnly, 171 | nng.NNG_EWRITEONLY: WriteOnly, 172 | nng.NNG_ECRYPTO: CryptoError, 173 | nng.NNG_EPEERAUTH: AuthenticationError, 174 | nng.NNG_ENOARG: NoArgument, 175 | nng.NNG_EAMBIGUOUS: Ambiguous, 176 | nng.NNG_EBADTYPE: BadType, 177 | nng.NNG_EINTERNAL: Internal, 178 | } 179 | 180 | 181 | class MessageStateError(Exception): 182 | """ 183 | Indicates that a Message was trying to be used in an invalid way. 184 | """ 185 | 186 | 187 | def check_err(err): 188 | """ 189 | Raises an exception if the return value of an nng_function is nonzero. 190 | 191 | The enum nng_errno_enum is defined in nng.h 192 | 193 | """ 194 | # fast path for success 195 | if not err: 196 | return 197 | 198 | msg = nng.nng_strerror(err) 199 | string = ffi.string(msg) 200 | string = string.decode() 201 | exc = EXCEPTION_MAP.get(err, NNGException) 202 | raise exc(string, err) 203 | -------------------------------------------------------------------------------- /test/test_tls.py: -------------------------------------------------------------------------------- 1 | from pynng import Pair0, TLSConfig 2 | 3 | SERVER_CERT = """ 4 | -----BEGIN CERTIFICATE----- 5 | MIID1jCCAr6gAwIBAgIUMq6zvsPyDm2s4dRJD3SLYmRW1BYwDQYJKoZIhvcNAQEL 6 | BQAwgY0xCzAJBgNVBAYTAk5MMRAwDgYDVQQIDAdMaW1idXJnMSswKQYDVQQHDCJL 7 | YXJ2ZWVsd2VnIDE5QiwgNjIyMiBOSiBNYWFzdHJpY2h0MRMwEQYDVQQKDApWZWN0 8 | aW9uZWVyMRQwEgYDVQQLDAtEZXZlbG9wbWVudDEUMBIGA1UEAwwLTW90b3Jjb3J0 9 | ZXgwIBcNMTkxMjA2MTEwODIwWhgPMjExOTExMTIxMTA4MjBaMIGNMQswCQYDVQQG 10 | EwJOTDEQMA4GA1UECAwHTGltYnVyZzErMCkGA1UEBwwiS2FydmVlbHdlZyAxOUIs 11 | IDYyMjIgTkogTWFhc3RyaWNodDETMBEGA1UECgwKVmVjdGlvbmVlcjEUMBIGA1UE 12 | CwwLRGV2ZWxvcG1lbnQxFDASBgNVBAMMC01vdG9yY29ydGV4MIIBIjANBgkqhkiG 13 | 9w0BAQEFAAOCAQ8AMIIBCgKCAQEAz7ai7pvSp+tAadRgeQrO0rZ8kmCDtHbN5xLV 14 | jZBHtr7lKlKZkyJfeV1ERgO2jPqnQ9uMIR52HHxQsKEzyveFoAmjrB0cQfJz35c4 15 | i8eLwDRnEv4lK9JVLhtoUIYrUjN8LzeeBEztBCCh0X8p7vcLX4+9Y679edzWdNGM 16 | HUpIVTrbg7qvpITe/VbBMYnkCaQU3HgMnpEpMA1EcYAovMHmss5ZLR4cA/FG2D7E 17 | 3FiFQCYic17/OMzr1r/3ybD0mNwwkJV0R2HnexGXmQ00W/QogAjAD4UjBXFjrlpp 18 | 78MR1rOyvKbSssSJEtzLw+eJUHce3xYRLbnk1kxkB9gi753frQIDAQABoyowKDAm 19 | BgNVHREEHzAdgglsb2NhbGhvc3SHBH8AAAGHBMCoso+HBMCoAmQwDQYJKoZIhvcN 20 | AQELBQADggEBAJS21jEXoQrA2U2JajV69BsetOpgE4Yj0cWbK1ujX4j5leTW5m+h 21 | qGOxj3vUCK4OakSqDtaxCI0Kqf1eghFge0nmUJdtz3/slqn0enIBcVRdZTDjP5+S 22 | +rFJBhCPRu+LnIsdPauO1zWHNWK1e+rrn1JXINpNvAkrCHkJE/gxNFKOSwD3bFB6 23 | tiGfIzxrWPAe+9yWJKG8swFzEWqIbw+1TgxBiqHGU+H/MZLUxswzQXDjOzClbKwd 24 | 4qDP9+0NM2Yorq1QHaXcJO+Xkja27PD7RMXxSqpOqI3jygw2SIgkG2dZ7tqIyQMi 25 | QlwvysgNBjtSmQ1wPjSbEYGqtvoSK4bWTsA= 26 | -----END CERTIFICATE-----""" 27 | 28 | SERVER_KEY = """ 29 | -----BEGIN PRIVATE KEY----- 30 | MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDPtqLum9Kn60Bp 31 | 1GB5Cs7StnySYIO0ds3nEtWNkEe2vuUqUpmTIl95XURGA7aM+qdD24whHnYcfFCw 32 | oTPK94WgCaOsHRxB8nPflziLx4vANGcS/iUr0lUuG2hQhitSM3wvN54ETO0EIKHR 33 | fynu9wtfj71jrv153NZ00YwdSkhVOtuDuq+khN79VsExieQJpBTceAyekSkwDURx 34 | gCi8weayzlktHhwD8UbYPsTcWIVAJiJzXv84zOvWv/fJsPSY3DCQlXRHYed7EZeZ 35 | DTRb9CiACMAPhSMFcWOuWmnvwxHWs7K8ptKyxIkS3MvD54lQdx7fFhEtueTWTGQH 36 | 2CLvnd+tAgMBAAECggEAI4UA0brVyB9DkZVetfQyL/hCzykv/IFAbp5a5G1ixg5Y 37 | 0+byGiYLm45maW6jHfKS/diiWtuBqRddGQdH+xJeyGI9meYUefaC+B487jI+ZKzR 38 | X38UTi0Wod7P9M0sxU7GkrB5FhUthsIpydBsFFAsorfK1CwNbnRkO+/FfRDB08kA 39 | sGu1qGc8Do4Bt8w8fevqHNaexbAX7RaokK3gWf4+kYn7drwl2Lp/8qSq+WNN4dYE 40 | x0fRiGQ/eMP+hPHWY0PIq9InjX0bl0sVeKBhVV4OLp8Vy7neY/11aEAj3lvioMog 41 | kimmmd7llQDqo3bUt/rPyi1z1BIuy1t1nVIug8SvwQKBgQD6/gotZJ1v7Scz2VtP 42 | FQxfYupgoMdBlUiaBcIjPisAjTNDAG8i54TZa4/ELC1f5yulu0vbsoLpalW3f2I0 43 | 4Eog1NiYteAwZxH85fVd5WTPBe+WRXyeAJl0ZRnmx/0oF7ToDdUpDFlIgBPzCfiK 44 | oqH5kNwef4UBXBzNCqwETpDDcQKBgQDT24v1Wi0AsPmo2gtxYq+aC9KHdWWiIp/O 45 | I6+PrIkigLDJiw5ltXZ8VnGEt/l03EI4+60O6l5jb9SxP+fs8pCOJ6Dpjm5ATxPZ 46 | NEu2/c2+YZrJdxNeB4UBi1Lrw6iPbsywaC8MuwMIYdf/5KOWG9TsLQHeXHl9IQBl 47 | p0PVlCnJ/QKBgBGKV2O8uFPuGuNAuWTZb7fqzb5a/hHTQPOim2KjIZY0R/TSvvGN 48 | hHc9URrAi5s8KIy4fiCoZQWy7LKaMF7JneSVe12QuE4ppdQqre8V7Oma3Jd/26mf 49 | GRpNRnYeW87FeVsOPGtV9ZdffAVsGPZ3TyKFwRxQhRcHPOwHZuYWJ3/BAoGBAJSG 50 | ERuj6XLXn19x5Z3K+qK7cQ/IDMVbEr+YowbNhaJrqATTePdy/Sr0C0dpFviHReHf 51 | Bxcy1ZNOnkTZMYYbE56lR5kVYlOxXI/kqsQSMMAsezCMS0abbPKFM0/X7n8HxXZS 52 | w9Ff9iNVPPHH36tnvaEJeIrkN8OydC3P0q2T3qwdAoGBANfSSQYCe7wBiS0YoNBZ 53 | NLtTIoIHJEcqpILFLN37tnFEeOT98WBGg9LRRmyzsc2x5UmbZYiB0CRf6QsJHXdX 54 | xK0rEGEfoh9GVozhNptOtDV8agUoX9Am/bU/kGa21j9GHvjq3j0C8RMXDba/RQXS 55 | nFcJUXxIIJLAKbqnMRIp46OP 56 | -----END PRIVATE KEY----- 57 | """ 58 | 59 | # we use a self-signed certificate 60 | CA_CERT = SERVER_CERT 61 | 62 | URL = "tls+tcp://localhost:5556" 63 | BYTES = b"1234567890" 64 | 65 | 66 | def test_config_string(): 67 | with Pair0(recv_timeout=1000, send_timeout=1000) as server, Pair0( 68 | recv_timeout=1000, send_timeout=1000 69 | ) as client: 70 | c_server = TLSConfig( 71 | TLSConfig.MODE_SERVER, 72 | own_key_string=SERVER_KEY, 73 | own_cert_string=SERVER_CERT, 74 | ) 75 | server.tls_config = c_server 76 | c_client = TLSConfig(TLSConfig.MODE_CLIENT, ca_string=CA_CERT) 77 | client.tls_config = c_client 78 | 79 | server.listen(URL) 80 | client.dial(URL) 81 | client.send(BYTES) 82 | assert server.recv() == BYTES 83 | server.send(BYTES) 84 | assert client.recv() == BYTES 85 | 86 | 87 | def test_config_file(tmp_path): 88 | ca_crt_file = tmp_path / "ca.crt" 89 | ca_crt_file.write_text(CA_CERT) 90 | 91 | key_pair_file = tmp_path / "key_pair_file.pem" 92 | key_pair_file.write_text(SERVER_CERT + SERVER_KEY) 93 | 94 | with Pair0(recv_timeout=1000, send_timeout=1000) as server, Pair0( 95 | recv_timeout=1000, send_timeout=1000 96 | ) as client: 97 | c_server = TLSConfig(TLSConfig.MODE_SERVER, cert_key_file=str(key_pair_file)) 98 | server.tls_config = c_server 99 | c_client = TLSConfig(TLSConfig.MODE_CLIENT, ca_files=[str(ca_crt_file)]) 100 | client.tls_config = c_client 101 | 102 | server.listen(URL) 103 | client.dial(URL) 104 | client.send(BYTES) 105 | assert server.recv() == BYTES 106 | server.send(BYTES) 107 | assert client.recv() == BYTES 108 | -------------------------------------------------------------------------------- /docs/core.rst: -------------------------------------------------------------------------------- 1 | ========================== 2 | Pynng's core functionality 3 | ========================== 4 | 5 | At the heart of pynng is the :class:`pynng.Socket`. It takes no positional 6 | arguments, and all keyword arguments are optional. It is the Python version of 7 | `nng_socket `_. 8 | 9 | ---------- 10 | The Socket 11 | ---------- 12 | 13 | .. Note:: 14 | 15 | You should never instantiate a :class:`pynng.Socket` directly. Rather, you 16 | should instantiate one of the :ref:`subclasses `. 17 | 18 | .. autoclass:: pynng.Socket(*, listen=None, dial=None, **kwargs) 19 | :members: listen, dial, send, recv, asend, arecv, recv_msg, arecv_msg, new_context 20 | 21 | Feel free to peruse the `examples online 22 | `_, or ask in the 23 | `gitter channel `_. 24 | 25 | .. _available-protocols : 26 | 27 | ################### 28 | Available Protocols 29 | ################### 30 | 31 | .. autoclass:: pynng.Pair0(**kwargs) 32 | .. autoclass:: pynng.Pair1 33 | .. autoclass:: pynng.Req0 34 | .. autoclass:: pynng.Rep0(**kwargs) 35 | .. autoclass:: pynng.Pub0(**kwargs) 36 | .. autoclass:: pynng.Sub0(**kwargs) 37 | :members: 38 | .. autoclass:: pynng.Push0(**kwargs) 39 | .. autoclass:: pynng.Pull0(**kwargs) 40 | .. autoclass:: pynng.Surveyor0(**kwargs) 41 | .. autoclass:: pynng.Respondent0(**kwargs) 42 | .. autoclass:: pynng.Bus0(**kwargs) 43 | 44 | ---- 45 | Pipe 46 | ---- 47 | 48 | .. autoclass:: pynng.Pipe(...) 49 | :members: send, asend 50 | 51 | ------- 52 | Context 53 | ------- 54 | 55 | .. autoclass:: pynng.Context(...) 56 | :members: send, asend, recv, arecv, recv_msg, arecv_msg, close 57 | 58 | ------- 59 | Message 60 | ------- 61 | 62 | .. autoclass:: pynng.Message(data) 63 | 64 | ------ 65 | Dialer 66 | ------ 67 | 68 | .. autoclass:: pynng.Dialer(...) 69 | :members: close 70 | 71 | -------- 72 | Listener 73 | -------- 74 | 75 | .. autoclass:: pynng.Listener(...) 76 | :members: close 77 | 78 | 79 | --------- 80 | TLSConfig 81 | --------- 82 | 83 | Sockets can make use of the TLS transport on top of TCP by specifying an 84 | address similar to how tcp is specified. The following are examples of valid 85 | TLS addresses: 86 | 87 | * ``"tls+tcp:127.0.0.1:1313"``, listening on TCP port 1313 on localhost. 88 | * ``"tls+tcp4:127.0.0.1:1313"``, explicitly requesting IPv4 for TCP port 1313 89 | on localhost. 90 | * ``"tls+tcp6://[::1]:4433"``, explicitly requesting IPv6 for IPv6 localhost on 91 | port 4433. 92 | 93 | 94 | .. autoclass:: pynng.TLSConfig(...) 95 | 96 | ----------------- 97 | Abstract Sockets 98 | ----------------- 99 | 100 | Abstract sockets are a Linux-specific feature that allows for socket communication 101 | without creating files in the filesystem. They are identified by names in an 102 | abstract namespace and are automatically freed by the system when no longer in use. 103 | 104 | Abstract sockets use the ``abstract://`` URL scheme. For example: 105 | 106 | * ``"abstract://my_socket"`` - a simple abstract socket name 107 | * ``"abstract://test%00socket"`` - a socket name with a NUL byte (URI-encoded) 108 | * ``"abstract://"`` - an empty name for auto-bind (system assigns a name) 109 | 110 | **Important:** Abstract sockets are only available on Linux systems. Attempting to use 111 | them on other platforms will result in an error. 112 | 113 | Abstract sockets have the following characteristics: 114 | 115 | * They do not have any representation in the file system 116 | * They are automatically freed by the system when no longer in use 117 | * They ignore socket permissions 118 | * They support arbitrary values in the path, including embedded NUL bytes 119 | * The name does not include the leading NUL byte used in the low-level socket address 120 | 121 | **URI Encoding:** Abstract socket names can contain arbitrary bytes, including NUL 122 | bytes. These are represented using URI encoding. For example, the name ``"a\0b"`` 123 | would be represented as ``"abstract://a%00b"``. 124 | 125 | **Auto-bind:** An empty name may be used with a listener to request "auto bind" 126 | be used to select a name. In this case, the system will allocate a free name. 127 | The name assigned can be retrieved using the ``NNG_OPT_LOCADDR`` option. 128 | 129 | **Example:** 130 | 131 | .. code-block:: python 132 | 133 | import pynng 134 | import platform 135 | 136 | # Check if we're on Linux 137 | if platform.system() != "Linux": 138 | print("Abstract sockets are only supported on Linux") 139 | exit(1) 140 | 141 | # Create a socket with abstract address 142 | with pynng.Pair0() as sock: 143 | # Listen on an abstract socket 144 | listener = sock.listen("abstract://my_test_socket") 145 | 146 | # Get the local address 147 | local_addr = listener.local_address 148 | print(f"Address family: {local_addr.family_as_str}") 149 | print(f"Address name: {local_addr.name}") 150 | 151 | # The address can be used for dialing from another process 152 | # dialer = sock.dial("abstract://my_test_socket") 153 | 154 | For a complete example, see :doc:`../examples/abstract`. 155 | -------------------------------------------------------------------------------- /pynng/tls.py: -------------------------------------------------------------------------------- 1 | import pynng 2 | 3 | 4 | class TLSConfig: 5 | """ 6 | TLS Configuration object. This object is used to configure sockets that 7 | are using the TLS transport. 8 | 9 | Args: 10 | mode: Must be ``TLSConfig.MODE_CLIENT`` or ``TLSConfig.MODE_SERVER``. 11 | Corresponds to nng's ``mode`` argument in ``nng_tls_config_alloc``. 12 | server_name (str): When configuring a client, ``server_name`` is used 13 | to compare the identity of the server's certificate. Corresponds 14 | to ``nng_tls_config_server_name``. 15 | ca_string (str): Set certificate authority with a string. Corresponds 16 | to ``nng_tls_config_ca_chain`` 17 | own_key_string (str): When passed with ``own_cert_string``, is used to 18 | set own certificate. Corresponds to ``nng_tls_config_own_cert``. 19 | own_cert_string (str): When passed with ``own_key_string``, is used to 20 | set own certificate. Corresponds to ``nng_tls_config_own_cert``. 21 | auth_mode: Set the authentication mode of the connection. Corresponds 22 | to ``nng_tls_config_auth_mode``. 23 | ca_files (str or list[str]): ca files to use for the TLS connection. 24 | Corresponds to ``nng_tls_config_ca_file``. 25 | cert_key_file (str): Corresponds to ``nng_tls_config_cert_key_file``. 26 | passwd (str): Password used for configuring certificates. 27 | 28 | Check the `TLS tests 29 | `_ for 30 | usage examples. 31 | 32 | """ 33 | 34 | MODE_CLIENT = pynng.lib.NNG_TLS_MODE_CLIENT 35 | MODE_SERVER = pynng.lib.NNG_TLS_MODE_SERVER 36 | 37 | AUTH_MODE_NONE = pynng.lib.NNG_TLS_AUTH_MODE_NONE 38 | AUTH_MODE_OPTIONAL = pynng.lib.NNG_TLS_AUTH_MODE_OPTIONAL 39 | AUTH_MODE_REQUIRED = pynng.lib.NNG_TLS_AUTH_MODE_REQUIRED 40 | 41 | def __init__( 42 | self, 43 | mode, 44 | server_name=None, 45 | ca_string=None, 46 | own_key_string=None, 47 | own_cert_string=None, 48 | auth_mode=None, 49 | ca_files=None, 50 | cert_key_file=None, 51 | passwd=None, 52 | ): 53 | if ca_string and ca_files: 54 | raise ValueError("Cannot set both ca_string and ca_files!") 55 | 56 | if (own_cert_string or own_key_string) and cert_key_file: 57 | raise ValueError("Cannot set both own_{key,cert}_string an cert_key_file!") 58 | 59 | if bool(own_cert_string) != bool(own_key_string): 60 | raise ValueError( 61 | "own_key_string and own_cert_string must be both set or unset" 62 | ) 63 | 64 | if isinstance(ca_files, str): 65 | # assume the user really intended to only set a single ca file. 66 | ca_files = [ca_files] 67 | 68 | tls_config_p = pynng.ffi.new("nng_tls_config **") 69 | pynng.check_err(pynng.lib.nng_tls_config_alloc(tls_config_p, mode)) 70 | self._tls_config = tls_config_p[0] 71 | 72 | if server_name: 73 | self.set_server_name(server_name) 74 | 75 | if ca_string: 76 | self.set_ca_chain(ca_string) 77 | 78 | if own_key_string and own_cert_string: 79 | self.set_own_cert(own_cert_string, own_key_string, passwd) 80 | 81 | if auth_mode: 82 | self.set_auth_mode(auth_mode) 83 | 84 | if ca_files: 85 | for f in ca_files: 86 | self.add_ca_file(f) 87 | 88 | if cert_key_file: 89 | self.set_cert_key_file(cert_key_file, passwd) 90 | 91 | def __del__(self): 92 | pynng.lib.nng_tls_config_free(self._tls_config) 93 | 94 | def set_server_name(self, server_name): 95 | """ 96 | Configure remote server name. 97 | """ 98 | server_name_char = pynng.nng.to_char(server_name) 99 | err = pynng.lib.nng_tls_config_server_name(self._tls_config, server_name_char) 100 | pynng.check_err(err) 101 | 102 | def set_ca_chain(self, chain, crl=None): 103 | """ 104 | Configure certificate authority certificate chain. 105 | """ 106 | chain_char = pynng.nng.to_char(chain) 107 | crl_char = pynng.nng.to_char(crl) if crl is not None else pynng.ffi.NULL 108 | 109 | err = pynng.lib.nng_tls_config_ca_chain(self._tls_config, chain_char, crl_char) 110 | pynng.check_err(err) 111 | 112 | def set_own_cert(self, cert, key, passwd=None): 113 | """ 114 | Configure own certificate and key. 115 | """ 116 | cert_char = pynng.nng.to_char(cert) 117 | key_char = pynng.nng.to_char(key) 118 | passwd_char = ( 119 | pynng.nng.to_char(passwd) if passwd is not None else pynng.ffi.NULL 120 | ) 121 | 122 | err = pynng.lib.nng_tls_config_own_cert( 123 | self._tls_config, cert_char, key_char, passwd_char 124 | ) 125 | pynng.check_err(err) 126 | 127 | def set_auth_mode(self, mode): 128 | """ 129 | Configure authentication mode. 130 | """ 131 | err = pynng.lib.nng_tls_config_auth_mode(self._tls_config, mode) 132 | pynng.check_err(err) 133 | 134 | def add_ca_file(self, path): 135 | """ 136 | Add a certificate authority from a file. 137 | """ 138 | path_char = pynng.nng.to_char(path) 139 | err = pynng.lib.nng_tls_config_ca_file(self._tls_config, path_char) 140 | pynng.check_err(err) 141 | 142 | def set_cert_key_file(self, path, passwd=None): 143 | """ 144 | Load own certificate and key from file. 145 | """ 146 | path_char = pynng.nng.to_char(path) 147 | passwd_char = ( 148 | pynng.nng.to_char(passwd) if passwd is not None else pynng.ffi.NULL 149 | ) 150 | 151 | err = pynng.lib.nng_tls_config_cert_key_file( 152 | self._tls_config, path_char, passwd_char 153 | ) 154 | pynng.check_err(err) 155 | -------------------------------------------------------------------------------- /test/test_api.py: -------------------------------------------------------------------------------- 1 | import gc 2 | import time 3 | 4 | import pytest 5 | import trio 6 | 7 | import pynng 8 | 9 | from _test_util import wait_pipe_len 10 | 11 | 12 | addr = "inproc://test-addr" 13 | addr2 = "inproc://test-addr2" 14 | 15 | 16 | def test_dialers_get_added(): 17 | with pynng.Pair0() as s: 18 | assert len(s.dialers) == 0 19 | s.dial(addr, block=False) 20 | assert len(s.dialers) == 1 21 | s.dial(addr2, block=False) 22 | assert len(s.dialers) == 2 23 | 24 | 25 | def test_listeners_get_added(): 26 | with pynng.Pair0() as s: 27 | assert len(s.listeners) == 0 28 | s.listen(addr) 29 | assert len(s.listeners) == 1 30 | s.listen(addr2) 31 | assert len(s.listeners) == 2 32 | 33 | 34 | def test_closing_listener_works(): 35 | with pynng.Pair0(listen=addr) as s: 36 | assert len(s.listeners) == 1 37 | s.listeners[0].close() 38 | assert len(s.listeners) == 0 39 | # if the listener is really closed, we should be able to listen at the 40 | # same address again; we'll sleep a little so OS X CI will pass. 41 | time.sleep(0.01) 42 | s.listen(addr) 43 | assert len(s.listeners) == 1 44 | assert len(s.listeners) == 0 45 | 46 | 47 | def test_closing_dialer_works(): 48 | with pynng.Pair0(dial=addr, block_on_dial=False) as s: 49 | assert len(s.dialers) == 1 50 | s.dialers[0].close() 51 | assert len(s.listeners) == 0 52 | 53 | 54 | def test_nonblocking_recv_works(): 55 | with pynng.Pair0(listen=addr) as s: 56 | with pytest.raises(pynng.TryAgain): 57 | s.recv(block=False) 58 | 59 | 60 | def test_nonblocking_send_works(): 61 | with pynng.Pair0(listen=addr) as s: 62 | with pytest.raises(pynng.TryAgain): 63 | s.send(b"sad message, never will be seen", block=False) 64 | 65 | 66 | @pytest.mark.trio 67 | async def test_context(): 68 | with pynng.Req0(listen=addr, recv_timeout=1000) as req_sock, pynng.Rep0( 69 | dial=addr, recv_timeout=1000 70 | ) as rep_sock: 71 | with req_sock.new_context() as req, rep_sock.new_context() as rep: 72 | assert isinstance(req, pynng.Context) 73 | assert isinstance(rep, pynng.Context) 74 | request = b"i am requesting" 75 | await req.asend(request) 76 | assert await rep.arecv() == request 77 | 78 | response = b"i am responding" 79 | await rep.asend(response) 80 | assert await req.arecv() == response 81 | 82 | with pytest.raises(pynng.BadState): 83 | await req.arecv() 84 | 85 | # responders can't send before receiving 86 | with pytest.raises(pynng.BadState): 87 | await rep.asend(b"I cannot do this why am I trying") 88 | 89 | 90 | @pytest.mark.trio 91 | async def test_multiple_contexts(): 92 | async def recv_and_send(ctx): 93 | data = await ctx.arecv() 94 | await trio.sleep(0.05) 95 | await ctx.asend(data) 96 | 97 | with pynng.Rep0(listen=addr, recv_timeout=500) as rep, pynng.Req0( 98 | dial=addr, recv_timeout=500 99 | ) as req1, pynng.Req0(dial=addr, recv_timeout=500) as req2: 100 | async with trio.open_nursery() as n: 101 | ctx1, ctx2 = [rep.new_context() for _ in range(2)] 102 | with ctx1, ctx2: 103 | n.start_soon(recv_and_send, ctx1) 104 | n.start_soon(recv_and_send, ctx2) 105 | 106 | await req1.asend(b"oh hi") 107 | await req2.asend(b"me toooo") 108 | assert await req1.arecv() == b"oh hi" 109 | assert await req2.arecv() == b"me toooo" 110 | 111 | 112 | def test_synchronous_recv_context(): 113 | with pynng.Rep0(listen=addr, recv_timeout=500) as rep, pynng.Req0( 114 | dial=addr, recv_timeout=500 115 | ) as req: 116 | req.send(b"oh hello there old pal") 117 | assert rep.recv() == b"oh hello there old pal" 118 | rep.send(b"it is so good to hear from you") 119 | assert req.recv() == b"it is so good to hear from you" 120 | 121 | 122 | def test_pair1_polyamorousness(): 123 | with pynng.Pair1( 124 | listen=addr, polyamorous=True, recv_timeout=500 125 | ) as s0, pynng.Pair1(dial=addr, polyamorous=True, recv_timeout=500) as s1: 126 | wait_pipe_len(s0, 1) 127 | # pipe for s1 . 128 | p1 = s0.pipes[0] 129 | with pynng.Pair1(dial=addr, polyamorous=True, recv_timeout=500) as s2: 130 | wait_pipe_len(s0, 2) 131 | # pipes is backed by a dict, so we can't rely on order in 132 | # Python 3.5. 133 | pipes = s0.pipes 134 | p2 = pipes[1] 135 | if p2 is p1: 136 | p2 = pipes[0] 137 | p1.send(b"hello s1") 138 | assert s1.recv() == b"hello s1" 139 | 140 | p2.send(b"hello there s2") 141 | assert s2.recv() == b"hello there s2" 142 | 143 | 144 | # ToDo: Check in detail what is going wrong on pp3x-* wheels! Skipping for now. 145 | @pytest.mark.skip 146 | def test_sub_sock_options(): 147 | with pynng.Pub0(listen=addr) as pub: 148 | # test single option topic 149 | with pynng.Sub0(dial=addr, topics="beep", recv_timeout=1500) as sub: 150 | wait_pipe_len(sub, 1) 151 | wait_pipe_len(pub, 1) 152 | pub.send(b"beep hi") 153 | assert sub.recv() == b"beep hi" 154 | with pynng.Sub0(dial=addr, topics=["beep", "hello"], recv_timeout=500) as sub: 155 | wait_pipe_len(sub, 1) 156 | wait_pipe_len(pub, 1) 157 | pub.send(b"beep hi") 158 | assert sub.recv() == b"beep hi" 159 | pub.send(b"hello there") 160 | assert sub.recv() == b"hello there" 161 | 162 | 163 | # skip because it fails in CI for pypy (all platforms?) and Python 3.7 on Mac. 164 | # ideal 165 | @pytest.mark.skip 166 | def test_sockets_get_garbage_collected(): 167 | # from issue90 168 | with pynng.Pub0() as _: 169 | pass 170 | _ = None 171 | gc.collect() 172 | objs = [o for o in gc.get_objects() if isinstance(o, pynng.Pub0)] 173 | assert len(objs) == 0 174 | -------------------------------------------------------------------------------- /pynng/sockaddr.py: -------------------------------------------------------------------------------- 1 | import socket 2 | import struct 3 | 4 | import pynng 5 | 6 | 7 | class SockAddr: 8 | """ 9 | Python wrapper for struct nng_sockaddr. 10 | 11 | """ 12 | 13 | type_to_str = { 14 | pynng.lib.NNG_AF_UNSPEC: "Unspecified", 15 | pynng.lib.NNG_AF_INPROC: "inproc", 16 | pynng.lib.NNG_AF_IPC: "ipc", 17 | pynng.lib.NNG_AF_INET: "inet", 18 | pynng.lib.NNG_AF_INET6: "inetv6", 19 | pynng.lib.NNG_AF_ZT: "zerotier", 20 | pynng.lib.NNG_AF_ABSTRACT: "abstract", 21 | } 22 | 23 | def __init__(self, ffi_sock_addr): 24 | self._sock_addr = ffi_sock_addr 25 | 26 | @property 27 | def sock_addr(self): 28 | return self._sock_addr[0] 29 | 30 | @property 31 | def family(self): 32 | """Return the integer representing the socket address family""" 33 | return self.sock_addr.s_family 34 | 35 | @property 36 | def family_as_str(self): 37 | """The string representation of the address family.""" 38 | return self.type_to_str[self.family] 39 | 40 | def __repr__(self): 41 | return "nng_sockaddr {{.family = {}}}".format(self.family) 42 | 43 | 44 | class InprocAddr(SockAddr): 45 | def __init__(self, ffi_sock_addr): 46 | super().__init__(ffi_sock_addr) 47 | # union member 48 | self._mem = self.sock_addr.s_inproc 49 | 50 | @property 51 | def name_bytes(self): 52 | name = pynng.ffi.string(self._mem.sa_name) 53 | return name 54 | 55 | @property 56 | def name(self): 57 | return self.name_bytes.decode() 58 | 59 | def __str__(self): 60 | return self.name 61 | 62 | 63 | class IPCAddr(SockAddr): 64 | def __init__(self, ffi_sock_addr): 65 | super().__init__(ffi_sock_addr) 66 | # union member 67 | self._mem = self.sock_addr.s_ipc 68 | 69 | @property 70 | def path_bytes(self): 71 | path = pynng.ffi.string(self._mem.sa_path) 72 | return path 73 | 74 | @property 75 | def path(self): 76 | return self.path_bytes.decode() 77 | 78 | def __str__(self): 79 | return self.path 80 | 81 | 82 | class InAddr(SockAddr): 83 | def __init__(self, ffi_sock_addr): 84 | super().__init__(ffi_sock_addr) 85 | # union member 86 | self._mem = self.sock_addr.s_in 87 | 88 | @property 89 | def port(self): 90 | """Port, big-endian style""" 91 | return self._mem.sa_port 92 | 93 | @property 94 | def addr(self): 95 | """IP address as big-endian 32-bit number""" 96 | return self._mem.sa_addr 97 | 98 | def __str__(self): 99 | as_bytes = struct.pack("I", self.addr) 100 | ip = socket.inet_ntop(socket.AF_INET, as_bytes) 101 | port = socket.ntohs(self.port) 102 | return "{}:{}".format(ip, port) 103 | 104 | 105 | class In6Addr(SockAddr): 106 | def __init__(self, ffi_sock_addr): 107 | super().__init__(ffi_sock_addr) 108 | # union member 109 | self._mem = self.sock_addr.s_in6 110 | 111 | @property 112 | def port(self): 113 | """Port, big-endian style""" 114 | return self._mem.sa_port 115 | 116 | @property 117 | def addr(self): 118 | """IP address as big-endian bytes""" 119 | return bytes(self._mem.sa_addr) 120 | 121 | def __str__(self): 122 | # TODO: not a good string repr at all 123 | ip = socket.inet_ntop(socket.AF_INET6, self.addr) 124 | port = socket.ntohs(self.port) 125 | return "[{}]:{}".format(ip, port) 126 | 127 | 128 | class ZTAddr(SockAddr): 129 | def __init__(self, ffi_sock_addr): 130 | super().__init__(ffi_sock_addr) 131 | # union member 132 | self._mem = self.sock_addr.s_zt 133 | 134 | @property 135 | def nwid(self): 136 | return self._mem.as_nwid 137 | 138 | @property 139 | def nodeid(self): 140 | return self._mem.as_nodeid 141 | 142 | @property 143 | def port(self): 144 | return self._mem.as_port 145 | 146 | 147 | class AbstractAddr(SockAddr): 148 | def __init__(self, ffi_sock_addr): 149 | super().__init__(ffi_sock_addr) 150 | # union member 151 | self._mem = self.sock_addr.s_abstract 152 | 153 | @property 154 | def name_bytes(self): 155 | # Get the length of the name from sa_len 156 | name_len = self._mem.sa_len 157 | # Return the raw bytes of the name (up to name_len) 158 | return bytes(self._mem.sa_name[0:name_len]) 159 | 160 | @property 161 | def name(self): 162 | # Decode the name bytes, handling any URI encoding 163 | import urllib.parse 164 | name_bytes = self.name_bytes 165 | try: 166 | # Try to decode as UTF-8 first 167 | name_str = name_bytes.decode('utf-8') 168 | # Unescape any URI-encoded characters 169 | return urllib.parse.unquote(name_str) 170 | except UnicodeDecodeError: 171 | # If UTF-8 decoding fails, return a representation of the bytes 172 | return repr(name_bytes) 173 | 174 | def __str__(self): 175 | # Return the abstract socket URI format 176 | import urllib.parse 177 | try: 178 | # Try to encode as UTF-8 and URI-encode 179 | name_str = self.name_bytes.decode('utf-8') 180 | encoded_name = urllib.parse.quote(name_str) 181 | return f"abstract://{encoded_name}" 182 | except UnicodeDecodeError: 183 | # If UTF-8 encoding fails, use a hex representation 184 | hex_name = self.name_bytes.hex() 185 | return f"abstract://{hex_name}" 186 | 187 | 188 | def _nng_sockaddr(sa): 189 | # ensures the correct class gets instantiated based on s_family 190 | lookup = { 191 | pynng.lib.NNG_AF_INPROC: InprocAddr, 192 | pynng.lib.NNG_AF_IPC: IPCAddr, 193 | pynng.lib.NNG_AF_INET: InAddr, 194 | pynng.lib.NNG_AF_INET6: In6Addr, 195 | pynng.lib.NNG_AF_ZT: ZTAddr, 196 | pynng.lib.NNG_AF_ABSTRACT: AbstractAddr, 197 | } 198 | # fall through to SockAddr, e.g. if it's unspecified 199 | cls = lookup.get(sa[0].s_family, SockAddr) 200 | return cls(sa) 201 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Configuration file for the Sphinx documentation builder. 4 | # 5 | # This file does only contain a selection of the most common options. For a 6 | # full list see the documentation: 7 | # http://www.sphinx-doc.org/en/master/config 8 | 9 | # -- Path setup -------------------------------------------------------------- 10 | 11 | # If extensions (or modules to document with autodoc) are in another directory, 12 | # add these directories to sys.path here. If the directory is relative to the 13 | # documentation root, use os.path.abspath to make it absolute, like shown here. 14 | # 15 | 16 | 17 | # -- Project information ----------------------------------------------------- 18 | 19 | project = "pynng" 20 | copyright = "2018 - 2024 Cody Piersall" 21 | author = "Cody Piersall" 22 | 23 | # The short X.Y version 24 | version = "" 25 | # The full version, including alpha/beta/rc tags 26 | release = "0.1.0" 27 | 28 | 29 | # -- General configuration --------------------------------------------------- 30 | 31 | # If your documentation needs a minimal Sphinx version, state it here. 32 | # 33 | # needs_sphinx = '1.0' 34 | 35 | # Add any Sphinx extension module names here, as strings. They can be 36 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 37 | # ones. 38 | extensions = [ 39 | "sphinx.ext.autodoc", 40 | "sphinx.ext.napoleon", 41 | "sphinx.ext.intersphinx", 42 | "sphinxcontrib_trio", 43 | ] 44 | 45 | intersphinx_mapping = { 46 | "python": ("https://docs.python.org/3", None), 47 | } 48 | 49 | # Add any paths that contain templates here, relative to this directory. 50 | templates_path = ["_templates"] 51 | 52 | # The suffix(es) of source filenames. 53 | # You can specify multiple suffix as a list of string: 54 | # 55 | # source_suffix = ['.rst', '.md'] 56 | source_suffix = ".rst" 57 | 58 | # The master toctree document. 59 | master_doc = "index" 60 | 61 | # The language for content autogenerated by Sphinx. Refer to documentation 62 | # for a list of supported languages. 63 | # 64 | # This is also used if you do content translation via gettext catalogs. 65 | # Usually you set "language" from the command line for these cases. 66 | language = "en" 67 | 68 | # List of patterns, relative to source directory, that match files and 69 | # directories to ignore when looking for source files. 70 | # This pattern also affects html_static_path and html_extra_path . 71 | exclude_patterns = [] 72 | 73 | # The name of the Pygments (syntax highlighting) style to use. 74 | pygments_style = "default" 75 | 76 | # highlight async correctly 77 | highlight_language = "python3" 78 | 79 | # -- Options for HTML output ------------------------------------------------- 80 | 81 | # The theme to use for HTML and HTML Help pages. See the documentation for 82 | # a list of builtin themes. 83 | # 84 | import sphinx_rtd_theme 85 | 86 | html_theme = "sphinx_rtd_theme" 87 | 88 | # Theme options are theme-specific and customize the look and feel of a theme 89 | # further. For a list of options available for each theme, see the 90 | # documentation. 91 | # 92 | # html_theme_options = {} 93 | 94 | # Add any paths that contain custom static files (such as style sheets) here, 95 | # relative to this directory. They are copied after the builtin static files, 96 | # so a file named "default.css" will overwrite the builtin "default.css". 97 | html_static_path = ["_static"] 98 | 99 | # Custom sidebar templates, must be a dictionary that maps document names 100 | # to template names. 101 | # 102 | # The default sidebars (for documents that don't match any pattern) are 103 | # defined by theme itself. Builtin themes are using these templates by 104 | # default: ``['localtoc.html', 'relations.html', 'sourcelink.html', 105 | # 'searchbox.html']``. 106 | # 107 | # html_sidebars = {} 108 | 109 | 110 | # -- Options for HTMLHelp output --------------------------------------------- 111 | 112 | # Output file base name for HTML help builder. 113 | htmlhelp_basename = "pynngdoc" 114 | 115 | 116 | # -- Options for LaTeX output ------------------------------------------------ 117 | 118 | latex_elements = { 119 | # The paper size ('letterpaper' or 'a4paper'). 120 | # 121 | # 'papersize': 'letterpaper', 122 | # The font size ('10pt', '11pt' or '12pt'). 123 | # 124 | # 'pointsize': '10pt', 125 | # Additional stuff for the LaTeX preamble. 126 | # 127 | # 'preamble': '', 128 | # Latex figure (float) alignment 129 | # 130 | # 'figure_align': 'htbp', 131 | } 132 | 133 | # Grouping the document tree into LaTeX files. List of tuples 134 | # (source start file, target name, title, 135 | # author, documentclass [howto, manual, or own class]). 136 | latex_documents = [ 137 | (master_doc, "pynng.tex", "pynng Documentation", "Cody Piersall", "manual"), 138 | ] 139 | 140 | 141 | # -- Options for manual page output ------------------------------------------ 142 | 143 | # One entry per manual page. List of tuples 144 | # (source start file, name, description, authors, manual section). 145 | man_pages = [(master_doc, "pynng", "pynng Documentation", [author], 1)] 146 | 147 | 148 | # -- Options for Texinfo output ---------------------------------------------- 149 | 150 | # Grouping the document tree into Texinfo files. List of tuples 151 | # (source start file, target name, title, author, 152 | # dir menu entry, description, category) 153 | texinfo_documents = [ 154 | ( 155 | master_doc, 156 | "pynng", 157 | "pynng Documentation", 158 | author, 159 | "pynng", 160 | "One line description of project.", 161 | "Miscellaneous", 162 | ), 163 | ] 164 | 165 | 166 | # -- Options for Epub output ------------------------------------------------- 167 | 168 | # Bibliographic Dublin Core info. 169 | epub_title = project 170 | epub_author = author 171 | epub_publisher = author 172 | epub_copyright = copyright 173 | 174 | # The unique identifier of the text. This can be a ISBN number 175 | # or the project homepage. 176 | # 177 | # epub_identifier = '' 178 | 179 | # A unique identification for the text. 180 | # 181 | # epub_uid = '' 182 | 183 | # A list of files that should not be packed into the epub file. 184 | epub_exclude_files = ["search.html"] 185 | 186 | # -- Extension configuration ------------------------------------------------- 187 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | from subprocess import check_call 3 | import platform 4 | import shutil 5 | import sys 6 | 7 | if platform.machine() == "i686" and platform.system() == "Linux": 8 | # mbedtls v3.5.1 will not build without these flags on 32-bit linux. 9 | # https://github.com/Mbed-TLS/mbedtls/issues/8334 10 | # this is hopefully going to be fixed in another release. 11 | # There is probably a better way to do this... 12 | os.environ["CFLAGS"] = "-mpclmul -msse2 -maes" 13 | from setuptools import Command, setup, find_packages 14 | from setuptools.command.build_ext import build_ext 15 | 16 | WINDOWS = sys.platform == "win32" 17 | 18 | THIS_DIR = os.path.abspath(os.path.dirname(__file__)) 19 | 20 | 21 | def maybe_copy(src, dst): 22 | os.makedirs(os.path.dirname(dst), exist_ok=True) 23 | if os.path.exists(src): 24 | shutil.copy(src, dst) 25 | 26 | 27 | class BuilderBase(Command): 28 | """Base Class for building vendored dependencies""" 29 | 30 | user_options = [ 31 | ("repo=", None, "GitHub repository URL."), 32 | ("rev=", None, "GitHub repository revision."), 33 | ] 34 | 35 | flags = ["-DCMAKE_POSITION_INDEPENDENT_CODE:BOOL=true"] 36 | is_64bit = sys.maxsize > 2**32 37 | if WINDOWS: 38 | if is_64bit: 39 | flags += ["-A", "x64"] 40 | else: 41 | flags += ["-A", "win32"] 42 | else: 43 | if shutil.which("ninja"): 44 | print("~~~building with ninja~~~", file=sys.stderr) 45 | # the ninja build generator is a million times faster. 46 | flags += ["-G", "Ninja"] 47 | else: 48 | print("~~~building without ninja~~~", file=sys.stderr) 49 | 50 | cmake_cmd = ["cmake"] + flags 51 | 52 | def initialize_options(self): 53 | """Set default values for options.""" 54 | self.repo = "" 55 | self.rev = "" 56 | 57 | def finalize_options(self): 58 | """Post-process options.""" 59 | pass 60 | 61 | def run(self): 62 | """Clone nng and build it with cmake, with TLS enabled.""" 63 | if not os.path.exists(self.git_dir): 64 | check_call("git clone {}".format(self.repo), shell=True) 65 | check_call("git checkout {}".format(self.rev), shell=True, cwd=self.git_dir) 66 | if not os.path.exists(self.build_dir): 67 | os.mkdir(self.build_dir) 68 | 69 | cmake_cmd = [*self.cmake_cmd, *self.cmake_extra_args, ".."] 70 | print(f"building {self.git_dir} with:", cmake_cmd, flush=True) 71 | check_call(cmake_cmd, cwd=self.build_dir) 72 | 73 | self.finalize_build() 74 | 75 | 76 | class BuildNng(BuilderBase): 77 | description = "build the nng library" 78 | build_dir = "nng/build" 79 | 80 | def __init__(self, *args, **kwargs): 81 | super().__init__(*args, **kwargs) 82 | self.git_dir = "nng" 83 | self.cmake_extra_args = [ 84 | "-DNNG_ENABLE_TLS=ON", 85 | "-DNNG_TESTS=OFF", 86 | "-DNNG_TOOLS=OFF", 87 | "-DCMAKE_BUILD_TYPE=Release", 88 | "-DMBEDTLS_ROOT_DIR={}/mbedtls/prefix/".format(THIS_DIR), 89 | ] 90 | 91 | def finalize_build(self): 92 | check_call( 93 | "cmake --build . --config Release", 94 | shell=True, 95 | cwd=self.build_dir, 96 | ) 97 | if WINDOWS: 98 | # Move ninja stuff to Release directory, so it is where the build_pynng script 99 | # expects. 100 | maybe_copy(f"nng/build/nng.lib", "nng/build/Release/nng.lib") 101 | 102 | 103 | class BuildMbedTls(BuilderBase): 104 | description = "build the mbedtls library" 105 | build_dir = "mbedtls/build" 106 | 107 | def __init__(self, *args, **kwargs): 108 | super().__init__(*args, **kwargs) 109 | self.git_dir = "mbedtls" 110 | self.cmake_extra_args = [ 111 | "-DENABLE_PROGRAMS=OFF", 112 | "-DCMAKE_BUILD_TYPE=Release", 113 | "-DCMAKE_INSTALL_PREFIX=../prefix", 114 | "-DENABLE_TESTING=OFF", 115 | ] 116 | 117 | def finalize_build(self): 118 | check_call( 119 | "cmake --build . --config Release --target install", 120 | shell=True, 121 | cwd=self.build_dir, 122 | ) 123 | if WINDOWS: 124 | # Move ninja stuff to Release directory, so it is where the build_pynng script 125 | # expects. 126 | src = "./mbedtls/build/library/" 127 | dst = "./mbedtls/build/library/Release/" 128 | maybe_copy(src + "mbedtls.lib", dst + "mbedtls.lib") 129 | maybe_copy(src + "mbedx509.lib", dst + "mbedx509.lib") 130 | maybe_copy(src + "mbedcrypto.lib", dst + "mbedcrypto.lib") 131 | else: 132 | # kinda hacky... 133 | # In CI, mbedtls installs its libraries into mbedtls/prefix/lib64. 134 | # Not totally sure when this happened, but something in mbedtls changed, 135 | # likely commit 0f2e87bdf534a967937882e7381e067d9b1cb135, when they started 136 | # using GnuInstallDirs. Couldn't build to verify but likely enough. 137 | src = f"{THIS_DIR}/mbedtls/prefix/lib64" 138 | dst = f"{THIS_DIR}/mbedtls/prefix/lib" 139 | if os.path.exists(src) and not os.path.exists(dst): 140 | shutil.copytree(src, dst) 141 | 142 | 143 | class BuildBuild(build_ext): 144 | """ 145 | Custom build command 146 | """ 147 | 148 | def initialize_options(self): 149 | """ 150 | Set default values for options 151 | Each user option must be listed here with their default value. 152 | """ 153 | build_ext.initialize_options(self) 154 | 155 | def run(self): 156 | """ 157 | Running... 158 | """ 159 | self.run_command("build_mbedtls") 160 | self.run_command("build_nng") 161 | 162 | build_ext.run(self) # proceed with "normal" build steps 163 | 164 | 165 | with open("README.md", "r", encoding="utf-8") as f: 166 | long_description = f.read() 167 | 168 | setup( 169 | cmdclass={ 170 | "build_mbedtls": BuildMbedTls, 171 | "build_nng": BuildNng, 172 | "build_ext": BuildBuild, 173 | }, 174 | cffi_modules=["build_pynng.py:ffibuilder"], 175 | ) 176 | -------------------------------------------------------------------------------- /test/test_pipe.py: -------------------------------------------------------------------------------- 1 | """ 2 | Let's test up those pipes 3 | """ 4 | 5 | 6 | import time 7 | 8 | import pytest 9 | 10 | import pynng 11 | from _test_util import wait_pipe_len 12 | 13 | addr = "inproc://test-addr" 14 | 15 | 16 | def test_pipe_gets_added_and_removed(): 17 | with pynng.Pair0(listen=addr) as s0, pynng.Pair0() as s1: 18 | assert len(s0.pipes) == 0 19 | assert len(s1.pipes) == 0 20 | s1.dial(addr) 21 | wait_pipe_len(s0, 1) 22 | wait_pipe_len(s1, 1) 23 | wait_pipe_len(s0, 0) 24 | wait_pipe_len(s1, 0) 25 | 26 | 27 | def test_close_pipe_works(): 28 | with pynng.Pair0() as s0, pynng.Pair0() as s1: 29 | # list of pipes that got the callback called on them 30 | cb_pipes = [] 31 | 32 | def cb(pipe): 33 | cb_pipes.append(pipe) 34 | 35 | # first add callbacks, before listening and dialing. The callback just adds 36 | # the pipe to cb_pipes; but this way we can ensure the callback got called. 37 | s0.add_post_pipe_remove_cb(cb) 38 | s1.add_post_pipe_remove_cb(cb) 39 | s0.listen(addr) 40 | s1.dial(addr) 41 | wait_pipe_len(s0, 1) 42 | wait_pipe_len(s1, 1) 43 | p0 = s0.pipes[0] 44 | p1 = s1.pipes[0] 45 | pipe0 = s0.pipes[0] 46 | pipe0.close() 47 | # time out in 5 seconds if stuff dosen't work 48 | timeout = time.monotonic() + 5.0 49 | while len(cb_pipes) < 2 and time.monotonic() < timeout: 50 | time.sleep(0.0005) 51 | if time.monotonic() > timeout: 52 | raise TimeoutError( 53 | "Pipe close callbacks were not called; pipe close doesn't work?" 54 | ) 55 | # we cannot assert the length of cb_pipes is 2 because the sockets might have 56 | # reconnected in the meantime, so we can only assert that the pipes that 57 | # *should* have been closed *have* been closed. 58 | assert p0 in cb_pipes and p1 in cb_pipes 59 | 60 | 61 | def test_pipe_local_and_remote_addresses(): 62 | with pynng.Pair0(listen=addr) as s0, pynng.Pair0(dial=addr) as s1: 63 | wait_pipe_len(s0, 1) 64 | wait_pipe_len(s1, 1) 65 | p0 = s0.pipes[0] 66 | p1 = s1.pipes[0] 67 | local_addr0 = p0.local_address 68 | remote_addr0 = p0.remote_address 69 | local_addr1 = p1.local_address 70 | remote_addr1 = p1.remote_address 71 | assert str(local_addr0) == addr.replace("tcp://", "") 72 | assert str(local_addr0) == str(remote_addr1) 73 | # on Windows, the local address is 0.0.0.0:0 74 | assert str(local_addr1) == str(remote_addr0) 75 | 76 | 77 | def test_pre_pipe_connect_cb_totally_works(): 78 | with pynng.Pair0(listen=addr) as s0, pynng.Pair0() as s1: 79 | called = False 80 | 81 | def pre_connect_cb(_): 82 | nonlocal called 83 | called = True 84 | 85 | s0.add_pre_pipe_connect_cb(pre_connect_cb) 86 | s1.dial(addr) 87 | wait_pipe_len(s0, 1) 88 | wait_pipe_len(s1, 1) 89 | assert called 90 | 91 | 92 | def test_closing_pipe_in_pre_connect_works(): 93 | with pynng.Pair0(listen=addr) as s0, pynng.Pair0() as s1: 94 | s0.name = "s0" 95 | s1.name = "s1" 96 | pre_connect_cb_was_called = False 97 | post_connect_cb_was_called = False 98 | 99 | def pre_connect_cb(pipe): 100 | pipe.close() 101 | nonlocal pre_connect_cb_was_called 102 | pre_connect_cb_was_called = True 103 | 104 | def post_connect_cb(pipe): 105 | nonlocal post_connect_cb_was_called 106 | post_connect_cb_was_called = True 107 | 108 | s0.add_pre_pipe_connect_cb(pre_connect_cb) 109 | s0.add_post_pipe_connect_cb(post_connect_cb) 110 | s1.add_pre_pipe_connect_cb(pre_connect_cb) 111 | s1.add_post_pipe_connect_cb(post_connect_cb) 112 | 113 | s1.dial(addr) 114 | later = time.monotonic() + 5 115 | while later > time.monotonic(): 116 | if pre_connect_cb_was_called: 117 | break 118 | # just give other threads a chance to run 119 | time.sleep(0.0001) 120 | assert pre_connect_cb_was_called 121 | wait_pipe_len(s0, 0) 122 | wait_pipe_len(s1, 0) 123 | assert not post_connect_cb_was_called 124 | 125 | 126 | def test_post_pipe_connect_cb_works(): 127 | with pynng.Pair0(listen=addr) as s0, pynng.Pair0() as s1: 128 | post_called = False 129 | 130 | def post_connect_cb(pipe): 131 | nonlocal post_called 132 | post_called = True 133 | 134 | s0.add_post_pipe_connect_cb(post_connect_cb) 135 | s1.dial(addr) 136 | 137 | later = time.time() + 10 138 | while later > time.time(): 139 | if post_called: 140 | break 141 | assert post_called 142 | 143 | 144 | def test_post_pipe_remove_cb_works(): 145 | with pynng.Pair0(listen=addr) as s0, pynng.Pair0() as s1: 146 | post_called = False 147 | 148 | def post_remove_cb(pipe): 149 | nonlocal post_called 150 | post_called = True 151 | 152 | s0.add_post_pipe_remove_cb(post_remove_cb) 153 | s1.dial(addr) 154 | wait_pipe_len(s0, 1) 155 | wait_pipe_len(s1, 1) 156 | assert not post_called 157 | 158 | later = time.time() + 10 159 | while later > time.time(): 160 | if post_called: 161 | break 162 | assert post_called 163 | 164 | 165 | def test_can_send_from_pipe(): 166 | with pynng.Pair0(listen=addr) as s0, pynng.Pair0(dial=addr) as s1: 167 | wait_pipe_len(s0, 1) 168 | s0.send(b"hello") 169 | assert s1.recv() == b"hello" 170 | s0.send_msg(pynng.Message(b"it is me again")) 171 | assert s1.recv() == b"it is me again" 172 | 173 | 174 | @pytest.mark.trio 175 | async def test_can_asend_from_pipe(): 176 | with pynng.Pair0(listen=addr) as s0, pynng.Pair0(dial=addr) as s1: 177 | wait_pipe_len(s0, 1) 178 | await s0.asend(b"hello") 179 | assert await s1.arecv() == b"hello" 180 | await s0.asend_msg(pynng.Message(b"it is me again")) 181 | assert await s1.arecv() == b"it is me again" 182 | 183 | 184 | def test_bad_callbacks_dont_cause_extra_failures(): 185 | called_pre_connect = False 186 | 187 | def pre_connect_cb(pipe): 188 | nonlocal called_pre_connect 189 | called_pre_connect = True 190 | 191 | with pynng.Pair0(listen=addr) as s0: 192 | # adding something that is not a callback should still allow correct 193 | # things to work. 194 | s0.add_pre_pipe_connect_cb(8) 195 | s0.add_pre_pipe_connect_cb(pre_connect_cb) 196 | with pynng.Pair0(dial=addr) as _: 197 | wait_pipe_len(s0, 1) 198 | later = time.time() + 10 199 | while later > time.time(): 200 | if called_pre_connect: 201 | break 202 | assert called_pre_connect 203 | -------------------------------------------------------------------------------- /test/test_protocols.py: -------------------------------------------------------------------------------- 1 | import time 2 | import threading 3 | 4 | import pytest 5 | 6 | import pynng 7 | 8 | from _test_util import wait_pipe_len 9 | 10 | # TODO: all sockets need timeouts 11 | 12 | addr = "inproc://test-addr" 13 | 14 | 15 | def test_bus(): 16 | with pynng.Bus0(recv_timeout=100) as s0, pynng.Bus0( 17 | recv_timeout=100 18 | ) as s1, pynng.Bus0(recv_timeout=100) as s2: 19 | s0.listen(addr) 20 | s1.dial(addr) 21 | s2.dial(addr) 22 | wait_pipe_len(s0, 2) 23 | s0.send(b"s1 and s2 get this") 24 | assert s1.recv() == b"s1 and s2 get this" 25 | assert s2.recv() == b"s1 and s2 get this" 26 | s1.send(b"only s0 gets this") 27 | assert s0.recv() == b"only s0 gets this" 28 | s2.recv_timeout = 0 29 | with pytest.raises(pynng.Timeout): 30 | s2.recv() 31 | 32 | 33 | def test_context_manager_works(): 34 | # we have to grab a reference to the sockets so garbage collection doesn't 35 | # close the socket for us automatically. 36 | with pynng.Pair0(listen=addr) as s0: # noqa 37 | pass 38 | # we should be able to do it again if the context manager worked 39 | with pynng.Pair0(listen=addr) as s1: # noqa 40 | pass 41 | 42 | 43 | def test_pair0(): 44 | with pynng.Pair0(listen=addr, recv_timeout=100) as s0, pynng.Pair0( 45 | dial=addr, recv_timeout=100 46 | ) as s1: 47 | s1.send(b"hey howdy there") 48 | assert s0.recv() == b"hey howdy there" 49 | 50 | 51 | def test_pair1(): 52 | with pynng.Pair1(listen=addr, recv_timeout=100) as s0, pynng.Pair1( 53 | dial=addr, recv_timeout=100 54 | ) as s1: 55 | s1.send(b"beep boop beep") 56 | assert s0.recv() == b"beep boop beep" 57 | 58 | 59 | def test_reqrep0(): 60 | with pynng.Req0(listen=addr, recv_timeout=100) as req, pynng.Rep0( 61 | dial=addr, recv_timeout=100 62 | ) as rep: 63 | request = b"i am requesting" 64 | req.send(request) 65 | assert rep.recv() == request 66 | 67 | response = b"i am responding" 68 | rep.send(response) 69 | assert req.recv() == response 70 | 71 | with pytest.raises(pynng.BadState): 72 | req.recv() 73 | 74 | # responders can't send before receiving 75 | with pytest.raises(pynng.BadState): 76 | rep.send(b"I cannot do this why am I trying") 77 | 78 | 79 | def test_pubsub0(): 80 | with pynng.Sub0(listen=addr, recv_timeout=100) as sub, pynng.Pub0( 81 | dial=addr, recv_timeout=100 82 | ) as pub: 83 | sub.subscribe(b"") 84 | msg = b"i am requesting" 85 | wait_pipe_len(sub, 1) 86 | wait_pipe_len(pub, 1) 87 | pub.send(msg) 88 | assert sub.recv() == msg 89 | 90 | # TODO: when changing exceptions elsewhere, change here! 91 | # publishers can't recv 92 | with pytest.raises(pynng.NotSupported): 93 | pub.recv() 94 | 95 | # responders can't send before receiving 96 | with pytest.raises(pynng.NotSupported): 97 | sub.send( 98 | b"""I am a bold subscribing socket. I believe I was truly 99 | meant to be a publisher. The world needs to hear what 100 | I have to say! 101 | """ 102 | ) 103 | # alas, it was not meant to be, subscriber. Not every socket was 104 | # meant to publish. 105 | 106 | 107 | def test_push_pull(): 108 | received = {"pull1": False, "pull2": False} 109 | with pynng.Push0(listen=addr) as push, pynng.Pull0( 110 | dial=addr, recv_timeout=1000 111 | ) as pull1, pynng.Pull0(dial=addr, recv_timeout=1000) as pull2: 112 | 113 | def recv1(): 114 | pull1.recv() 115 | received["pull1"] = True 116 | 117 | def recv2(): 118 | pull2.recv() 119 | received["pull2"] = True 120 | 121 | # push/pull does round robin style distribution 122 | t1 = threading.Thread(target=recv1, daemon=True) 123 | t2 = threading.Thread(target=recv2, daemon=True) 124 | 125 | t1.start() 126 | t2.start() 127 | wait_pipe_len(push, 2) 128 | wait_pipe_len(pull1, 1) 129 | wait_pipe_len(pull1, 1) 130 | 131 | push.send(b"somewhere someone should see this") 132 | push.send(b"somewhereeeee") 133 | t1.join() 134 | t2.join() 135 | assert received["pull1"] 136 | assert received["pull2"] 137 | 138 | 139 | def test_surveyor_respondent(): 140 | with pynng.Surveyor0(listen=addr, recv_timeout=4000) as surveyor, pynng.Respondent0( 141 | dial=addr, recv_timeout=4000 142 | ) as resp1, pynng.Respondent0(dial=addr, recv_timeout=4000) as resp2: 143 | query = b"hey how's it going buddy?" 144 | # wait for sockets to connect 145 | wait_pipe_len(surveyor, 2) 146 | wait_pipe_len(resp1, 1) 147 | wait_pipe_len(resp2, 1) 148 | surveyor.send(query) 149 | assert resp1.recv() == query 150 | assert resp2.recv() == query 151 | resp1.send(b"not too bad I suppose") 152 | 153 | msg2 = b""" 154 | Thanks for asking. It's been a while since I've had 155 | human contact; times have been difficult for me. I woke up this 156 | morning and again could not find a pair of matching socks. I know that 157 | a lot of people think it's worth it to just throw all your old socks 158 | out and buy like 12 pairs of identical socks, but that just seems so 159 | mundane. Life is about more than socks, you know? So anyway, since I 160 | couldn't find any socks, I went ahead and put banana peels on my 161 | feet. They don't match *perfectly* but it's close enough. Anyway 162 | thanks for asking, I guess I'm doing pretty good. 163 | """ 164 | resp2.send(msg2) 165 | resp = [surveyor.recv() for _ in range(2)] 166 | assert b"not too bad I suppose" in resp 167 | assert msg2 in resp 168 | 169 | with pytest.raises(pynng.BadState): 170 | resp2.send(b"oadsfji") 171 | 172 | now = time.monotonic() 173 | # 1 millisecond timeout 174 | surveyor.survey_time = 10 175 | surveyor.send(b"hey nobody should respond to me") 176 | with pytest.raises(pynng.Timeout): 177 | surveyor.recv() 178 | later = time.monotonic() 179 | # nng default survey time is 1 second 180 | assert later - now < 0.9 181 | 182 | 183 | def test_cannot_instantiate_socket_without_opener(): 184 | with pytest.raises(TypeError): 185 | pynng.Socket() 186 | 187 | 188 | def test_can_instantiate_socket_with_raw_opener(): 189 | with pynng.Socket(opener=pynng.lib.nng_sub0_open_raw): 190 | pass 191 | 192 | 193 | def test_can_pass_addr_as_bytes_or_str(): 194 | with pynng.Pair0(listen=b"tcp://127.0.0.1:42421"), pynng.Pair0( 195 | dial="tcp://127.0.0.1:42421" 196 | ): 197 | pass 198 | -------------------------------------------------------------------------------- /pynng/options.py: -------------------------------------------------------------------------------- 1 | import pynng 2 | import pynng.sockaddr 3 | import pynng.tls 4 | 5 | 6 | def _get_inst_and_func(py_obj, option_type, get_or_set): 7 | """ 8 | Given the Python wrapper for one of nng's object types, return a tuple of 9 | (nng_object, nng_function). This is only an _internal_ function. 10 | 11 | Args: 12 | py_obj Union(pynng.Socket, pynng.Dialer, pynng.Listener): the Python wrapper of 13 | the library type. 14 | option_type (str): The type of option. 15 | 16 | Returns: 17 | The tuple (nng_object, nng_func) for use in the module-level getopt*, 18 | setopt* functions 19 | 20 | """ 21 | # nng option getter/setter functions are all named in the same consistent manner. 22 | # The form is: nng_{nng_type}_{access_method}_{option_type} 23 | # 24 | # Where: 25 | # * lib_type is "socket", "dialer", "listener", "pipe", or "ctx"; 26 | # * access_method is "get" or "set"; 27 | # * option_type is "bool", "int", "ms", "size", "uint64", "ptr", or "string" 28 | # 29 | # we can figure out which method to call based on the py_obj (Socket, Dialer, 30 | # etc.), whether we are getting or settting, and 31 | 32 | if isinstance(py_obj, pynng.Socket): 33 | name = "socket" 34 | obj = py_obj.socket 35 | elif isinstance(py_obj, pynng.Dialer): 36 | name = "dialer" 37 | obj = py_obj.dialer 38 | elif isinstance(py_obj, pynng.Listener): 39 | name = "listener" 40 | obj = py_obj.listener 41 | elif isinstance(py_obj, pynng.Pipe): 42 | name = "pipe" 43 | obj = py_obj.pipe 44 | elif isinstance(py_obj, pynng.Context): 45 | name = "ctx" 46 | obj = py_obj.context 47 | else: 48 | raise TypeError(f'The type "{type(py_obj)}" is not supported') 49 | 50 | if option_type: 51 | function_name = f"nng_{name}_{get_or_set}_{option_type}" 52 | else: 53 | # opaque get/set 54 | function_name = f"nng_{name}_{get_or_set}" 55 | nng_func = getattr(pynng.lib, function_name) 56 | return obj, nng_func 57 | 58 | 59 | def _getopt_int(obj, option): 60 | """Gets the specified option""" 61 | i = pynng.ffi.new("int []", 1) 62 | opt_as_char = pynng.nng.to_char(option) 63 | obj, lib_func = _get_inst_and_func(obj, "int", "get") 64 | # attempt to accept floats that are exactly int 65 | ret = lib_func(obj, opt_as_char, i) 66 | pynng.check_err(ret) 67 | return i[0] 68 | 69 | 70 | def _setopt_int(py_obj, option, value): 71 | """Sets the specified option to the specified value""" 72 | opt_as_char = pynng.nng.to_char(option) 73 | # attempt to accept floats that are exactly int 74 | if not int(value) == value: 75 | msg = "Invalid value {} of type {}. Expected int." 76 | msg = msg.format(value, type(value)) 77 | raise ValueError(msg) 78 | obj, lib_func = _get_inst_and_func(py_obj, "int", "set") 79 | value = int(value) 80 | err = lib_func(obj, opt_as_char, value) 81 | pynng.check_err(err) 82 | 83 | 84 | def _getopt_size(py_obj, option): 85 | """Gets the specified size option""" 86 | i = pynng.ffi.new("size_t []", 1) 87 | opt_as_char = pynng.nng.to_char(option) 88 | # attempt to accept floats that are exactly int 89 | obj, lib_func = _get_inst_and_func(py_obj, "size", "get") 90 | ret = lib_func(obj, opt_as_char, i) 91 | pynng.check_err(ret) 92 | return i[0] 93 | 94 | 95 | def _setopt_size(py_obj, option, value): 96 | """Sets the specified size option to the specified value""" 97 | opt_as_char = pynng.nng.to_char(option) 98 | # attempt to accept floats that are exactly int 99 | if not int(value) == value: 100 | msg = "Invalid value {} of type {}. Expected int." 101 | msg = msg.format(value, type(value)) 102 | raise ValueError(msg) 103 | value = int(value) 104 | obj, lib_func = _get_inst_and_func(py_obj, "size", "set") 105 | lib_func(obj, opt_as_char, value) 106 | 107 | 108 | def _getopt_ms(py_obj, option): 109 | """Gets the specified option""" 110 | ms = pynng.ffi.new("nng_duration []", 1) 111 | opt_as_char = pynng.nng.to_char(option) 112 | obj, lib_func = _get_inst_and_func(py_obj, "ms", "get") 113 | ret = lib_func(obj, opt_as_char, ms) 114 | pynng.check_err(ret) 115 | return ms[0] 116 | 117 | 118 | def _setopt_ms(py_obj, option, value): 119 | """Sets the specified option to the specified value""" 120 | opt_as_char = pynng.nng.to_char(option) 121 | # attempt to accept floats that are exactly int (duration types are 122 | # just integers) 123 | if not int(value) == value: 124 | msg = "Invalid value {} of type {}. Expected int." 125 | msg = msg.format(value, type(value)) 126 | raise ValueError(msg) 127 | value = int(value) 128 | obj, lib_func = _get_inst_and_func(py_obj, "ms", "set") 129 | lib_func(obj, opt_as_char, value) 130 | 131 | 132 | def _getopt_string(py_obj, option): 133 | """Gets the specified string option""" 134 | opt = pynng.ffi.new("char *[]", 1) 135 | opt_as_char = pynng.nng.to_char(option) 136 | obj, lib_func = _get_inst_and_func(py_obj, "string", "get") 137 | ret = lib_func(obj, opt_as_char, opt) 138 | pynng.check_err(ret) 139 | py_string = pynng.ffi.string(opt[0]).decode() 140 | pynng.lib.nng_strfree(opt[0]) 141 | return py_string 142 | 143 | 144 | def _setopt_string(py_obj, option, value): 145 | """Sets the specified option to the specified value""" 146 | opt_as_char = pynng.nng.to_char(option) 147 | val_as_char = pynng.nng.to_char(value) 148 | obj, lib_func = _get_inst_and_func(py_obj, "string", "set") 149 | ret = lib_func(obj, opt_as_char, val_as_char) 150 | pynng.check_err(ret) 151 | 152 | 153 | def _setopt_string_nonnull(py_obj, option, value): 154 | opt_as_char = pynng.nng.to_char(option) 155 | val_as_char = pynng.nng.to_char(value) 156 | obj, lib_func = _get_inst_and_func(py_obj, "", "set") 157 | ret = lib_func(obj, opt_as_char, val_as_char, len(value)) 158 | pynng.check_err(ret) 159 | 160 | 161 | def _getopt_bool(py_obj, option): 162 | """Return the boolean value of the specified option""" 163 | opt_as_char = pynng.nng.to_char(option) 164 | b = pynng.ffi.new("bool []", 1) 165 | obj, lib_func = _get_inst_and_func(py_obj, "bool", "get") 166 | ret = lib_func(obj, opt_as_char, b) 167 | pynng.check_err(ret) 168 | return b[0] 169 | 170 | 171 | def _setopt_bool(py_obj, option, value): 172 | """Sets the specified option to the specified value.""" 173 | opt_as_char = pynng.nng.to_char(option) 174 | obj, lib_func = _get_inst_and_func(py_obj, "bool", "set") 175 | ret = lib_func(obj, opt_as_char, value) 176 | pynng.check_err(ret) 177 | 178 | 179 | def _getopt_sockaddr(py_obj, option): 180 | opt_as_char = pynng.nng.to_char(option) 181 | sock_addr = pynng.ffi.new("nng_sockaddr []", 1) 182 | obj, lib_func = _get_inst_and_func(py_obj, "addr", "get") 183 | ret = lib_func(obj, opt_as_char, sock_addr) 184 | pynng.check_err(ret) 185 | return pynng.sockaddr._nng_sockaddr(sock_addr) 186 | 187 | 188 | def _setopt_ptr(py_obj, option, value): 189 | if isinstance(value, pynng.tls.TLSConfig): 190 | value_ptr = value._tls_config 191 | else: 192 | msg = "Invalid value {} of type {}. Expected TLSConfig." 193 | msg = msg.format(value, type(value)) 194 | raise ValueError(msg) 195 | 196 | option_char = pynng.nng.to_char(option) 197 | obj, lib_func = _get_inst_and_func(py_obj, "ptr", "set") 198 | ret = lib_func(obj, option_char, value_ptr) 199 | pynng.check_err(ret) 200 | -------------------------------------------------------------------------------- /pynng/_aio.py: -------------------------------------------------------------------------------- 1 | """ 2 | Helpers for AIO functions 3 | """ 4 | 5 | import asyncio 6 | import sniffio 7 | 8 | from ._nng import ffi, lib 9 | import pynng 10 | from .exceptions import check_err 11 | import concurrent 12 | 13 | # global variable for mapping asynchronous operations with the Python data 14 | # assocated with them. Key is id(obj), value is obj 15 | _aio_map = {} 16 | 17 | 18 | @ffi.def_extern() 19 | def _async_complete(void_p): 20 | """ 21 | This is the callback provided to nng_aio_* functions which completes the 22 | Python future argument passed to it. It schedules _set_future_finished 23 | to run to complete the future associated with the event. 24 | """ 25 | # this is not a public interface, so asserting invariants is good. 26 | assert isinstance(void_p, ffi.CData) 27 | id = int(ffi.cast("size_t", void_p)) 28 | 29 | rescheduler = _aio_map.pop(id) 30 | rescheduler() 31 | 32 | 33 | def curio_helper(aio): 34 | import curio 35 | 36 | fut = concurrent.futures.Future() 37 | 38 | async def wait_for_aio(): 39 | try: 40 | await curio.traps._future_wait(fut) 41 | except curio.CancelledError: 42 | if fut.cancelled(): 43 | lib.nng_aio_cancel(aio.aio) 44 | 45 | err = lib.nng_aio_result(aio.aio) 46 | if err == lib.NNG_ECANCELED: 47 | raise curio.CancelledError() 48 | check_err(err) 49 | 50 | def callback(): 51 | if not fut.cancelled(): 52 | fut.set_result(True) 53 | 54 | return wait_for_aio(), callback 55 | 56 | 57 | def asyncio_helper(aio): 58 | """ 59 | Returns a callable that will be passed to _async_complete. The callable is 60 | responsible for rescheduling the event loop 61 | 62 | """ 63 | loop = asyncio.get_event_loop() 64 | fut = loop.create_future() 65 | 66 | async def wait_for_aio(): 67 | already_called_nng_aio_cancel = False 68 | while True: 69 | try: 70 | await asyncio.shield(fut) 71 | except asyncio.CancelledError: 72 | if not already_called_nng_aio_cancel: 73 | lib.nng_aio_cancel(aio.aio) 74 | already_called_nng_aio_cancel = True 75 | else: 76 | break 77 | err = lib.nng_aio_result(aio.aio) 78 | if err == lib.NNG_ECANCELED: 79 | raise asyncio.CancelledError 80 | check_err(err) 81 | 82 | def _set_future_finished(fut): 83 | if not fut.done(): 84 | fut.set_result(None) 85 | 86 | def rescheduler(): 87 | loop.call_soon_threadsafe(_set_future_finished, fut) 88 | 89 | return wait_for_aio(), rescheduler 90 | 91 | 92 | def trio_helper(aio): 93 | # Record the info needed to get back into this task 94 | import trio 95 | 96 | token = trio.lowlevel.current_trio_token() 97 | task = trio.lowlevel.current_task() 98 | 99 | def resumer(): 100 | token.run_sync_soon(trio.lowlevel.reschedule, task) 101 | 102 | async def wait_for_aio(): 103 | # Machinery to handle Trio cancellation, and convert it into nng cancellation 104 | raise_cancel_fn = None 105 | 106 | def abort_fn(raise_cancel_arg): 107 | # This function is called if Trio wants to cancel the operation. 108 | # First, ask nng to cancel the operation. 109 | lib.nng_aio_cancel(aio.aio) 110 | # nng cancellation doesn't happen immediately, so we need to save the raise_cancel function 111 | # into the enclosing scope to call it later, after we find out if the cancellation actually happened. 112 | nonlocal raise_cancel_fn 113 | raise_cancel_fn = raise_cancel_arg 114 | # And then tell Trio that we weren't able to cancel the operation immediately, so it should keep 115 | # waiting. 116 | return trio.lowlevel.Abort.FAILED 117 | 118 | # Put the Trio task to sleep. 119 | await trio.lowlevel.wait_task_rescheduled(abort_fn) 120 | 121 | err = lib.nng_aio_result(aio.aio) 122 | if err == lib.NNG_ECANCELED: 123 | # This operation was successfully cancelled. 124 | # Call the function Trio gave us, which raises the proper Trio cancellation exception 125 | raise_cancel_fn() 126 | check_err(err) 127 | 128 | return wait_for_aio(), resumer 129 | 130 | 131 | class AIOHelper: 132 | """ 133 | Handles the nng_aio operations for the correct event loop. This class 134 | mostly exists to easily keep up with resources and, to some extent, 135 | abstract away different event loops; event loop implementations are now 136 | punted into the module level helper functions. Theoretically it should be 137 | somewhat straightforward to support different event loops by adding a key 138 | to the ``_aio_helper_map`` and supplying a helper function. 139 | """ 140 | 141 | # global dict that maps {event loop: helper_function}. The helper function 142 | # takes one argument (an AIOHelper instance) and returns an (awaitable, 143 | # callback_function) tuple. The callback_function will be called (with no 144 | # argumnts provided) to mark the awaitable ready. 145 | # 146 | # It might just be clearer to look at the implementation of trio_helper and 147 | # asyncio_helper to get an idea of what the functions need to do. 148 | _aio_helper_map = { 149 | "asyncio": asyncio_helper, 150 | "trio": trio_helper, 151 | "curio": curio_helper, 152 | } 153 | 154 | def __init__(self, obj, async_backend): 155 | # set to None now so we can know if we need to free it later 156 | # This should be at the top of __init__ so that __del__ doesn't raise 157 | # an unexpected AttributeError if something funky happens 158 | self.aio = None 159 | # this is not a public interface, let's make some assertions 160 | assert isinstance(obj, (pynng.Socket, pynng.Context)) 161 | # we need to choose the correct nng lib functions based on the type of 162 | # object we've been passed; but really, all the logic is identical 163 | if isinstance(obj, pynng.Socket): 164 | self._nng_obj = obj.socket 165 | self._lib_arecv = lib.nng_recv_aio 166 | self._lib_asend = lib.nng_send_aio 167 | else: 168 | self._nng_obj = obj.context 169 | self._lib_arecv = lib.nng_ctx_recv 170 | self._lib_asend = lib.nng_ctx_send 171 | self.obj = obj 172 | if async_backend is None: 173 | async_backend = sniffio.current_async_library() 174 | if async_backend not in self._aio_helper_map: 175 | raise ValueError( 176 | "The async backend {} is not currently supported.".format(async_backend) 177 | ) 178 | self.awaitable, self.cb_arg = self._aio_helper_map[async_backend](self) 179 | aio_p = ffi.new("nng_aio **") 180 | _aio_map[id(self.cb_arg)] = self.cb_arg 181 | idarg = id(self.cb_arg) 182 | as_void = ffi.cast("void *", idarg) 183 | lib.nng_aio_alloc(aio_p, lib._async_complete, as_void) 184 | self.aio = aio_p[0] 185 | 186 | async def arecv(self): 187 | msg = await self.arecv_msg() 188 | return msg.bytes 189 | 190 | async def arecv_msg(self): 191 | check_err(self._lib_arecv(self._nng_obj, self.aio)) 192 | await self.awaitable 193 | check_err(lib.nng_aio_result(self.aio)) 194 | msg = lib.nng_aio_get_msg(self.aio) 195 | return pynng.Message(msg) 196 | 197 | async def asend(self, data): 198 | msg_p = ffi.new("nng_msg **") 199 | check_err(lib.nng_msg_alloc(msg_p, 0)) 200 | msg = msg_p[0] 201 | check_err(lib.nng_msg_append(msg, data, len(data))) 202 | check_err(lib.nng_aio_set_msg(self.aio, msg)) 203 | check_err(self._lib_asend(self._nng_obj, self.aio)) 204 | return await self.awaitable 205 | 206 | async def asend_msg(self, msg): 207 | """ 208 | Asynchronously send a Message 209 | 210 | """ 211 | lib.nng_aio_set_msg(self.aio, msg._nng_msg) 212 | check_err(self._lib_asend(self._nng_obj, self.aio)) 213 | msg._mem_freed = True 214 | return await self.awaitable 215 | 216 | def _free(self): 217 | """ 218 | Free resources allocated with nng 219 | """ 220 | # TODO: Do we need to check if self.awaitable is not finished? 221 | if self.aio is not None: 222 | lib.nng_aio_free(self.aio) 223 | self.aio = None 224 | 225 | def __enter__(self): 226 | return self 227 | 228 | def __exit__(self, *_exc_info): 229 | self._free() 230 | 231 | def __del__(self): 232 | self._free() 233 | -------------------------------------------------------------------------------- /docs/exceptions.rst: -------------------------------------------------------------------------------- 1 | Exceptions in pynng 2 | =================== 3 | 4 | pynng translates all of NNG error codes into Python Exceptions. The root 5 | exception of the hierarchy is the ``NNGException``; ``NNGException`` inherits 6 | from Exception, and all other exceptions defined in this library inherit from 7 | ``NNGException``. 8 | 9 | The following table describes all the exceptions defined by pynng. The first 10 | column is the name of the exception in pynng (defined in ``pynng.exceptions``), 11 | the second is the nng error code (defined in ``nng.h``), and the third is a 12 | description of the exception. 13 | 14 | +----------------------------+----------------------+--------------------------------------------------+ 15 | | pynng Exception | nng error code | Description | 16 | +============================+======================+==================================================+ 17 | | ``Interrupted`` | ``NNG_EINTR`` | The call was interrupted; if this happens, | 18 | | | | Python may throw a KeyboardInterrupt. (I'm not | 19 | | | | sure if this is an exception you can even get | 20 | | | | with these bindings) | 21 | +----------------------------+----------------------+--------------------------------------------------+ 22 | | ``NoMemory`` | ``NNG_ENOMEM`` | Not enough memory to complete the operation. | 23 | +----------------------------+----------------------+--------------------------------------------------+ 24 | | ``InvalidOperation`` | ``NNG_EINVAL`` | An invalid operation was requested on the | 25 | | | | resource. | 26 | +----------------------------+----------------------+--------------------------------------------------+ 27 | | ``Busy`` | ``NNG_EBUSY`` | | 28 | +----------------------------+----------------------+--------------------------------------------------+ 29 | | ``Timeout`` | ``NNG_ETIMEDOUT`` | The operation timed out. Some operations | 30 | | | | cannot time out; an example that cannot time | 31 | | | | out is a ``send()`` on a ``Pub0`` socket | 32 | +----------------------------+----------------------+--------------------------------------------------+ 33 | | ``ConnectionRefused`` | ``NNG_ECONNREFUSED`` | The remote socket refused a connection. | 34 | +----------------------------+----------------------+--------------------------------------------------+ 35 | | ``Closed`` | ``NNG_ECLOSED`` | The resource was already closed and cannot | 36 | | | | complete the requested operation. | 37 | +----------------------------+----------------------+--------------------------------------------------+ 38 | | ``TryAgain`` | ``NNG_EAGAIN`` | The requested operation would block, but | 39 | | | | non-blocking mode was requested. | 40 | +----------------------------+----------------------+--------------------------------------------------+ 41 | | ``NotSupported`` | ``NNG_ENOTSUP`` | The operation is not supported on the socket. | 42 | | | | For example, attempting to ``send`` on a | 43 | | | | ``Sub0`` socket will raise this. | 44 | +----------------------------+----------------------+--------------------------------------------------+ 45 | | ``AddressInUse`` | ``NNG_EADDRINUSE`` | The requested address is already in use and | 46 | | | | cannot be bound to. This happens if multiple | 47 | | | | sockets attempt to ``listen()`` at the same | 48 | | | | address. | 49 | +----------------------------+----------------------+--------------------------------------------------+ 50 | | ``BadState`` | ``NNG_ESTATE`` | An operation was attempted in a bad state; for | 51 | | | | example, attempting to ``recv()`` twice in a | 52 | | | | row of a single ``Req0`` socket. | 53 | +----------------------------+----------------------+--------------------------------------------------+ 54 | | ``NoEntry`` | ``NNG_ENOENT`` | The requested resource does not exist. | 55 | +----------------------------+----------------------+--------------------------------------------------+ 56 | | ``ProtocolError`` | ``NNG_EPROTO`` | | 57 | +----------------------------+----------------------+--------------------------------------------------+ 58 | | ``DestinationUnreachable`` | ``NNG_EUNREACHABLE`` | Could not reach the destination. | 59 | +----------------------------+----------------------+--------------------------------------------------+ 60 | | ``AddressInvalid`` | ``NNG_EADDRINVAL`` | An invalid address was specified. For example, | 61 | | | | attempting to listen on ``"tcp://127.0.0.1:-1"`` | 62 | | | | will throw. | 63 | +----------------------------+----------------------+--------------------------------------------------+ 64 | | ``PermissionDenied`` | ``NNG_EPERM`` | You did not have permission to do the requested | 65 | | | | operation. | 66 | +----------------------------+----------------------+--------------------------------------------------+ 67 | | ``MessageTooLarge`` | ``NNG_EMSGSiZE`` | | 68 | +----------------------------+----------------------+--------------------------------------------------+ 69 | | ``ConnectionReset`` | ``NNG_ECONNRESET`` | | 70 | +----------------------------+----------------------+--------------------------------------------------+ 71 | | ``ConnectionAborted`` | ``NNG_ECONNABORTED`` | | 72 | +----------------------------+----------------------+--------------------------------------------------+ 73 | | ``Canceled`` | ``NNG_ECANCELED`` | | 74 | +----------------------------+----------------------+--------------------------------------------------+ 75 | | ``OutOfFiles`` | ``NNG_ENOFILES`` | | 76 | +----------------------------+----------------------+--------------------------------------------------+ 77 | | ``OutOfSpace`` | ``NNG_ENOSPC`` | | 78 | +----------------------------+----------------------+--------------------------------------------------+ 79 | | ``AlreadyExists`` | ``NNG_EEXIST`` | | 80 | +----------------------------+----------------------+--------------------------------------------------+ 81 | | ``ReadOnly`` | ``NNG_EREADONLY`` | | 82 | +----------------------------+----------------------+--------------------------------------------------+ 83 | | ``WriteOnly`` | ``NNG_EWRITEONLY`` | | 84 | +----------------------------+----------------------+--------------------------------------------------+ 85 | | ``CryptoError`` | ``NNG_ECRYPTO`` | | 86 | +----------------------------+----------------------+--------------------------------------------------+ 87 | | ``AuthenticationError`` | ``NNG_EPEERAUTH`` | | 88 | +----------------------------+----------------------+--------------------------------------------------+ 89 | | ``NoArgument`` | ``NNG_ENOARG`` | | 90 | +----------------------------+----------------------+--------------------------------------------------+ 91 | | ``Ambiguous`` | ``NNG_EAMBIGUOUS`` | | 92 | +----------------------------+----------------------+--------------------------------------------------+ 93 | | ``BadType`` | ``NNG_EBADTYPE`` | | 94 | +----------------------------+----------------------+--------------------------------------------------+ 95 | | ``Internal`` | ``NNG_EINTERNAL`` | | 96 | +----------------------------+----------------------+--------------------------------------------------+ 97 | -------------------------------------------------------------------------------- /test/test_abstract.py: -------------------------------------------------------------------------------- 1 | import time 2 | import pytest 3 | import pynng 4 | import pynng.sockaddr 5 | import platform 6 | 7 | 8 | def test_abstract_addr_basic(): 9 | """Test basic AbstractAddr functionality""" 10 | # Create a mock nng_sockaddr_abstract structure 11 | # We can't easily create a real one without the actual nng library, 12 | # so we'll test the class methods indirectly 13 | 14 | # Test that NNG_AF_ABSTRACT is available 15 | assert hasattr(pynng.lib, "NNG_AF_ABSTRACT") 16 | assert pynng.lib.NNG_AF_ABSTRACT == 6 17 | 18 | 19 | def test_abstract_addr_in_type_to_str(): 20 | """Test that abstract socket family is in type_to_str mapping""" 21 | assert pynng.lib.NNG_AF_ABSTRACT in pynng.sockaddr.SockAddr.type_to_str 22 | assert pynng.sockaddr.SockAddr.type_to_str[pynng.lib.NNG_AF_ABSTRACT] == "abstract" 23 | 24 | 25 | def test_nng_sockaddr_includes_abstract(): 26 | """Test that _nng_sockaddr function includes AbstractAddr in lookup""" 27 | # Test that the lookup dictionary in the source code includes AbstractAddr 28 | # We'll check this by examining the source code directly 29 | import inspect 30 | from pynng.sockaddr import _nng_sockaddr 31 | 32 | # Get the source code of the function 33 | source = inspect.getsource(_nng_sockaddr) 34 | 35 | # Check that NNG_AF_ABSTRACT and AbstractAddr are in the source 36 | assert "NNG_AF_ABSTRACT" in source 37 | assert "AbstractAddr" in source 38 | 39 | 40 | @pytest.mark.skipif( 41 | platform.system() != "Linux", reason="Abstract sockets are Linux-specific" 42 | ) 43 | def test_abstract_socket_connection(): 44 | """Test actual abstract socket connection on Linux""" 45 | # Test with a simple abstract socket name 46 | abstract_addr = "abstract://test_socket" 47 | 48 | with pynng.Pair0(recv_timeout=1000) as sock1, pynng.Pair0( 49 | recv_timeout=1000 50 | ) as sock2: 51 | # Test listening on abstract socket 52 | listener = sock1.listen(abstract_addr) 53 | assert len(sock1.listeners) == 1 54 | 55 | # Test dialing abstract socket 56 | sock2.dial(abstract_addr) 57 | assert len(sock2.dialers) == 1 58 | 59 | # Test basic communication 60 | sock1.send(b"hello") 61 | received = sock2.recv() 62 | assert received == b"hello" 63 | 64 | # Test that local address is AbstractAddr 65 | local_addr = listener.local_address 66 | assert isinstance(local_addr, pynng.sockaddr.AbstractAddr) 67 | assert local_addr.family == pynng.lib.NNG_AF_ABSTRACT 68 | assert local_addr.family_as_str == "abstract" 69 | 70 | 71 | @pytest.mark.skipif( 72 | platform.system() != "Linux", reason="Abstract sockets are Linux-specific" 73 | ) 74 | def test_abstract_socket_with_special_chars(): 75 | """Test abstract socket with special characters including NUL bytes""" 76 | # Test with URI-encoded special characters 77 | abstract_addr = "abstract://test%00socket%20with%20spaces" 78 | 79 | with pynng.Pair0(recv_timeout=100) as sock1, pynng.Pair0(recv_timeout=100) as sock2: 80 | listener = sock1.listen(abstract_addr) 81 | sock2.dial(abstract_addr) 82 | 83 | # Test communication 84 | sock1.send(b"test message") 85 | received = sock2.recv() 86 | assert received == b"test message" 87 | 88 | # Test that the address is properly handled 89 | local_addr = listener.local_address 90 | assert isinstance(local_addr, pynng.sockaddr.AbstractAddr) 91 | 92 | # Test string representation 93 | addr_str = str(local_addr) 94 | assert addr_str.startswith("abstract://") 95 | 96 | 97 | @pytest.mark.skipif( 98 | platform.system() != "Linux", reason="Abstract sockets are Linux-specific" 99 | ) 100 | def test_abstract_socket_auto_bind(): 101 | """Test abstract socket auto-bind functionality with empty name""" 102 | # Test with empty abstract socket name for auto-bind 103 | abstract_addr = "abstract://" 104 | 105 | with pynng.Pair0(recv_timeout=100) as sock1, pynng.Pair0(recv_timeout=100) as sock2: 106 | try: 107 | listener = sock1.listen(abstract_addr) 108 | sock2.dial(abstract_addr) 109 | 110 | # Test communication 111 | sock1.send(b"auto-bind test") 112 | received = sock2.recv() 113 | assert received == b"auto-bind test" 114 | 115 | # Test that the address is properly handled 116 | local_addr = listener.local_address 117 | assert isinstance(local_addr, pynng.sockaddr.AbstractAddr) 118 | except pynng.exceptions.InvalidOperation: 119 | # Auto-bind with empty name might not be supported 120 | # This is acceptable behavior for some implementations 121 | pytest.skip("Auto-bind with empty name not supported") 122 | 123 | 124 | @pytest.mark.skipif( 125 | platform.system() != "Linux", reason="Abstract sockets are Linux-specific" 126 | ) 127 | def test_abstract_socket_with_different_protocols(): 128 | """Test abstract sockets with different protocol types""" 129 | protocols = [ 130 | (pynng.Pair0, pynng.Pair0, "pair"), 131 | (pynng.Pub0, pynng.Sub0, "pubsub"), 132 | (pynng.Push0, pynng.Pull0, "pushpull"), 133 | (pynng.Req0, pynng.Rep0, "reqrep"), 134 | ] 135 | 136 | for server_proto, client_proto, proto_name in protocols: 137 | # Use a unique address for each protocol to avoid "Address in use" errors 138 | abstract_addr = f"abstract://test_{proto_name}_protocol" 139 | 140 | # Retry logic to handle race conditions 141 | max_retries = 5 142 | for retry in range(max_retries): 143 | try: 144 | with server_proto(recv_timeout=100) as server, client_proto( 145 | recv_timeout=100 146 | ) as client: 147 | if server_proto == pynng.Pub0 and client_proto == pynng.Sub0: 148 | # Special handling for pub/sub 149 | server.listen(abstract_addr) 150 | client.dial(abstract_addr) 151 | client.subscribe("") # Subscribe to all messages 152 | # Add a small delay to ensure subscription is processed 153 | time.sleep(0.01) 154 | server.send(b"pubsub test") 155 | received = client.recv() 156 | assert received == b"pubsub test" 157 | elif server_proto == pynng.Push0 and client_proto == pynng.Pull0: 158 | # Special handling for push/pull 159 | server.listen(abstract_addr) 160 | client.dial(abstract_addr) 161 | # Add a small delay to ensure connection is established 162 | time.sleep(0.01) 163 | server.send(b"pushpull test") 164 | received = client.recv() 165 | assert received == b"pushpull test" 166 | elif server_proto == pynng.Req0 and client_proto == pynng.Rep0: 167 | # Special handling for req/rep 168 | client.listen(abstract_addr) 169 | server.dial(abstract_addr) 170 | # Add a small delay to ensure connection is established 171 | time.sleep(0.01) 172 | server.send(b"reqrep test") 173 | received = client.recv() 174 | assert received == b"reqrep test" 175 | client.send(b"reply") 176 | reply = server.recv() 177 | assert reply == b"reply" 178 | else: 179 | # Default handling for pair protocols 180 | server.listen(abstract_addr) 181 | client.dial(abstract_addr) 182 | # Add a small delay to ensure connection is established 183 | time.sleep(0.01) 184 | server.send(b"pair test") 185 | received = client.recv() 186 | assert received == b"pair test" 187 | 188 | # If we get here, the test passed for this protocol 189 | break 190 | 191 | except pynng.exceptions.Timeout: 192 | if retry == max_retries - 1: 193 | # This was the last retry, re-raise the exception 194 | raise 195 | # Log the retry attempt 196 | print( 197 | f"Retry {retry + 1}/{max_retries} for {proto_name} protocol due to timeout" 198 | ) 199 | # Add a small delay before retrying 200 | time.sleep(0.1) 201 | 202 | 203 | @pytest.mark.skipif( 204 | platform.system() == "Linux", reason="Test error handling on non-Linux systems" 205 | ) 206 | def test_abstract_socket_error_on_non_linux(): 207 | """Test that abstract sockets raise appropriate errors on non-Linux systems""" 208 | abstract_addr = "abstract://test_socket" 209 | 210 | with pytest.raises(pynng.exceptions.NNGException): 211 | with pynng.Pair0() as sock: 212 | sock.listen(abstract_addr) 213 | 214 | 215 | def test_abstract_addr_name_bytes(): 216 | """Test AbstractAddr name_bytes property""" 217 | 218 | # Create a mock abstract sockaddr 219 | class MockAbstractSockAddr: 220 | def __init__(self): 221 | self.s_family = pynng.lib.NNG_AF_ABSTRACT 222 | self.s_abstract = MockAbstract() 223 | 224 | class MockAbstract: 225 | def __init__(self): 226 | self.sa_len = 5 227 | self.sa_name = bytearray(107) 228 | # Set first 5 bytes 229 | self.sa_name[0:5] = [ord("t"), ord("e"), ord("s"), ord("t"), 0] 230 | 231 | # Create a mock ffi_sock_addr 232 | mock_sock_addr = [MockAbstractSockAddr()] 233 | 234 | # Create AbstractAddr instance 235 | abstract_addr = pynng.sockaddr.AbstractAddr(mock_sock_addr) 236 | 237 | # Test name_bytes property 238 | name_bytes = abstract_addr.name_bytes 239 | assert len(name_bytes) == 5 240 | assert name_bytes == b"test\x00" 241 | 242 | 243 | def test_abstract_addr_name_with_uri_encoding(): 244 | """Test AbstractAddr name property with URI encoding/decoding""" 245 | 246 | # Create a mock abstract sockaddr with URI-encoded characters 247 | class MockAbstractSockAddr: 248 | def __init__(self): 249 | self.s_family = pynng.lib.NNG_AF_ABSTRACT 250 | self.s_abstract = MockAbstract() 251 | 252 | class MockAbstract: 253 | def __init__(self): 254 | self.sa_len = 11 255 | self.sa_name = bytearray(107) 256 | # Set bytes that represent "test%00socket" when URI-decoded 257 | test_bytes = b"test\x00socket" 258 | self.sa_name[0:11] = test_bytes 259 | 260 | # Create a mock ffi_sock_addr 261 | mock_sock_addr = [MockAbstractSockAddr()] 262 | 263 | # Create AbstractAddr instance 264 | abstract_addr = pynng.sockaddr.AbstractAddr(mock_sock_addr) 265 | 266 | # Test name property with URI decoding 267 | name = abstract_addr.name 268 | # The name should handle the NUL byte appropriately 269 | assert "test" in name and "socket" in name 270 | 271 | 272 | def test_abstract_addr_str_representation(): 273 | """Test AbstractAddr string representation""" 274 | 275 | # Create a mock abstract sockaddr 276 | class MockAbstractSockAddr: 277 | def __init__(self): 278 | self.s_family = pynng.lib.NNG_AF_ABSTRACT 279 | self.s_abstract = MockAbstract() 280 | 281 | class MockAbstract: 282 | def __init__(self): 283 | self.sa_len = 9 284 | self.sa_name = bytearray(107) 285 | # Set bytes for "test_name" 286 | test_bytes = b"test_name" 287 | self.sa_name[0:9] = test_bytes 288 | 289 | # Create a mock ffi_sock_addr 290 | mock_sock_addr = [MockAbstractSockAddr()] 291 | 292 | # Create AbstractAddr instance 293 | abstract_addr = pynng.sockaddr.AbstractAddr(mock_sock_addr) 294 | 295 | # Test string representation 296 | addr_str = str(abstract_addr) 297 | assert addr_str.startswith("abstract://") 298 | assert "test_name" in addr_str 299 | -------------------------------------------------------------------------------- /nng_api.h: -------------------------------------------------------------------------------- 1 | // THIS FILE WAS AUTOMATICALLY GENERATED BY ./generate_api.sh 2 | typedef struct nng_ctx_s { 3 | uint32_t id; 4 | } nng_ctx; 5 | typedef struct nng_dialer_s { 6 | uint32_t id; 7 | } nng_dialer; 8 | typedef struct nng_listener_s { 9 | uint32_t id; 10 | } nng_listener; 11 | typedef struct nng_pipe_s { 12 | uint32_t id; 13 | } nng_pipe; 14 | typedef struct nng_socket_s { 15 | uint32_t id; 16 | } nng_socket; 17 | typedef int32_t nng_duration; 18 | typedef struct nng_msg nng_msg; 19 | typedef struct nng_stat nng_stat; 20 | typedef struct nng_aio nng_aio; 21 | struct nng_sockaddr_inproc { 22 | uint16_t sa_family; 23 | char sa_name[(128)]; 24 | }; 25 | struct nng_sockaddr_path { 26 | uint16_t sa_family; 27 | char sa_path[(128)]; 28 | }; 29 | struct nng_sockaddr_in6 { 30 | uint16_t sa_family; 31 | uint16_t sa_port; 32 | uint8_t sa_addr[16]; 33 | uint32_t sa_scope; 34 | }; 35 | struct nng_sockaddr_in { 36 | uint16_t sa_family; 37 | uint16_t sa_port; 38 | uint32_t sa_addr; 39 | }; 40 | struct nng_sockaddr_zt { 41 | uint16_t sa_family; 42 | uint64_t sa_nwid; 43 | uint64_t sa_nodeid; 44 | uint32_t sa_port; 45 | }; 46 | struct nng_sockaddr_abstract { 47 | uint16_t sa_family; 48 | uint16_t sa_len; 49 | uint8_t sa_name[107]; 50 | }; 51 | struct nng_sockaddr_storage { 52 | uint16_t sa_family; 53 | uint64_t sa_pad[16]; 54 | }; 55 | typedef struct nng_sockaddr_inproc nng_sockaddr_inproc; 56 | typedef struct nng_sockaddr_path nng_sockaddr_path; 57 | typedef struct nng_sockaddr_path nng_sockaddr_ipc; 58 | typedef struct nng_sockaddr_in nng_sockaddr_in; 59 | typedef struct nng_sockaddr_in6 nng_sockaddr_in6; 60 | typedef struct nng_sockaddr_zt nng_sockaddr_zt; 61 | typedef struct nng_sockaddr_abstract nng_sockaddr_abstract; 62 | typedef struct nng_sockaddr_storage nng_sockaddr_storage; 63 | typedef union nng_sockaddr { 64 | uint16_t s_family; 65 | nng_sockaddr_ipc s_ipc; 66 | nng_sockaddr_inproc s_inproc; 67 | nng_sockaddr_in6 s_in6; 68 | nng_sockaddr_in s_in; 69 | nng_sockaddr_zt s_zt; 70 | nng_sockaddr_abstract s_abstract; 71 | nng_sockaddr_storage s_storage; 72 | } nng_sockaddr; 73 | enum nng_sockaddr_family { 74 | NNG_AF_UNSPEC = 0, 75 | NNG_AF_INPROC = 1, 76 | NNG_AF_IPC = 2, 77 | NNG_AF_INET = 3, 78 | NNG_AF_INET6 = 4, 79 | NNG_AF_ZT = 5, 80 | NNG_AF_ABSTRACT = 6 81 | }; 82 | typedef struct nng_iov { 83 | void * iov_buf; 84 | size_t iov_len; 85 | } nng_iov; 86 | extern void nng_fini(void); 87 | extern int nng_close(nng_socket); 88 | extern int nng_socket_id(nng_socket); 89 | extern int nng_socket_set(nng_socket, const char *, const void *, size_t); 90 | extern int nng_socket_set_bool(nng_socket, const char *, bool); 91 | extern int nng_socket_set_int(nng_socket, const char *, int); 92 | extern int nng_socket_set_size(nng_socket, const char *, size_t); 93 | extern int nng_socket_set_uint64(nng_socket, const char *, uint64_t); 94 | extern int nng_socket_set_string(nng_socket, const char *, const char *); 95 | extern int nng_socket_set_ptr(nng_socket, const char *, void *); 96 | extern int nng_socket_set_ms(nng_socket, const char *, nng_duration); 97 | extern int nng_socket_set_addr( 98 | nng_socket, const char *, const nng_sockaddr *); 99 | extern int nng_socket_get(nng_socket, const char *, void *, size_t *); 100 | extern int nng_socket_get_bool(nng_socket, const char *, bool *); 101 | extern int nng_socket_get_int(nng_socket, const char *, int *); 102 | extern int nng_socket_get_size(nng_socket, const char *, size_t *); 103 | extern int nng_socket_get_uint64(nng_socket, const char *, uint64_t *); 104 | extern int nng_socket_get_string(nng_socket, const char *, char **); 105 | extern int nng_socket_get_ptr(nng_socket, const char *, void **); 106 | extern int nng_socket_get_ms(nng_socket, const char *, nng_duration *); 107 | extern int nng_socket_get_addr(nng_socket, const char *, nng_sockaddr *); 108 | typedef enum { 109 | NNG_PIPE_EV_ADD_PRE, 110 | NNG_PIPE_EV_ADD_POST, 111 | NNG_PIPE_EV_REM_POST, 112 | NNG_PIPE_EV_NUM, 113 | } nng_pipe_ev; 114 | typedef void (*nng_pipe_cb)(nng_pipe, nng_pipe_ev, void *); 115 | extern int nng_pipe_notify(nng_socket, nng_pipe_ev, nng_pipe_cb, void *); 116 | extern int nng_listen(nng_socket, const char *, nng_listener *, int); 117 | extern int nng_dial(nng_socket, const char *, nng_dialer *, int); 118 | extern int nng_dialer_create(nng_dialer *, nng_socket, const char *); 119 | extern int nng_listener_create(nng_listener *, nng_socket, const char *); 120 | extern int nng_dialer_start(nng_dialer, int); 121 | extern int nng_listener_start(nng_listener, int); 122 | extern int nng_dialer_close(nng_dialer); 123 | extern int nng_listener_close(nng_listener); 124 | extern int nng_dialer_id(nng_dialer); 125 | extern int nng_listener_id(nng_listener); 126 | extern int nng_dialer_set(nng_dialer, const char *, const void *, size_t); 127 | extern int nng_dialer_set_bool(nng_dialer, const char *, bool); 128 | extern int nng_dialer_set_int(nng_dialer, const char *, int); 129 | extern int nng_dialer_set_size(nng_dialer, const char *, size_t); 130 | extern int nng_dialer_set_uint64(nng_dialer, const char *, uint64_t); 131 | extern int nng_dialer_set_string(nng_dialer, const char *, const char *); 132 | extern int nng_dialer_set_ptr(nng_dialer, const char *, void *); 133 | extern int nng_dialer_set_ms(nng_dialer, const char *, nng_duration); 134 | extern int nng_dialer_set_addr( 135 | nng_dialer, const char *, const nng_sockaddr *); 136 | extern int nng_dialer_get(nng_dialer, const char *, void *, size_t *); 137 | extern int nng_dialer_get_bool(nng_dialer, const char *, bool *); 138 | extern int nng_dialer_get_int(nng_dialer, const char *, int *); 139 | extern int nng_dialer_get_size(nng_dialer, const char *, size_t *); 140 | extern int nng_dialer_get_uint64(nng_dialer, const char *, uint64_t *); 141 | extern int nng_dialer_get_string(nng_dialer, const char *, char **); 142 | extern int nng_dialer_get_ptr(nng_dialer, const char *, void **); 143 | extern int nng_dialer_get_ms(nng_dialer, const char *, nng_duration *); 144 | extern int nng_dialer_get_addr(nng_dialer, const char *, nng_sockaddr *); 145 | extern int nng_listener_set( 146 | nng_listener, const char *, const void *, size_t); 147 | extern int nng_listener_set_bool(nng_listener, const char *, bool); 148 | extern int nng_listener_set_int(nng_listener, const char *, int); 149 | extern int nng_listener_set_size(nng_listener, const char *, size_t); 150 | extern int nng_listener_set_uint64(nng_listener, const char *, uint64_t); 151 | extern int nng_listener_set_string(nng_listener, const char *, const char *); 152 | extern int nng_listener_set_ptr(nng_listener, const char *, void *); 153 | extern int nng_listener_set_ms(nng_listener, const char *, nng_duration); 154 | extern int nng_listener_set_addr( 155 | nng_listener, const char *, const nng_sockaddr *); 156 | extern int nng_listener_get(nng_listener, const char *, void *, size_t *); 157 | extern int nng_listener_get_bool(nng_listener, const char *, bool *); 158 | extern int nng_listener_get_int(nng_listener, const char *, int *); 159 | extern int nng_listener_get_size(nng_listener, const char *, size_t *); 160 | extern int nng_listener_get_uint64(nng_listener, const char *, uint64_t *); 161 | extern int nng_listener_get_string(nng_listener, const char *, char **); 162 | extern int nng_listener_get_ptr(nng_listener, const char *, void **); 163 | extern int nng_listener_get_ms(nng_listener, const char *, nng_duration *); 164 | extern int nng_listener_get_addr(nng_listener, const char *, nng_sockaddr *); 165 | extern const char *nng_strerror(int); 166 | extern int nng_send(nng_socket, void *, size_t, int); 167 | extern int nng_recv(nng_socket, void *, size_t *, int); 168 | extern int nng_sendmsg(nng_socket, nng_msg *, int); 169 | extern int nng_recvmsg(nng_socket, nng_msg **, int); 170 | extern void nng_send_aio(nng_socket, nng_aio *); 171 | extern void nng_recv_aio(nng_socket, nng_aio *); 172 | extern int nng_ctx_open(nng_ctx *, nng_socket); 173 | extern int nng_ctx_close(nng_ctx); 174 | extern int nng_ctx_id(nng_ctx); 175 | extern void nng_ctx_recv(nng_ctx, nng_aio *); 176 | extern int nng_ctx_recvmsg(nng_ctx, nng_msg **, int); 177 | extern void nng_ctx_send(nng_ctx, nng_aio *); 178 | extern int nng_ctx_sendmsg(nng_ctx, nng_msg *, int); 179 | extern int nng_ctx_get(nng_ctx, const char *, void *, size_t *); 180 | extern int nng_ctx_get_bool(nng_ctx, const char *, bool *); 181 | extern int nng_ctx_get_int(nng_ctx, const char *, int *); 182 | extern int nng_ctx_get_size(nng_ctx, const char *, size_t *); 183 | extern int nng_ctx_get_uint64(nng_ctx, const char *, uint64_t *); 184 | extern int nng_ctx_get_string(nng_ctx, const char *, char **); 185 | extern int nng_ctx_get_ptr(nng_ctx, const char *, void **); 186 | extern int nng_ctx_get_ms(nng_ctx, const char *, nng_duration *); 187 | extern int nng_ctx_get_addr(nng_ctx, const char *, nng_sockaddr *); 188 | extern int nng_ctx_set(nng_ctx, const char *, const void *, size_t); 189 | extern int nng_ctx_set_bool(nng_ctx, const char *, bool); 190 | extern int nng_ctx_set_int(nng_ctx, const char *, int); 191 | extern int nng_ctx_set_size(nng_ctx, const char *, size_t); 192 | extern int nng_ctx_set_uint64(nng_ctx, const char *, uint64_t); 193 | extern int nng_ctx_set_string(nng_ctx, const char *, const char *); 194 | extern int nng_ctx_set_ptr(nng_ctx, const char *, void *); 195 | extern int nng_ctx_set_ms(nng_ctx, const char *, nng_duration); 196 | extern int nng_ctx_set_addr(nng_ctx, const char *, const nng_sockaddr *); 197 | extern void *nng_alloc(size_t); 198 | extern void nng_free(void *, size_t); 199 | extern char *nng_strdup(const char *); 200 | extern void nng_strfree(char *); 201 | extern int nng_aio_alloc(nng_aio **, void (*)(void *), void *); 202 | extern void nng_aio_free(nng_aio *); 203 | extern void nng_aio_reap(nng_aio *); 204 | extern void nng_aio_stop(nng_aio *); 205 | extern int nng_aio_result(nng_aio *); 206 | extern size_t nng_aio_count(nng_aio *); 207 | extern void nng_aio_cancel(nng_aio *); 208 | extern void nng_aio_abort(nng_aio *, int); 209 | extern void nng_aio_wait(nng_aio *); 210 | extern bool nng_aio_busy(nng_aio *); 211 | extern void nng_aio_set_msg(nng_aio *, nng_msg *); 212 | extern nng_msg *nng_aio_get_msg(nng_aio *); 213 | extern int nng_aio_set_input(nng_aio *, unsigned, void *); 214 | extern void *nng_aio_get_input(nng_aio *, unsigned); 215 | extern int nng_aio_set_output(nng_aio *, unsigned, void *); 216 | extern void *nng_aio_get_output(nng_aio *, unsigned); 217 | extern void nng_aio_set_timeout(nng_aio *, nng_duration); 218 | extern int nng_aio_set_iov(nng_aio *, unsigned, const nng_iov *); 219 | extern bool nng_aio_begin(nng_aio *); 220 | extern void nng_aio_finish(nng_aio *, int); 221 | typedef void (*nng_aio_cancelfn)(nng_aio *, void *, int); 222 | extern void nng_aio_defer(nng_aio *, nng_aio_cancelfn, void *); 223 | extern void nng_sleep_aio(nng_duration, nng_aio *); 224 | extern int nng_msg_alloc(nng_msg **, size_t); 225 | extern void nng_msg_free(nng_msg *); 226 | extern int nng_msg_realloc(nng_msg *, size_t); 227 | extern int nng_msg_reserve(nng_msg *, size_t); 228 | extern size_t nng_msg_capacity(nng_msg *); 229 | extern void * nng_msg_header(nng_msg *); 230 | extern size_t nng_msg_header_len(const nng_msg *); 231 | extern void * nng_msg_body(nng_msg *); 232 | extern size_t nng_msg_len(const nng_msg *); 233 | extern int nng_msg_append(nng_msg *, const void *, size_t); 234 | extern int nng_msg_insert(nng_msg *, const void *, size_t); 235 | extern int nng_msg_trim(nng_msg *, size_t); 236 | extern int nng_msg_chop(nng_msg *, size_t); 237 | extern int nng_msg_header_append(nng_msg *, const void *, size_t); 238 | extern int nng_msg_header_insert(nng_msg *, const void *, size_t); 239 | extern int nng_msg_header_trim(nng_msg *, size_t); 240 | extern int nng_msg_header_chop(nng_msg *, size_t); 241 | extern int nng_msg_header_append_u16(nng_msg *, uint16_t); 242 | extern int nng_msg_header_append_u32(nng_msg *, uint32_t); 243 | extern int nng_msg_header_append_u64(nng_msg *, uint64_t); 244 | extern int nng_msg_header_insert_u16(nng_msg *, uint16_t); 245 | extern int nng_msg_header_insert_u32(nng_msg *, uint32_t); 246 | extern int nng_msg_header_insert_u64(nng_msg *, uint64_t); 247 | extern int nng_msg_header_chop_u16(nng_msg *, uint16_t *); 248 | extern int nng_msg_header_chop_u32(nng_msg *, uint32_t *); 249 | extern int nng_msg_header_chop_u64(nng_msg *, uint64_t *); 250 | extern int nng_msg_header_trim_u16(nng_msg *, uint16_t *); 251 | extern int nng_msg_header_trim_u32(nng_msg *, uint32_t *); 252 | extern int nng_msg_header_trim_u64(nng_msg *, uint64_t *); 253 | extern int nng_msg_append_u16(nng_msg *, uint16_t); 254 | extern int nng_msg_append_u32(nng_msg *, uint32_t); 255 | extern int nng_msg_append_u64(nng_msg *, uint64_t); 256 | extern int nng_msg_insert_u16(nng_msg *, uint16_t); 257 | extern int nng_msg_insert_u32(nng_msg *, uint32_t); 258 | extern int nng_msg_insert_u64(nng_msg *, uint64_t); 259 | extern int nng_msg_chop_u16(nng_msg *, uint16_t *); 260 | extern int nng_msg_chop_u32(nng_msg *, uint32_t *); 261 | extern int nng_msg_chop_u64(nng_msg *, uint64_t *); 262 | extern int nng_msg_trim_u16(nng_msg *, uint16_t *); 263 | extern int nng_msg_trim_u32(nng_msg *, uint32_t *); 264 | extern int nng_msg_trim_u64(nng_msg *, uint64_t *); 265 | extern int nng_msg_dup(nng_msg **, const nng_msg *); 266 | extern void nng_msg_clear(nng_msg *); 267 | extern void nng_msg_header_clear(nng_msg *); 268 | extern void nng_msg_set_pipe(nng_msg *, nng_pipe); 269 | extern nng_pipe nng_msg_get_pipe(const nng_msg *); 270 | extern int nng_pipe_get(nng_pipe, const char *, void *, size_t *); 271 | extern int nng_pipe_get_bool(nng_pipe, const char *, bool *); 272 | extern int nng_pipe_get_int(nng_pipe, const char *, int *); 273 | extern int nng_pipe_get_ms(nng_pipe, const char *, nng_duration *); 274 | extern int nng_pipe_get_size(nng_pipe, const char *, size_t *); 275 | extern int nng_pipe_get_uint64(nng_pipe, const char *, uint64_t *); 276 | extern int nng_pipe_get_string(nng_pipe, const char *, char **); 277 | extern int nng_pipe_get_ptr(nng_pipe, const char *, void **); 278 | extern int nng_pipe_get_addr(nng_pipe, const char *, nng_sockaddr *); 279 | extern int nng_pipe_close(nng_pipe); 280 | extern int nng_pipe_id(nng_pipe); 281 | extern nng_socket nng_pipe_socket(nng_pipe); 282 | extern nng_dialer nng_pipe_dialer(nng_pipe); 283 | extern nng_listener nng_pipe_listener(nng_pipe); 284 | extern int nng_stats_get(nng_stat **); 285 | extern void nng_stats_free(nng_stat *); 286 | extern void nng_stats_dump(nng_stat *); 287 | extern nng_stat *nng_stat_next(nng_stat *); 288 | extern nng_stat *nng_stat_child(nng_stat *); 289 | extern const char *nng_stat_name(nng_stat *); 290 | extern int nng_stat_type(nng_stat *); 291 | extern nng_stat *nng_stat_find(nng_stat *, const char *); 292 | extern nng_stat *nng_stat_find_socket(nng_stat *, nng_socket); 293 | extern nng_stat *nng_stat_find_dialer(nng_stat *, nng_dialer); 294 | extern nng_stat *nng_stat_find_listener(nng_stat *, nng_listener); 295 | enum nng_stat_type_enum { 296 | NNG_STAT_SCOPE = 0, 297 | NNG_STAT_LEVEL = 1, 298 | NNG_STAT_COUNTER = 2, 299 | NNG_STAT_STRING = 3, 300 | NNG_STAT_BOOLEAN = 4, 301 | NNG_STAT_ID = 5, 302 | }; 303 | extern int nng_stat_unit(nng_stat *); 304 | enum nng_unit_enum { 305 | NNG_UNIT_NONE = 0, 306 | NNG_UNIT_BYTES = 1, 307 | NNG_UNIT_MESSAGES = 2, 308 | NNG_UNIT_MILLIS = 3, 309 | NNG_UNIT_EVENTS = 4 310 | }; 311 | extern uint64_t nng_stat_value(nng_stat *); 312 | extern bool nng_stat_bool(nng_stat *); 313 | extern const char *nng_stat_string(nng_stat *); 314 | extern const char *nng_stat_desc(nng_stat *); 315 | extern uint64_t nng_stat_timestamp(nng_stat *); 316 | extern int nng_device(nng_socket, nng_socket); 317 | extern void nng_device_aio(nng_aio *, nng_socket, nng_socket); 318 | enum nng_errno_enum { 319 | NNG_EINTR = 1, 320 | NNG_ENOMEM = 2, 321 | NNG_EINVAL = 3, 322 | NNG_EBUSY = 4, 323 | NNG_ETIMEDOUT = 5, 324 | NNG_ECONNREFUSED = 6, 325 | NNG_ECLOSED = 7, 326 | NNG_EAGAIN = 8, 327 | NNG_ENOTSUP = 9, 328 | NNG_EADDRINUSE = 10, 329 | NNG_ESTATE = 11, 330 | NNG_ENOENT = 12, 331 | NNG_EPROTO = 13, 332 | NNG_EUNREACHABLE = 14, 333 | NNG_EADDRINVAL = 15, 334 | NNG_EPERM = 16, 335 | NNG_EMSGSIZE = 17, 336 | NNG_ECONNABORTED = 18, 337 | NNG_ECONNRESET = 19, 338 | NNG_ECANCELED = 20, 339 | NNG_ENOFILES = 21, 340 | NNG_ENOSPC = 22, 341 | NNG_EEXIST = 23, 342 | NNG_EREADONLY = 24, 343 | NNG_EWRITEONLY = 25, 344 | NNG_ECRYPTO = 26, 345 | NNG_EPEERAUTH = 27, 346 | NNG_ENOARG = 28, 347 | NNG_EAMBIGUOUS = 29, 348 | NNG_EBADTYPE = 30, 349 | NNG_ECONNSHUT = 31, 350 | NNG_EINTERNAL = 1000, 351 | NNG_ESYSERR = 0x10000000, 352 | NNG_ETRANERR = 0x20000000 353 | }; 354 | typedef struct nng_url { 355 | char *u_rawurl; 356 | char *u_scheme; 357 | char *u_userinfo; 358 | char *u_host; 359 | char *u_hostname; 360 | char *u_port; 361 | char *u_path; 362 | char *u_query; 363 | char *u_fragment; 364 | char *u_requri; 365 | } nng_url; 366 | extern int nng_url_parse(nng_url **, const char *); 367 | extern void nng_url_free(nng_url *); 368 | extern int nng_url_clone(nng_url **, const nng_url *); 369 | extern const char *nng_version(void); 370 | typedef struct nng_stream nng_stream; 371 | typedef struct nng_stream_dialer nng_stream_dialer; 372 | typedef struct nng_stream_listener nng_stream_listener; 373 | extern void nng_stream_free(nng_stream *); 374 | extern void nng_stream_close(nng_stream *); 375 | extern void nng_stream_send(nng_stream *, nng_aio *); 376 | extern void nng_stream_recv(nng_stream *, nng_aio *); 377 | extern int nng_stream_get(nng_stream *, const char *, void *, size_t *); 378 | extern int nng_stream_get_bool(nng_stream *, const char *, bool *); 379 | extern int nng_stream_get_int(nng_stream *, const char *, int *); 380 | extern int nng_stream_get_ms(nng_stream *, const char *, nng_duration *); 381 | extern int nng_stream_get_size(nng_stream *, const char *, size_t *); 382 | extern int nng_stream_get_uint64(nng_stream *, const char *, uint64_t *); 383 | extern int nng_stream_get_string(nng_stream *, const char *, char **); 384 | extern int nng_stream_get_ptr(nng_stream *, const char *, void **); 385 | extern int nng_stream_get_addr(nng_stream *, const char *, nng_sockaddr *); 386 | extern int nng_stream_set(nng_stream *, const char *, const void *, size_t); 387 | extern int nng_stream_set_bool(nng_stream *, const char *, bool); 388 | extern int nng_stream_set_int(nng_stream *, const char *, int); 389 | extern int nng_stream_set_ms(nng_stream *, const char *, nng_duration); 390 | extern int nng_stream_set_size(nng_stream *, const char *, size_t); 391 | extern int nng_stream_set_uint64(nng_stream *, const char *, uint64_t); 392 | extern int nng_stream_set_string(nng_stream *, const char *, const char *); 393 | extern int nng_stream_set_ptr(nng_stream *, const char *, void *); 394 | extern int nng_stream_set_addr( 395 | nng_stream *, const char *, const nng_sockaddr *); 396 | extern int nng_stream_dialer_alloc(nng_stream_dialer **, const char *); 397 | extern int nng_stream_dialer_alloc_url( 398 | nng_stream_dialer **, const nng_url *); 399 | extern void nng_stream_dialer_free(nng_stream_dialer *); 400 | extern void nng_stream_dialer_close(nng_stream_dialer *); 401 | extern void nng_stream_dialer_dial(nng_stream_dialer *, nng_aio *); 402 | extern int nng_stream_dialer_set( 403 | nng_stream_dialer *, const char *, const void *, size_t); 404 | extern int nng_stream_dialer_get( 405 | nng_stream_dialer *, const char *, void *, size_t *); 406 | extern int nng_stream_dialer_get_bool( 407 | nng_stream_dialer *, const char *, bool *); 408 | extern int nng_stream_dialer_get_int( 409 | nng_stream_dialer *, const char *, int *); 410 | extern int nng_stream_dialer_get_ms( 411 | nng_stream_dialer *, const char *, nng_duration *); 412 | extern int nng_stream_dialer_get_size( 413 | nng_stream_dialer *, const char *, size_t *); 414 | extern int nng_stream_dialer_get_uint64( 415 | nng_stream_dialer *, const char *, uint64_t *); 416 | extern int nng_stream_dialer_get_string( 417 | nng_stream_dialer *, const char *, char **); 418 | extern int nng_stream_dialer_get_ptr( 419 | nng_stream_dialer *, const char *, void **); 420 | extern int nng_stream_dialer_get_addr( 421 | nng_stream_dialer *, const char *, nng_sockaddr *); 422 | extern int nng_stream_dialer_set_bool( 423 | nng_stream_dialer *, const char *, bool); 424 | extern int nng_stream_dialer_set_int(nng_stream_dialer *, const char *, int); 425 | extern int nng_stream_dialer_set_ms( 426 | nng_stream_dialer *, const char *, nng_duration); 427 | extern int nng_stream_dialer_set_size( 428 | nng_stream_dialer *, const char *, size_t); 429 | extern int nng_stream_dialer_set_uint64( 430 | nng_stream_dialer *, const char *, uint64_t); 431 | extern int nng_stream_dialer_set_string( 432 | nng_stream_dialer *, const char *, const char *); 433 | extern int nng_stream_dialer_set_ptr( 434 | nng_stream_dialer *, const char *, void *); 435 | extern int nng_stream_dialer_set_addr( 436 | nng_stream_dialer *, const char *, const nng_sockaddr *); 437 | extern int nng_stream_listener_alloc(nng_stream_listener **, const char *); 438 | extern int nng_stream_listener_alloc_url( 439 | nng_stream_listener **, const nng_url *); 440 | extern void nng_stream_listener_free(nng_stream_listener *); 441 | extern void nng_stream_listener_close(nng_stream_listener *); 442 | extern int nng_stream_listener_listen(nng_stream_listener *); 443 | extern void nng_stream_listener_accept(nng_stream_listener *, nng_aio *); 444 | extern int nng_stream_listener_set( 445 | nng_stream_listener *, const char *, const void *, size_t); 446 | extern int nng_stream_listener_get( 447 | nng_stream_listener *, const char *, void *, size_t *); 448 | extern int nng_stream_listener_get_bool( 449 | nng_stream_listener *, const char *, bool *); 450 | extern int nng_stream_listener_get_int( 451 | nng_stream_listener *, const char *, int *); 452 | extern int nng_stream_listener_get_ms( 453 | nng_stream_listener *, const char *, nng_duration *); 454 | extern int nng_stream_listener_get_size( 455 | nng_stream_listener *, const char *, size_t *); 456 | extern int nng_stream_listener_get_uint64( 457 | nng_stream_listener *, const char *, uint64_t *); 458 | extern int nng_stream_listener_get_string( 459 | nng_stream_listener *, const char *, char **); 460 | extern int nng_stream_listener_get_ptr( 461 | nng_stream_listener *, const char *, void **); 462 | extern int nng_stream_listener_get_addr( 463 | nng_stream_listener *, const char *, nng_sockaddr *); 464 | extern int nng_stream_listener_set_bool( 465 | nng_stream_listener *, const char *, bool); 466 | extern int nng_stream_listener_set_int( 467 | nng_stream_listener *, const char *, int); 468 | extern int nng_stream_listener_set_ms( 469 | nng_stream_listener *, const char *, nng_duration); 470 | extern int nng_stream_listener_set_size( 471 | nng_stream_listener *, const char *, size_t); 472 | extern int nng_stream_listener_set_uint64( 473 | nng_stream_listener *, const char *, uint64_t); 474 | extern int nng_stream_listener_set_string( 475 | nng_stream_listener *, const char *, const char *); 476 | extern int nng_stream_listener_set_ptr( 477 | nng_stream_listener *, const char *, void *); 478 | extern int nng_stream_listener_set_addr( 479 | nng_stream_listener *, const char *, const nng_sockaddr *); 480 | int nng_bus0_open(nng_socket *); 481 | int nng_bus0_open_raw(nng_socket *); 482 | int nng_pair0_open(nng_socket *); 483 | int nng_pair0_open_raw(nng_socket *); 484 | int nng_pair1_open(nng_socket *); 485 | int nng_pair1_open_raw(nng_socket *); 486 | int nng_pair1_open_poly(nng_socket *); 487 | int nng_push0_open(nng_socket *); 488 | int nng_push0_open_raw(nng_socket *); 489 | int nng_pull0_open(nng_socket *); 490 | int nng_pull0_open_raw(nng_socket *); 491 | int nng_pub0_open(nng_socket *); 492 | int nng_pub0_open_raw(nng_socket *); 493 | int nng_sub0_open(nng_socket *); 494 | int nng_sub0_open_raw(nng_socket *); 495 | int nng_req0_open(nng_socket *); 496 | int nng_req0_open_raw(nng_socket *); 497 | int nng_rep0_open(nng_socket *); 498 | int nng_rep0_open_raw(nng_socket *); 499 | int nng_surveyor0_open(nng_socket *); 500 | int nng_surveyor0_open_raw(nng_socket *); 501 | int nng_respondent0_open(nng_socket *); 502 | int nng_respondent0_open_raw(nng_socket *); 503 | typedef struct nng_tls_config nng_tls_config; 504 | typedef enum nng_tls_mode { 505 | NNG_TLS_MODE_CLIENT = 0, 506 | NNG_TLS_MODE_SERVER = 1, 507 | } nng_tls_mode; 508 | typedef enum nng_tls_auth_mode { 509 | NNG_TLS_AUTH_MODE_NONE = 0, 510 | NNG_TLS_AUTH_MODE_OPTIONAL = 1, 511 | NNG_TLS_AUTH_MODE_REQUIRED = 2, 512 | } nng_tls_auth_mode; 513 | typedef enum nng_tls_version { 514 | NNG_TLS_1_0 = 0x301, 515 | NNG_TLS_1_1 = 0x302, 516 | NNG_TLS_1_2 = 0x303, 517 | NNG_TLS_1_3 = 0x304 518 | } nng_tls_version; 519 | int nng_tls_config_alloc(nng_tls_config **, nng_tls_mode); 520 | void nng_tls_config_hold(nng_tls_config *); 521 | void nng_tls_config_free(nng_tls_config *); 522 | int nng_tls_config_server_name(nng_tls_config *, const char *); 523 | int nng_tls_config_ca_chain( 524 | nng_tls_config *, const char *, const char *); 525 | int nng_tls_config_own_cert( 526 | nng_tls_config *, const char *, const char *, const char *); 527 | int nng_tls_config_auth_mode(nng_tls_config *, nng_tls_auth_mode); 528 | int nng_tls_config_ca_file(nng_tls_config *, const char *); 529 | int nng_tls_config_cert_key_file( 530 | nng_tls_config *, const char *, const char *); 531 | int nng_tls_config_version( 532 | nng_tls_config *, nng_tls_version, nng_tls_version); 533 | const char *nng_tls_engine_name(void); 534 | const char *nng_tls_engine_description(void); 535 | bool nng_tls_engine_fips_mode(void); 536 | int nng_tls_register(void); 537 | #define NNG_FLAG_ALLOC 1u // Recv to allocate receive buffer 538 | #define NNG_FLAG_NONBLOCK 2u // Non-blocking operations 539 | #define NNG_MAJOR_VERSION 1 540 | #define NNG_MINOR_VERSION 6 541 | #define NNG_PATCH_VERSION 0 542 | --------------------------------------------------------------------------------