aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.gitignore15
-rw-r--r--Makefile144
-rw-r--r--src/bus.py169
-rw-r--r--src/native_bus.pyx221
4 files changed, 544 insertions, 5 deletions
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 <maandree@member.fsf.org>
+
+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 <maandree@member.fsf.org>
+
+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 <long>malloc(n)
+
+
+def bus_deallocate(address : int):
+ '''
+ Deallocate memory for a bus
+
+ @param address The address of the allocated memory
+ '''
+ free(<void *><long>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, <char **>NULL)
+ return file if r == 0 else None
+ r = bus_create(<char *>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(<long>bus, cfile, <int>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(<long>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(<long>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(<long>bus, <int (*)(const char *, void *)><void *>callback, <void *>user_data)
+