From 082c0c1c7b0e728b5d7122b90aec0230fad9ebd9 Mon Sep 17 00:00:00 2001 From: Mattias Andrée Date: Wed, 22 Apr 2015 01:44:46 +0200 Subject: ... MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Mattias Andrée --- .gitignore | 15 ++-- Makefile | 144 ++++++++++++++++++++++++++++++++++ src/bus.py | 169 ++++++++++++++++++++++++++++++++++++++++ src/native_bus.pyx | 221 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 544 insertions(+), 5 deletions(-) create mode 100644 Makefile create mode 100644 src/bus.py create mode 100644 src/native_bus.pyx diff --git a/.gitignore b/.gitignore index 8925c19..6cef52e 100644 --- a/.gitignore +++ b/.gitignore @@ -6,9 +6,14 @@ __pycache__/ .* !.git* *~ -.bak -.swo -.swp -.pyc -.pyo +*.bak +*.swo +*.swp +*.pyc +*.pyo +*.o +*.out +*.so +*.gch +*.c diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..4d78c66 --- /dev/null +++ b/Makefile @@ -0,0 +1,144 @@ +# The package path prefix, if you want to install to another root, set DESTDIR to that root +PREFIX = /usr +# The library path excluding prefix +LIB = /lib +# The resource path excluding prefix +DATA = /share +# The library path including prefix +LIBDIR = ${PREFIX}${LIB} +# The resource path including prefix +DATADIR = ${PREFIX}${DATA} +# The generic documentation path including prefix +DOCDIR = ${DATADIR}/doc +# The license base path including prefix +LICENSEDIR = ${DATADIR}/licenses + + +# The major version number of the current Python installation +PY_MAJOR = $(shell python -V | cut -d ' ' -f 2 | cut -d . -f 1) +# The minor version number of the current Python installation +PY_MINOR = $(shell python -V | cut -d ' ' -f 2 | cut -d . -f 2) +# The version number of the current Python installation without a dot +PY_VER = ${PY_MAJOR}${PY_MINOR} +# The version number of the current Python installation with a dot +PY_VERSION = ${PY_MAJOR}.${PY_MINOR} + + +# The directory for python modules +PYTHONDIR = ${LIBDIR}/python${PY_VERSION} + + +# The name of the package as it should be installed +PKGNAME = python-bus + + +# The installed pkg-config command +PKGCONFIG ?= pkg-config +# The installed cython command +CYTHON ?= cython +# The installed python command +PYTHON = python${PY_MAJOR} + + +# Libraries to link with using pkg-config +LIBS = python${PY_MAJOR} + + +# The C standard for C code compilation +STD = c99 +# Optimisation settings for C code compilation +OPTIMISE = -O2 + + +# Flags to use when compiling +CC_FLAGS = $$(${PKGCONFIG} --cflags ${LIBS}) -std=${STD} ${OPTIMISE} -fPIC ${CFLAGS} ${CPPFLAGS} + +# Flags to use when linking +LD_FLAGS = $$(${PKGCONFIG} --libs ${LIBS}) -lbus -std=${STD} ${OPTIMISE} -shared ${LDFLAGS} + + +# The suffixless basename of the .py-files +PYTHON_SRC = bus + +# The suffixless basename of the .py-files +CYTHON_SRC = native_bus + + + +all: pyc-files pyo-files so-files + +pyc-files: $(foreach M,${PYTHON_SRC},src/__pycache__/${M}.cpython-${PY_VER}.pyc) +pyo-files: $(foreach M,${PYTHON_SRC},src/__pycache__/${M}.cpython-${PY_VER}.pyo) +so-files: $(foreach M,${CYTHON_SRC},bin/${M}.so) + +bin/%.so: obj/%.o + @mkdir -p bin + ${CC} ${LD_FLAGS} -o $@ $^ + +obj/%.o: obj/%.c + ${CC} ${CC_FLAGS} -iquote"src" -c -o $@ $< + +obj/%.c: obj/%.pyx + if ! ${CYTHON} -3 -v $< ; then rm $@ ; false ; fi + +obj/%.pyx: src/%.pyx + @mkdir -p obj + cp $< $@ + +src/__pycache__/%.cpython-$(PY_VER).pyc: src/%.py + ${PYTHON} -m compileall $< + +src/__pycache__/%.cpython-$(PY_VER).pyo: src/%.py + ${PYTHON} -OO -m compileall $< + + + +install: install-base + +install-all: install-base +install-base: install-lib install-copyright +install-lib: install-source install-compiled install-optimised install-native + +install-source: $(foreach M,$(PYTHON_SRC),src/$(M).py) + install -dm755 -- "$(DESTDIR)$(PYTHONDIR)" + install -m644 $^ -- "$(DESTDIR)$(PYTHONDIR)" + +install-compiled: $(foreach M,$(PYTHON_SRC),src/__pycache__/$(M).cpython-$(PY_VER).pyc) + install -dm755 -- "$(DESTDIR)$(PYTHONDIR)/__pycache__" + install -m644 $^ -- "$(DESTDIR)$(PYTHONDIR)/__pycache__" + +install-optimised: $(foreach M,$(PYTHON_SRC),src/__pycache__/$(M).cpython-$(PY_VER).pyo) + install -dm755 -- "$(DESTDIR)$(PYTHONDIR)/__pycache__" + install -m644 $^ -- "$(DESTDIR)$(PYTHONDIR)/__pycache__" + +install-native: $(foreach M,$(CYTHON_SRC),bin/$(M).so) + install -dm755 -- "$(DESTDIR)$(PYTHONDIR)" + install -m755 $^ -- "$(DESTDIR)$(PYTHONDIR)" + +install-copyright: install-license + +install-license: LICENSE + install -dm755 -- "$(DESTDIR)$(LICENSEDIR)/$(PKGNAME)" + install -m644 $^ -- "$(DESTDIR)$(LICENSEDIR)/$(PKGNAME)" + + + +uninstall: + -rm -- "${DESTDIR}${LICENSEDIR}/${PKGNAME}/LICENSE" + -rmdir -- "${DESTDIR}${LICENSEDIR}/${PKGNAME}" + -rm -- $(foreach M,${PYTHON_SRC},"${DESTDIR}${PYTHONDIR}/__pycache__/${M}.cpython-${PY_VER}.pyo") + -rm -- $(foreach M,${PYTHON_SRC},"${DESTDIR}${PYTHONDIR}/__pycache__/${M}.cpython-${PY_VER}.pyc") + -rm -- $(foreach M,${PYTHON_SRC},"${DESTDIR}${PYTHONDIR}/${M}.py") + -rm -- $(foreach M,${CYTHON_SRC},"${DESTDIR}${PYTHONDIR}/${M}.so") + + + +clean: + -rm -r obj bin src/__pycache__ + + + +.PHONY: all pyc-files pyo-files so-files install install-all install-base \ + install-lib install-source install-compiled install-optimised \ + install-native install-copyright install-license uninstall clean + diff --git a/src/bus.py b/src/bus.py new file mode 100644 index 0000000..267a435 --- /dev/null +++ b/src/bus.py @@ -0,0 +1,169 @@ +# -*- python -*- +''' +MIT/X Consortium License + +Copyright © 2015 Mattias Andrée + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. +''' + + +class Bus: + ''' + Message broadcasting interprocess communication + ''' + + + RDONLY = 1 + ''' + Open the bus for reading only + ''' + + WRONLY = 0 + ''' + Open the bus for writing only + ''' + + RDWR = 0 + ''' + Open the bus for both reading and writing only + ''' + + EXCL = 2 + ''' + Fail to create bus if its file already exists + ''' + + INTR = 4 + ''' + Fail if interrupted + ''' + + + def __init__(self, pathname : str = None): + ''' + Constructor + + @param pathname:str The pathname of the bus, `None` if `create` should select a random pathname + ''' + self.pathname = pathname + self.bus = None + + + def create(self, flags : int = 0) -> str: + ''' + Create the bus + + @param flags:or_flag `Bus.EXCL` (if the pathname is not `None`) to fail if the file + already exists, otherwise if the file exists, nothing will happen; + `Bus.INTR` to fail if interrupted + @return :str The pathname of the bus + ''' + from native_bus import bus_create_wrapped + self.pathname = bus_create_wrapped(self.pathname, flags) + if self.pathname is None: + raise self.__oserror() + return self.pathname + + + def unlink(self): + ''' + Remove the bus + ''' + from native_bus import bus_unlink_wrapped + if bus_unlink_wrapped(self.pathname) == -1: + raise self.__oserror() + + + def open(self, flags : int = 0): + ''' + Open an existing bus + + @param flags:int `Bus.RDONLY`, `Bus.WRONLY` or `Bus.RDWR`, the value must not be negative + ''' + from native_bus import bus_close_wrapped, bus_allocate, bus_open_wrapped + if self.bus is not None: + if bus_close_wrapped(self.bus) == -1: + raise self.__oserror() + else: + self.bus = bus_allocate() + if self.bus == 0: + raise self.__oserror() + if bus_open_wrapped(self.bus, self.pathname, flags) == -1: + raise self.__oserror() + + + def close(self): + ''' + Close the bus + ''' + from native_bus import bus_close_wrapped, bus_deallocate + if self.bus is not None: + if bus_close_wrapped(self.bus) == -1: + raise self.__oserror() + bus_deallocate(self.bus) + self.bus = None + + + def write(self, message : str): + ''' + Broadcast a message a bus + + @param message:str The message to write, may not be longer than 2047 bytes after UTF-8 encoding + ''' + from native_bus import bus_write + if bus_write(self.bus, message) == -1: + raise self.__oserror() + + + def read(self, callback : callable, user_data = None): + ''' + Listen (in a loop, forever) for new message on a bus + + @param bus Bus information + @param callback Function to call when a message is received, the + input parameters will be the read message and + `user_data` from the function's [Bus.read] parameter + with the same name. The message must have been parsed + or copied when `callback` returns as it may be over + overridden after that time. `callback` should + return either of the the values: + 0: stop listening + 1: continue listening + -1: an error has occurred + @param user_data See description of `callback` + ''' + from native_bus import bus_read + if bus_read(self.bus, callback, user_data) == -1: + raise self.__oserror() + + + def __oserror(self): + ''' + Create an OSError + + @return :OSError The OS error + ''' + import os, ctypes + err = ctypes.get_errno() + err = OSError(err, os.strerror(err)) + if err.errno == os.errno.ENOENT: + err.filename = self.pathname + return err + diff --git a/src/native_bus.pyx b/src/native_bus.pyx new file mode 100644 index 0000000..016fa3c --- /dev/null +++ b/src/native_bus.pyx @@ -0,0 +1,221 @@ +# -*- python -*- +''' +MIT/X Consortium License + +Copyright © 2015 Mattias Andrée + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. +''' + +cimport cython + +from libc.stdlib cimport malloc, free + + +cdef extern int bus_create(const char *, int, char **) +''' +Create a new bus + +@param file The pathname of the bus, `NULL` to create a random one +@param flags `BUS_EXCL` (if `file` is not `NULL`) to fail if the file + already exists, otherwise if the file exists, nothing + will happen; + `BUS_INTR` to fail if interrupted +@param out_file Output parameter for the pathname of the bus +@return 0 on success, -1 on error +''' + +cdef extern int bus_unlink(const char *) +''' +Remove a bus + +@param file The pathname of the bus +@return 0 on success, -1 on error +''' + +cdef extern int bus_open(long, const char *, int) +''' +Open an existing bus + +@param bus Bus information to fill +@param file The filename of the bus +@param flags `BUS_RDONLY`, `BUS_WRONLY` or `BUS_RDWR`, + the value must not be negative +@return 0 on success, -1 on error +''' + +cdef extern int bus_close(long) +''' +Close a bus + +@param bus Bus information +@return 0 on success, -1 on error +''' + +cdef extern int bus_write(long, const char *) +''' +Broadcast a message a bus + +@param bus Bus information +@param message The message to write, may not be longer than + `BUS_MEMORY_SIZE` including the NUL-termination +@return 0 on success, -1 on error +''' + +cdef extern int bus_read(long, int (*)(const char *, void *), void *) +''' +Listen (in a loop, forever) for new message on a bus + +@param bus Bus information +@param callback Function to call when a message is received, the + input parameters will be the read message and + `user_data` from `bus_read`'s parameter with the + same name. The message must have been parsed or + copied when `callback` returns as it may be over + overridden after that time. `callback` should + return either of the the values: + 0: stop listening + 1: continue listening + -1: an error has occurred +@return 0 on success, -1 on error +''' + + +def bus_allocate() -> int: + ''' + Allocate memory for a bus + + @return The address of the allocated memory + ''' + n = 2 * sizeof(long long) + sizeof(int) + sizeof(char *) + return malloc(n) + + +def bus_deallocate(address : int): + ''' + Deallocate memory for a bus + + @param address The address of the allocated memory + ''' + free(address) + + +def bus_create_wrapped(file : str, flags : int) -> str: + ''' + Create a new bus + + @param file The pathname of the bus, `None` to create a random one + @param flags `BUS_EXCL` (if `file` is not `None`) to fail if the file + already exists, otherwise if the file exists, nothing + will happen; + `BUS_INTR` to fail if interrupted + @return The pathname of the bus, `None` on error; + `file` is returned unless `file` is `None` + ''' + cdef const char* cfile + cdef char* ofile + cdef bytes bs + if file is not None: + bs = file.encode('utf-8') + bytes([0]) + cfile = bs + r = bus_create(cfile, flags, NULL) + return file if r == 0 else None + r = bus_create(NULL, flags, &ofile) + if r == 0: + bs = ofile + return bs.encode('utf-8', 'strict') + return None + + +def bus_unlink_wrapped(file : str) -> int: + ''' + Remove a bus + + @param file The pathname of the bus + @return 0 on success, -1 on error + ''' + cdef const char* cfile + cdef bytes bs + bs = file.encode('utf-8') + bytes([0]) + cfile = bs + return bus_unlink(cfile) + + +def bus_open_wrapped(bus : int, file : str, flags : int) -> int: + ''' + Open an existing bus + + @param bus Bus information to fill + @param file The filename of the bus + @param flags `BUS_RDONLY`, `BUS_WRONLY` or `BUS_RDWR`, + the value must not be negative + @return 0 on success, -1 on error + ''' + cdef const char* cfile + cdef bytes bs + bs = file.encode('utf-8') + bytes([0]) + cfile = bs + return bus_open(bus, cfile, flags) + + +def bus_close_wrapped(bus : int) -> int: + ''' + Close a bus + + @param bus Bus information + @return 0 on success, -1 on error + ''' + return bus_close(bus) + + +def bus_write_wrapped(bus : int, message : str) -> int: + ''' + Broadcast a message a bus + + @param bus Bus information + @param message The message to write, may not be longer than + `BUS_MEMORY_SIZE` including the NUL-termination + @return 0 on success, -1 on error + ''' + cdef const char* cmessage + cdef bytes bs + bs = message.encode('utf-8') + bytes([0]) + cmessage = bs + return bus_write(bus, cmessage) + + +def bus_read_wrapped(bus : int, callback : callable, user_data) -> int: + ''' + Listen (in a loop, forever) for new message on a bus + + @param bus Bus information + @param callback Function to call when a message is received, the + input parameters will be the read message and + `user_data` from `bus_read`'s parameter with the + same name. The message must have been parsed or + copied when `callback` returns as it may be over + overridden after that time. `callback` should + return either of the the values: + 0: stop listening + 1: continue listening + -1: an error has occurred + @return 0 on success, -1 on error + ''' + return bus_read(bus, callback, user_data) + -- cgit v1.2.3-70-g09d2