Commit 3121b290 authored by Kirill Smelkov's avatar Kirill Smelkov

golang: Teach pychan to work with channels of C types, not only PyObjects

Introduce notion of data type (dtype similarly to NumPy) into pychan and
teach it to accept for send objects only matching that dtype. Likewise
teach pychan to decode raw bytes received from underlying channel into
Python object correspodningg to pychan dtype. For C dtypes, e.g.
'C.int', 'C.double' etc, contrary to chan of python objects, the
transfer can be done without depending on Python GIL. This way channels
of such C-level dtypes can be used to implement interaction in between
Python and nogil worlds.
parent 2c8063f4
...@@ -99,6 +99,14 @@ channels. For example:: ...@@ -99,6 +99,14 @@ channels. For example::
# default case # default case
... ...
By default `chan` creates new channel that can carry arbitrary Python objects.
However type of channel elements can be specified via `chan(dtype=X)` - for
example `chan(dtype='C.int')` creates new channel whose elements are C
integers. `chan.nil(X)` creates typed nil channel. `Cython/nogil API`_
explains how channels with non-Python dtypes, besides in-Python usage, can be
additionally used for interaction in between Python and nogil worlds.
Methods Methods
------- -------
...@@ -238,6 +246,13 @@ can be used to multiplex on several channels. For example:: ...@@ -238,6 +246,13 @@ can be used to multiplex on several channels. For example::
# default case # default case
... ...
Channels created from Python are represented by `pychan` cdef class. Python
channels that carry non-Python elements (`pychan.dtype != DTYPE_PYOBJECT`) can
be converted to Cython/nogil `chan[T]` via `pychan.chan_*()`. For example
`pychan.chan_int()` converts Python channel created via `pychan(dtype='C.int')`
into `chan[int]`. This provides interaction mechanism in between *nogil* and
Python worlds.
`panic` stops normal execution of current goroutine by throwing a C-level `panic` stops normal execution of current goroutine by throwing a C-level
exception. On Python/C boundaries C-level exceptions have to be converted to exception. On Python/C boundaries C-level exceptions have to be converted to
Python-level exceptions with `topyexc`. For example:: Python-level exceptions with `topyexc`. For example::
......
...@@ -110,6 +110,7 @@ cdef extern from "golang/libgolang.h" namespace "golang" nogil: ...@@ -110,6 +110,7 @@ cdef extern from "golang/libgolang.h" namespace "golang" nogil:
_chan *ch _chan *ch
_chanop op _chanop op
unsigned flags unsigned flags
unsigned user
void *ptxrx void *ptxrx
uint64_t itxrx uint64_t itxrx
cbool *rxok cbool *rxok
...@@ -127,9 +128,40 @@ cdef extern from "golang/libgolang.h" namespace "golang" nogil: ...@@ -127,9 +128,40 @@ cdef extern from "golang/libgolang.h" namespace "golang" nogil:
cdef void topyexc() except * cdef void topyexc() except *
cpdef pypanic(arg) cpdef pypanic(arg)
# pychan is chan<object> # pychan is python wrapper over chan<object> or chan<structZ|bool|int|double|...>
from cython cimport final from cython cimport final
# DType describes type of channel elements.
# TODO consider supporting NumPy dtypes too.
cdef enum DType:
DTYPE_PYOBJECT = 0 # chan[object]
DTYPE_STRUCTZ = 1 # chan[structZ]
DTYPE_BOOL = 2 # chan[bool]
DTYPE_INT = 3 # chan[int]
DTYPE_DOUBLE = 4 # chan[double]
DTYPE_NTYPES = 5
# pychan wraps a channel into python object.
#
# Type of channel can be either channel of python objects, or channel of
# C-level objects. If channel elements are C-level objects, the channel - even
# via pychan wrapper - can be used to interact with nogil world.
#
# There can be multiple pychan(s) wrapping a particular raw channel.
@final @final
cdef class pychan: cdef class pychan:
cdef _chan *_ch cdef _chan *_ch
cdef DType dtype # type of channel elements
# pychan.nil(X) creates new nil pychan with element type X.
@staticmethod # XXX needs to be `cpdef nil()` but cython:
cdef pychan _nil(object dtype) # "static cpdef methods not yet supported"
# chan_X returns ._ch wrapped into typesafe pyx/nogil-level chan[X].
# chan_X panics if channel type != X.
# X can be any C-level type, but not PyObject.
cdef nogil:
chan[structZ] chan_structZ (pychan pych)
chan[cbool] chan_bool (pychan pych)
chan[int] chan_int (pychan pych)
chan[double] chan_double (pychan pych)
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# cython: language_level=2 # cython: language_level=2
# cython: binding=False # cython: binding=False
# cython: c_string_type=str, c_string_encoding=utf8
# distutils: language = c++ # distutils: language = c++
# distutils: depends = libgolang.h # distutils: depends = libgolang.h
# #
...@@ -142,25 +143,44 @@ cdef void __goviac(void *arg) nogil: ...@@ -142,25 +143,44 @@ cdef void __goviac(void *arg) nogil:
# ---- channels ---- # ---- channels ----
# pychan is chan<object>.
@final @final
cdef class pychan: cdef class pychan:
def __cinit__(pychan pych, size=0): def __cinit__(pychan pych, size=0, dtype=object):
pych._ch = _makechan_pyexc(sizeof(PyObject*), size) pych.dtype = parse_dtype(dtype)
pych._ch = _makechan_pyexc(dtypeRegistry[<int>pych.dtype].size, size)
# pychan.nil(X) creates new nil pychan with specified dtype.
# TODO try to avoid exposing .nil on pychan instances, and expose only pychan.nil
# http://code.activestate.com/recipes/578486-class-only-methods/
@staticmethod
def nil(dtype):
return pychan._nil(dtype)
@staticmethod
cdef pychan _nil(object dtype):
return pynil(parse_dtype(dtype))
def __dealloc__(pychan pych): def __dealloc__(pychan pych):
# on del: drain buffered channel to decref sent objects.
# verify that the channel is not connected anywhere outside us.
# (if it was present also somewhere else - draining would be incorrect)
if pych._ch == NULL: if pych._ch == NULL:
return return
# pychan[X!=object]: just decref the raw chan and we are done.
if pych.dtype != DTYPE_PYOBJECT:
_chanxdecref(pych._ch)
pych._ch = NULL
return
# pychan[object], for now, owns the underlying channel.
# drain buffered channel to decref sent objects.
# verify that the channel is not connected anywhere outside us.
# (if it was present also somewhere else - draining would be incorrect)
#
# TODO: in the future there could be multiple pychan[object] wrapping # TODO: in the future there could be multiple pychan[object] wrapping
# the same underlying raw channel: e.g. ch=chan(); ch2=ch.txonly() # the same underlying raw channel: e.g. ch=chan(); ch2=ch.txonly()
# -> drain underlying channel only when its last reference goes to 0. # -> drain underlying channel only when its last reference goes to 0.
cdef int refcnt = _chanrefcnt(pych._ch) cdef int refcnt = _chanrefcnt(pych._ch)
if refcnt != 1: if refcnt != 1:
# cannot raise py-level exception in __dealloc__ # cannot raise py-level exception in __dealloc__
Py_FatalError("pychan.__dealloc__: chan.refcnt=%d ; must be =1" % refcnt) Py_FatalError("pychan[object].__dealloc__: chan.refcnt=%d ; must be =1" % refcnt)
cdef chan[pPyObject] ch = _wrapchan[pPyObject](pych._ch) cdef chan[pPyObject] ch = _wrapchan[pPyObject](pych._ch)
_chanxdecref(pych._ch) _chanxdecref(pych._ch)
...@@ -182,17 +202,23 @@ cdef class pychan: ...@@ -182,17 +202,23 @@ cdef class pychan:
# send sends object to a receiver. # send sends object to a receiver.
def send(pychan pych, obj): def send(pychan pych, obj):
cdef PyObject *_tx = <PyObject*>obj cdef chanElemBuf _tx = 0
# increment obj reference count - until received the channel is holding pointer to the object. if pych.dtype == DTYPE_PYOBJECT:
Py_INCREF(obj) # increment obj reference count - until received the channel is
# holding pointer to the object.
Py_INCREF(obj)
(<PyObject **>&_tx)[0] = <PyObject *>obj
else:
py_to_c(pych.dtype, obj, &_tx)
try: try:
with nogil: with nogil:
_chansend_pyexc(pych._ch, &_tx) _chansend_pyexc(pych._ch, &_tx)
except: # not only _PanicError as send can also throw e.g. bad_alloc except: # not only _PanicError as send can also throw e.g. bad_alloc
# the object was not sent - e.g. it was "send on a closed channel" # the object was not sent - e.g. it was "send on a closed channel"
Py_DECREF(obj) if pych.dtype == DTYPE_PYOBJECT:
Py_DECREF(obj)
raise raise
# recv_ is "comma-ok" version of recv. # recv_ is "comma-ok" version of recv.
...@@ -200,18 +226,23 @@ cdef class pychan: ...@@ -200,18 +226,23 @@ cdef class pychan:
# ok is true - if receive was delivered by a successful send. # ok is true - if receive was delivered by a successful send.
# ok is false - if receive is due to channel being closed and empty. # ok is false - if receive is due to channel being closed and empty.
def recv_(pychan pych): # -> (rx, ok) def recv_(pychan pych): # -> (rx, ok)
cdef PyObject *_rx = NULL cdef chanElemBuf _rx = 0
cdef bint ok cdef bint ok
with nogil: with nogil:
ok = _chanrecv__pyexc(pych._ch, &_rx) ok = _chanrecv__pyexc(pych._ch, &_rx)
if not ok: cdef object rx = None
return (None, ok) cdef PyObject *_rxpy
if pych.dtype == DTYPE_PYOBJECT:
_rxpy = (<PyObject **>&_rx)[0]
if _rxpy != NULL:
# we received the object and the channel dropped pointer to it.
rx = <object>_rxpy
Py_DECREF(rx)
else:
rx = c_to_py(pych.dtype, &_rx)
# we received the object and the channel dropped pointer to it.
rx = <object>_rx
Py_DECREF(rx)
return (rx, ok) return (rx, ok)
# recv receives from the channel. # recv receives from the channel.
...@@ -229,7 +260,10 @@ cdef class pychan: ...@@ -229,7 +260,10 @@ cdef class pychan:
def __repr__(pychan pych): def __repr__(pychan pych):
if pych._ch == NULL: if pych._ch == NULL:
return "nilchan" if pych.dtype == DTYPE_PYOBJECT:
return "nilchan"
else:
return "chan.nil(%r)" % dtypeinfo(pych.dtype).name
else: else:
return super(pychan, pych).__repr__() return super(pychan, pych).__repr__()
...@@ -242,16 +276,41 @@ cdef class pychan: ...@@ -242,16 +276,41 @@ cdef class pychan:
cdef pychan b = rhs cdef pychan b = rhs
if a._ch != b._ch: if a._ch != b._ch:
return False return False
return True # a and b point to the same underlying channel object.
# they compare as same if their types match, or, for nil, additionally,
# if one of the sides is nil[*] (untyped nil).
if a.dtype == b.dtype:
return True
if a._ch != NULL:
return False
if a.dtype == DTYPE_PYOBJECT or b.dtype == DTYPE_PYOBJECT:
return True
return False
# pynilchan is the nil py channel. # pychan -> chan[X]
# # ( should live in "runtime support for channel types" if we could define
# On nil channel: send/recv block forever; close panics. # methods separate from class )
cdef pychan _pynilchan = pychan() cdef nogil:
_chanxdecref(_pynilchan._ch) chan[structZ] chan_structZ (pychan pych):
_pynilchan._ch = NULL pychan_asserttype(pych, DTYPE_STRUCTZ)
pynilchan = _pynilchan return _wrapchan[structZ](pych._ch)
chan[cbool] chan_bool (pychan pych):
pychan_asserttype(pych, DTYPE_BOOL)
return _wrapchan[cbool] (pych._ch)
chan[int] chan_int (pychan pych):
pychan_asserttype(pych, DTYPE_INT)
return _wrapchan[int] (pych._ch)
chan[double] chan_double (pychan pych):
pychan_asserttype(pych, DTYPE_DOUBLE)
return _wrapchan[double] (pych._ch)
cdef void pychan_asserttype(pychan pych, DType dtype) nogil:
if pych.dtype != dtype:
panic("pychan: channel type mismatch")
# pydefault represents default case for pyselect. # pydefault represents default case for pyselect.
...@@ -288,7 +347,7 @@ def pyselect(*pycasev): ...@@ -288,7 +347,7 @@ def pyselect(*pycasev):
cdef int i, n = len(pycasev), selected cdef int i, n = len(pycasev), selected
cdef vector[_selcase] casev = vector[_selcase](n, default) cdef vector[_selcase] casev = vector[_selcase](n, default)
cdef pychan pych cdef pychan pych
cdef PyObject *_rx = NULL # all select recvs are setup to receive into _rx cdef chanElemBuf _rx = 0 # all select recvs are setup to receive into _rx
cdef cbool rxok = False # (its ok as only one receive will be actually executed) cdef cbool rxok = False # (its ok as only one receive will be actually executed)
selected = -1 selected = -1
...@@ -315,14 +374,20 @@ def pyselect(*pycasev): ...@@ -315,14 +374,20 @@ def pyselect(*pycasev):
pypanic("pyselect: send expected: %r" % (pysend,)) pypanic("pyselect: send expected: %r" % (pysend,))
tx = <object>(_tcase.ob_item[1]) tx = <object>(_tcase.ob_item[1])
# incref tx as if corresponding channel is holding pointer to the object while it is being sent.
# we'll decref the object if it won't be sent.
# see pychan.send for details.
Py_INCREF(tx)
casev[i] = _selsend(pych._ch, NULL) casev[i] = _selsend(pych._ch, NULL)
casev[i].flags = _INPLACE_DATA casev[i].flags = _INPLACE_DATA
(<PyObject **>&casev[i].itxrx)[0] = <PyObject *>tx casev[i].user = pych.dtype
if pych.dtype == DTYPE_PYOBJECT:
# incref tx as if corresponding channel is holding pointer to the object while it is being sent.
# we'll decref the object if it won't be sent.
# see pychan.send for details.
Py_INCREF(tx)
(<PyObject **>&casev[i].itxrx)[0] = <PyObject *>tx
else:
# NOTE vvv assumes that size for all dtypes fit into 64 bits
py_to_c(pych.dtype, tx, &casev[i].itxrx) # NOTE can raise exception
# recv # recv
else: else:
...@@ -338,13 +403,15 @@ def pyselect(*pycasev): ...@@ -338,13 +403,15 @@ def pyselect(*pycasev):
else: else:
pypanic("pyselect: recv expected: %r" % (pyrecv,)) pypanic("pyselect: recv expected: %r" % (pyrecv,))
casev[i].user = pych.dtype
with nogil: with nogil:
selected = _chanselect_pyexc(&casev[0], casev.size()) selected = _chanselect_pyexc(&casev[0], casev.size())
finally: finally:
# decref not sent tx (see ^^^ send prepare) # decref not sent tx (see ^^^ send prepare)
for i in range(n): for i in range(n):
if casev[i].op == _CHANSEND and (i != selected): if casev[i].op == _CHANSEND and casev[i].user == DTYPE_PYOBJECT and (i != selected):
_tx = (<PyObject **>casev[i].ptx())[0] _tx = (<PyObject **>casev[i].ptx())[0]
tx = <object>_tx tx = <object>_tx
Py_DECREF(tx) Py_DECREF(tx)
...@@ -358,12 +425,20 @@ def pyselect(*pycasev): ...@@ -358,12 +425,20 @@ def pyselect(*pycasev):
if op != _CHANRECV: if op != _CHANRECV:
raise AssertionError("pyselect: chanselect returned with bad op") raise AssertionError("pyselect: chanselect returned with bad op")
# we received NULL or the object; if it is object, corresponding channel
# dropped pointer to it (see pychan.recv_ for details).
cdef object rx = None cdef object rx = None
if _rx != NULL: cdef PyObject *_rxpy
rx = <object>_rx cdef DType rxtype = <DType>casev[selected].user
Py_DECREF(rx) if rxtype == DTYPE_PYOBJECT:
# we received NULL or the object; if it is object, corresponding channel
# dropped pointer to it (see pychan.recv_ for details).
_rxpy = (<PyObject **>&_rx)[0]
if _rxpy != NULL:
rx = <object>_rxpy
Py_DECREF(rx)
else:
rx = c_to_py(rxtype, &_rx)
if casev[selected].rxok != NULL: if casev[selected].rxok != NULL:
return selected, (rx, rxok) return selected, (rx, rxok)
...@@ -444,3 +519,194 @@ cdef nogil: ...@@ -444,3 +519,194 @@ cdef nogil:
void _taskgo_pyexc(void (*f)(void *) nogil, void *arg) except +topyexc: void _taskgo_pyexc(void (*f)(void *) nogil, void *arg) except +topyexc:
_taskgo(f, arg) _taskgo(f, arg)
# ---- runtime support for channel types ----
# chanElemBuf is large enough to keep any dtype.
# NOTE sizeof(chanElemBuf) = max(_.size) from dtypeRegistry (this is checked at
# runtime on module init).
ctypedef uint64_t chanElemBuf
# DTypeInfo provides runtime information for a DType:
# dtype name, element size, py <-> c element conversion routines and typed nil instance.
cdef struct DTypeInfo:
const char *name # name of the type, e.g. "C.int"
unsigned size
# py_to_c converts python object to C-level data of dtype.
# If conversion fails, corresponding exception is raised.
bint (*py_to_c)(object obj, chanElemBuf *cto) except False
# c_to_py converts C-level data into python object according to dtype.
# The conversion cannot fail, but this can raise exception e.g. due to
# error when allocating result object.
object (*c_to_py) (const chanElemBuf *cfrom)
# pynil points to pychan instance that represents nil[dtype].
# it holds one reference and is never freed.
PyObject *pynil
# py_to_c converts Python object to C-level data according to dtype.
cdef bint py_to_c(DType dtype, object obj, chanElemBuf *cto) except False:
dtypei = dtypeinfo(dtype)
return dtypei.py_to_c(obj, cto)
# c_to_py converts C-level data into Python object according to dtype.
cdef object c_to_py(DType dtype, const chanElemBuf *cfrom):
dtypei = dtypeinfo(dtype)
return dtypei.c_to_py(cfrom)
# mkpynil creates pychan instance that represents nil[dtype].
cdef PyObject *mkpynil(DType dtype):
cdef pychan pynil = pychan.__new__(pychan)
pynil.dtype = dtype
pynil._ch = NULL # should be already NULL
Py_INCREF(pynil)
return <PyObject *>pynil
# pynil returns pychan instance corresponding to nil[dtype].
cdef pychan pynil(DType dtype):
dtypei = dtypeinfo(dtype)
return <pychan>dtypei.pynil
# {} dtype -> DTypeInfo.
# XXX const
cdef DTypeInfo[<int>DTYPE_NTYPES] dtypeRegistry
# dtypeinfo returns DTypeInfo corresponding to dtype.
cdef DTypeInfo* dtypeinfo(DType dtype) nogil:
if not (0 <= dtype < DTYPE_NTYPES):
# no need to ->pyexc, as this bug means memory corruption and so is fatal
panic("BUG: pychan dtype invalid")
return &dtypeRegistry[<int>dtype]
# DTYPE_PYOBJECT
dtypeRegistry[<int>DTYPE_PYOBJECT] = DTypeInfo(
name = "object",
size = sizeof(PyObject*),
# py_to_c/c_to_py must not be called for pyobj - as pyobj is the common
# case, they are manually inlined for speed. The code is also more clear
# when Py_INCREF/Py_DECREF go in send/recv/select directly.
py_to_c = NULL, # must not be called for pyobj
c_to_py = NULL, # must not be called for pyobj
pynil = mkpynil(DTYPE_PYOBJECT),
)
# pynilchan is nil py channel.
#
# On nil channel: send/recv block forever; close panics.
# pynilchan is alias for pychan.nil(object).
pynilchan = pychan._nil(object)
# DTYPE_STRUCTZ
dtypeRegistry[<int>DTYPE_STRUCTZ] = DTypeInfo(
name = "C.structZ",
size = 0, # NOTE = _elemsize<structZ>, but _not_ sizeof(structZ) which = 1
py_to_c = structZ_py_to_c,
c_to_py = structZ_c_to_py,
pynil = mkpynil(DTYPE_STRUCTZ),
)
cdef bint structZ_py_to_c(object obj, chanElemBuf *cto) except False:
# for structZ the only accepted value from python is None
if obj is not None:
raise TypeError("type mismatch: expect structZ; got %r" % (obj,))
# nothing to do - size = 0
return True
cdef object structZ_c_to_py(const chanElemBuf *cfrom):
return None
# DTYPE_BOOL
dtypeRegistry[<int>DTYPE_BOOL] = DTypeInfo(
name = "C.bool",
size = sizeof(cbool),
py_to_c = bool_py_to_c,
c_to_py = bool_c_to_py,
pynil = mkpynil(DTYPE_BOOL),
)
cdef bint bool_py_to_c(object obj, chanElemBuf *cto) except False:
# don't accept int/double/str/whatever.
if type(obj) is not bool:
raise TypeError("type mismatch: expect bool; got %r" % (obj,))
(<cbool *>cto)[0] = obj # raises *Error if conversion fails
return True
cdef object bool_c_to_py(const chanElemBuf *cfrom):
return (<cbool *>cfrom)[0]
# DTYPE_INT
dtypeRegistry[<int>DTYPE_INT] = DTypeInfo(
name = "C.int",
size = sizeof(int),
py_to_c = int_py_to_c,
c_to_py = int_c_to_py,
pynil = mkpynil(DTYPE_INT),
)
cdef bint int_py_to_c(object obj, chanElemBuf *cto) except False:
# don't accept bool
if isinstance(obj, bool):
raise TypeError("type mismatch: expect int; got %r" % (obj,))
# don't allow e.g. 3.14 to be implicitly truncated to just 3
cdef double objf = obj
if (<int>objf) != objf:
raise TypeError("type mismatch: expect int; got %r" % (obj,))
(<int *>cto)[0] = obj # raises *Error if conversion fails
return True
cdef object int_c_to_py(const chanElemBuf *cfrom):
return (<int *>cfrom)[0]
# DTYPE_DOUBLE
dtypeRegistry[<int>DTYPE_DOUBLE] = DTypeInfo(
name = "C.double",
size = sizeof(double),
py_to_c = double_py_to_c,
c_to_py = double_c_to_py,
pynil = mkpynil(DTYPE_DOUBLE),
)
cdef bint double_py_to_c(object obj, chanElemBuf *cto) except False:
# don't accept bool
if isinstance(obj, bool):
raise TypeError("type mismatch: expect float; got %r" % (obj,))
(<double *>cto)[0] = obj # raises *Error if conversion fails
return True
cdef object double_c_to_py(const chanElemBuf *cfrom):
return (<double *>cfrom)[0]
# verify at init time that sizeof(chanElemBuf) = max(_.size)
cdef verify_chanElemBuf():
cdef int size_max = 0
for dtype in range(DTYPE_NTYPES):
size_max = max(size_max, dtypeRegistry[<int>dtype].size)
if size_max != sizeof(chanElemBuf):
raise AssertionError("golang: module is miscompiled: max(dtype.size) = %d ; compiled with = %d" % (size_max, sizeof(chanElemBuf)))
verify_chanElemBuf()
# {} dtype name (str) -> dtype
cdef dict name2dtype = {}
cdef init_name2dtype():
for dtype in range(DTYPE_NTYPES):
name2dtype[dtypeRegistry[<int>dtype].name] = dtype
init_name2dtype()
# parse_dtype converts object or string dtype, as e.g. passed to
# pychan(dtype=...) into DType.
cdef DType parse_dtype(dtype) except <DType>-1:
if dtype is object:
return DTYPE_PYOBJECT
_ = name2dtype.get(dtype)
if _ is None:
raise TypeError("pychan: invalid dtype: %r" % (dtype,))
return _
...@@ -210,3 +210,72 @@ def test_select_win_while_queue(): ...@@ -210,3 +210,72 @@ def test_select_win_while_queue():
def test_select_inplace(): def test_select_inplace():
with nogil: with nogil:
_test_select_inplace() _test_select_inplace()
# helpers for pychan(dtype=X) py <-> c tests.
def pychan_structZ_recv(pychan pych):
with nogil: _pychan_structZ_recv(pych)
return None
def pychan_structZ_send(pychan pych, obj):
if obj is not None:
raise TypeError("cannot convert %r to structZ" % (obj,))
cdef structZ _
with nogil:
_pychan_structZ_send(pych, _)
def pychan_structZ_close(pychan pych):
with nogil: _pychan_structZ_close(pych)
def pychan_bool_recv(pychan pych):
with nogil: _ = _pychan_bool_recv(pych)
return _
def pychan_bool_send(pychan pych, cbool obj):
with nogil: _pychan_bool_send(pych, obj)
def pychan_bool_close(pychan pych):
with nogil: _pychan_bool_close(pych)
def pychan_int_recv(pychan pych):
with nogil: _ = _pychan_int_recv(pych)
return _
def pychan_int_send(pychan pych, int obj):
with nogil: _pychan_int_send(pych, obj)
def pychan_int_close(pychan pych):
with nogil: _pychan_int_close(pych)
def pychan_double_recv(pychan pych):
with nogil: _ = _pychan_double_recv(pych)
return _
def pychan_double_send(pychan pych, double obj):
with nogil: _pychan_double_send(pych, obj)
def pychan_double_close(pychan pych):
with nogil: _pychan_double_close(pych)
cdef nogil:
structZ _pychan_structZ_recv(pychan pych) except +topyexc:
return pych.chan_structZ().recv()
void _pychan_structZ_send(pychan pych, structZ obj) except +topyexc:
pych.chan_structZ().send(obj)
void _pychan_structZ_close(pychan pych) except +topyexc:
pych.chan_structZ().close()
cbool _pychan_bool_recv(pychan pych) except +topyexc:
return pych.chan_bool().recv()
void _pychan_bool_send(pychan pych, cbool obj) except +topyexc:
pych.chan_bool().send(obj)
void _pychan_bool_close(pychan pych) except +topyexc:
pych.chan_bool().close()
int _pychan_int_recv(pychan pych) except +topyexc:
return pych.chan_int().recv()
void _pychan_int_send(pychan pych, int obj) except +topyexc:
pych.chan_int().send(obj)
void _pychan_int_close(pychan pych) except +topyexc:
pych.chan_int().close()
double _pychan_double_recv(pychan pych) except +topyexc:
return pych.chan_double().recv()
void _pychan_double_send(pychan pych, double obj) except +topyexc:
pych.chan_double().send(obj)
void _pychan_double_close(pychan pych) except +topyexc:
pych.chan_double().close()
...@@ -29,6 +29,7 @@ from subprocess import Popen, PIPE ...@@ -29,6 +29,7 @@ from subprocess import Popen, PIPE
from six.moves import range as xrange from six.moves import range as xrange
import gc, weakref import gc, weakref
from golang import _golang_test
from golang._golang_test import pywaitBlocked as waitBlocked, pylen_recvq as len_recvq, \ from golang._golang_test import pywaitBlocked as waitBlocked, pylen_recvq as len_recvq, \
pylen_sendq as len_sendq, pypanicWhenBlocked as panicWhenBlocked pylen_sendq as len_sendq, pypanicWhenBlocked as panicWhenBlocked
...@@ -738,15 +739,160 @@ def _test_blockforever(): ...@@ -738,15 +739,160 @@ def _test_blockforever():
with panics("t: blocks forever"): select((z.send, 1), z.recv) with panics("t: blocks forever"): select((z.send, 1), z.recv)
def test_chan_misc(): # verify chan(dtype=X) functionality.
nilch = nilchan def test_chan_dtype_invalid():
with raises(TypeError) as exc:
chan(dtype="BadType")
assert exc.value.args == ("pychan: invalid dtype: 'BadType'",)
chantypev = [
# dtype obj zero-obj
('object', 'abc', None),
('C.structZ', None, None),
('C.bool', True, False),
('C.int', 4, 0),
('C.double', 3.14, 0.0),
]
@mark.parametrize('dtype,obj,zobj', chantypev)
def test_chan_dtype(dtype, obj, zobj):
# py -> py (pysend/pyrecv; buffered)
ch = chan(1, dtype=dtype)
ch.send(obj)
obj2, ok = ch.recv_()
assert ok == True
assert type(obj2) is type(obj)
assert obj2 == obj
# send with different type - rejected
for (dtype2, obj2, _) in chantypev:
if dtype2 == dtype or dtype == "object":
continue # X -> X; object accepts *,
if (dtype2, dtype) == ('C.int', 'C.double'): # int -> double ok
continue
with raises(TypeError) as exc:
ch.send(obj2)
# XXX we can implement vvv, but it will potentially hide cause error
# XXX (or use raise from?)
#assert exc.value.args == ("type mismatch: expect %s; got %r" % (dtype, obj2),)
with raises(TypeError) as exc:
select((ch.send, obj2))
# py -> py (pyclose/pyrecv)
ch.close()
obj2, ok = ch.recv_()
assert ok == False
assert type(obj2) is type(zobj)
assert obj2 == zobj
# below tests are for py <-> c interaction
if dtype == "object":
return
ctype = dtype[2:] # C.int -> int
ch = chan(dtype=dtype) # recreate after close; mode=synchronous
# recv/send/close via C
def crecv(ch):
return getattr(_golang_test, "pychan_%s_recv" % ctype)(ch)
def csend(ch, obj):
getattr(_golang_test, "pychan_%s_send" % ctype)(ch, obj)
def cclose(ch):
getattr(_golang_test, "pychan_%s_close" % ctype)(ch)
# py -> c (pysend/crecv)
rx = chan()
def _():
_ = crecv(ch)
rx.send(_)
go(_)
ch.send(obj)
obj2 = rx.recv()
assert type(obj2) is type(obj)
assert obj2 == obj
# py -> c (pyselect/crecv)
rx = chan()
def _():
_ = crecv(ch)
rx.send(_)
go(_)
_, _rx = select(
(ch.send, obj), # 0
)
assert (_, _rx) == (0, None)
obj2 = rx.recv()
assert type(obj2) is type(obj)
assert obj2 == obj
# py -> c (pyclose/crecv)
rx = chan()
def _():
_ = crecv(ch)
rx.send(_)
go(_)
ch.close()
obj2 = rx.recv()
assert type(obj2) is type(zobj)
assert obj2 == zobj
ch = chan(dtype=dtype) # recreate after close
# py <- c (pyrecv/csend)
def _():
csend(ch, obj)
go(_)
obj2 = ch.recv()
assert type(obj2) is type(obj)
assert obj2 == obj
# py <- c (pyselect/csend)
def _():
csend(ch, obj)
go(_)
_, _rx = select(
ch.recv, # 0
)
assert _ == 0
obj2 = _rx
assert type(obj2) is type(obj)
assert obj2 == obj
# py <- c (pyrecv/cclose)
def _():
cclose(ch)
go(_)
obj2 = ch.recv()
assert type(obj2) is type(zobj)
assert obj2 == zobj
@mark.parametrize('dtype', [_[0] for _ in chantypev])
def test_chan_dtype_misc(dtype):
nilch = chan.nil(dtype)
# nil repr
if dtype == "object":
assert repr(nilch) == "nilchan"
else:
assert repr(nilch) == ("chan.nil(%r)" % dtype)
assert nilch == nilch # nil == nil # optimization: nil[X]() -> always same object
nilch_ = chan.nil(dtype)
assert nilch is nilch_
if dtype == "object":
assert nilch is nilchan
assert hash(nilch) == hash(nilchan)
assert nilch == nilch # nil[X] == nil[X]
assert nilch == nilchan # nil[X] == nil[*]
assert nilchan == nilch # nil[*] == nil[X]
# channels can be compared, different channels differ # channels can be compared, different channels differ
assert nilch != None # just in case assert nilch != None # just in case
ch1 = chan() ch1 = chan(dtype=dtype)
ch2 = chan() ch2 = chan(dtype=dtype)
ch3 = chan() ch3 = chan()
assert ch1 != ch2; assert ch1 == ch1 assert ch1 != ch2; assert ch1 == ch1
assert ch1 != ch3; assert ch2 == ch2 assert ch1 != ch3; assert ch2 == ch2
...@@ -758,6 +904,43 @@ def test_chan_misc(): ...@@ -758,6 +904,43 @@ def test_chan_misc():
assert nilch != ch2 assert nilch != ch2
assert nilch != ch3 assert nilch != ch3
# .nil on chan instance XXX doesn't work (yet ?)
"""
ch = chan() # non-nil chan object instance
with raises(AttributeError):
ch.nil
"""
# nil[X] vs nil[Y]
for (dtype2, _, _) in chantypev:
nilch2 = chan.nil(dtype2)
# nil[*] stands for untyped nil - it is equal to nil[X] for ∀ X
if dtype == "object" or dtype2 == "object":
if dtype != dtype2:
assert nilch is not nilch2
assert hash(nilch) == hash(nilch2)
assert (nilch == nilch2) == True
assert (nilch2 == nilch) == True
assert (nilch != nilch2) == False
assert (nilch2 != nilch) == False
continue
# nil[X] == nil[X]
if dtype == dtype2:
assert hash(nilch) == hash(nilch2)
assert (nilch == nilch2) == True
assert (nilch2 == nilch) == True
assert (nilch != nilch2) == False
assert (nilch2 != nilch) == False
continue
# nil[X] != nil[Y]
assert nilch is not nilch2
assert (nilch == nilch2) == False
assert (nilch2 == nilch) == False
assert (nilch != nilch2) == True
assert (nilch2 != nilch) == True
def test_func(): def test_func():
# test how @func(cls) works # test how @func(cls) works
......
...@@ -164,7 +164,9 @@ typedef struct _selcase { ...@@ -164,7 +164,9 @@ typedef struct _selcase {
_chan *ch; // channel _chan *ch; // channel
enum _chanop op : 8; // chansend/chanrecv/default enum _chanop op : 8; // chansend/chanrecv/default
enum _selflags flags : 8; // e.g. _INPLACE_DATA enum _selflags flags : 8; // e.g. _INPLACE_DATA
unsigned :16; unsigned user : 8; // arbitrary value that can be set by user
// (e.g. pyselect stores channel type here)
unsigned : 8;
union { union {
void *ptxrx; // chansend: ptx; chanrecv: prx void *ptxrx; // chansend: ptx; chanrecv: prx
uint64_t itxrx; // used instead of .ptxrx if .flags&_INPLACE_DATA != 0 uint64_t itxrx; // used instead of .ptxrx if .flags&_INPLACE_DATA != 0
...@@ -191,6 +193,7 @@ _selcase _selsend(_chan *ch, const void *ptx) { ...@@ -191,6 +193,7 @@ _selcase _selsend(_chan *ch, const void *ptx) {
.ch = ch, .ch = ch,
.op = _CHANSEND, .op = _CHANSEND,
.flags = (enum _selflags)0, .flags = (enum _selflags)0,
.user = 0xff,
.ptxrx = (void *)ptx, .ptxrx = (void *)ptx,
.rxok = NULL, .rxok = NULL,
}; };
...@@ -204,6 +207,7 @@ _selcase _selrecv(_chan *ch, void *prx) { ...@@ -204,6 +207,7 @@ _selcase _selrecv(_chan *ch, void *prx) {
.ch = ch, .ch = ch,
.op = _CHANRECV, .op = _CHANRECV,
.flags = (enum _selflags)0, .flags = (enum _selflags)0,
.user = 0xff,
.ptxrx = prx, .ptxrx = prx,
.rxok = NULL, .rxok = NULL,
}; };
...@@ -217,6 +221,7 @@ _selcase _selrecv_(_chan *ch, void *prx, bool *pok) { ...@@ -217,6 +221,7 @@ _selcase _selrecv_(_chan *ch, void *prx, bool *pok) {
.ch = ch, .ch = ch,
.op = _CHANRECV, .op = _CHANRECV,
.flags = (enum _selflags)0, .flags = (enum _selflags)0,
.user = 0xff,
.ptxrx = prx, .ptxrx = prx,
.rxok = pok, .rxok = pok,
}; };
......
...@@ -860,6 +860,7 @@ const _selcase _default = { ...@@ -860,6 +860,7 @@ const _selcase _default = {
.ch = NULL, .ch = NULL,
.op = _DEFAULT, .op = _DEFAULT,
.flags = (_selflags)0, .flags = (_selflags)0,
.user = 0xff,
.ptxrx = NULL, .ptxrx = NULL,
.rxok = NULL, .rxok = NULL,
}; };
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment