1 # ==================================================================================
2 # Copyright (c) 2019-2020 Nokia
3 # Copyright (c) 2018-2020 AT&T Intellectual Property.
5 # Licensed under the Apache License, Version 2.0 (the "License");
6 # you may not use this file except in compliance with the License.
7 # You may obtain a copy of the License at
9 # http://www.apache.org/licenses/LICENSE-2.0
11 # Unless required by applicable law or agreed to in writing, software
12 # distributed under the License is distributed on an "AS IS" BASIS,
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 # See the License for the specific language governing permissions and
15 # limitations under the License.
16 # ==================================================================================
18 from ctypes import POINTER, Structure
19 from ctypes import c_int, c_char, c_char_p, c_void_p, memmove, cast, create_string_buffer
21 from ricxappframe.rmr.exceptions import BadBufferAllocation, MeidSizeOutOfRange, InitFailed
22 from ricxappframe.rmr.rmrclib.rmrclib import rmr_c_lib, get_constants, state_to_status
29 def _get_rmr_constant(key: str, default=None):
31 Gets the constant with the named key from the RMR C library.
32 Returns None if the value is not a simple type. This happens
33 during sphinx autodoc document generation, which mocks the
34 rmrclib package to work without the RMR shared object file,
35 and the response is something like this:
36 <class 'ricxappframe.rmr.rmrclib.rmrclib.get_constants.get'>
37 Workaround for https://github.com/sphinx-doc/sphinx/issues/7422
39 val = get_constants().get(key, default)
40 return val if isinstance(val, (type(None), bool, bytes, float, int, str)) else None
43 # argtypes and restype are important:
44 # https://stackoverflow.com/questions/24377845/ctype-why-specify-argtypes
45 def _wrap_rmr_function(funcname, restype, argtypes):
47 Simplify wrapping ctypes functions.
52 Name of library method
54 Name of ctypes class; e.g., c_char_p
56 List of ctypes classes; e.g., [ c_char_p, int ]
61 Pointer to C library function
63 func = rmr_c_lib.__getattr__(funcname)
64 func.restype = restype
65 func.argtypes = argtypes
73 # Publish constants from RMR C-language header files for use by importers of this library.
74 # TODO: Are there others that will be useful?
75 #: Typical size message to receive; size is not limited
76 RMR_MAX_RCV_BYTES = _get_rmr_constant('RMR_MAX_RCV_BYTES')
77 #: Multi-threaded initialization flag
78 RMRFL_MTCALL = _get_rmr_constant('RMRFL_MTCALL', 0x02) # initialization flags
80 RMRFL_NONE = _get_rmr_constant('RMRFL_NONE', 0x0)
81 #: State constant for OK
82 RMR_OK = _get_rmr_constant('RMR_OK', 0x00)
83 #: State constant for timeout
84 RMR_ERR_TIMEOUT = _get_rmr_constant('RMR_ERR_TIMEOUT')
85 #: State constant for retry
86 RMR_ERR_RETRY = _get_rmr_constant('RMR_ERR_RETRY')
89 class rmr_mbuf_t(Structure):
91 Mirrors public members of type rmr_mbuf_t from RMR header file src/common/include/rmr.h
94 | int state; // state of processing
95 | int mtype; // message type
96 | int len; // length of data in the payload (send or received)
97 | unsigned char* payload; // transported data
98 | unsigned char* xaction; // pointer to fixed length transaction id bytes
99 | int sub_id; // subscription id
100 | int tp_state; // transport state (a.k.a errno)
102 | these things are off limits to the user application
104 | void* tp_buf; // underlying transport allocated pointer (e.g. nng message)
105 | void* header; // internal message header (whole buffer: header+payload)
106 | unsigned char* id; // if we need an ID in the message separate from the xaction id
107 | int flags; // various MFL (private) flags as needed
108 | int alloc_len; // the length of the allocated space (hdr+payload)
111 RE PAYLOADs type below, see the documentation for c_char_p:
112 class ctypes.c_char_p
113 Represents the C char * datatype when it points to a zero-terminated string.
114 For a general character pointer that may also point to binary data, POINTER(c_char)
115 must be used. The constructor accepts an integer address, or a bytes object.
122 ("payload", POINTER(c_char)), # according to the following the python bytes are already unsigned
123 # https://bytes.com/topic/python/answers/695078-ctypes-unsigned-char
124 ("xaction", POINTER(c_char)),
130 _rmr_init = _wrap_rmr_function('rmr_init', c_void_p, [c_char_p, c_int, c_int])
133 def rmr_init(uproto_port: c_char_p, max_msg_size: int, flags: int) -> c_void_p:
135 Prepares the environment for sending and receiving messages.
136 Refer to RMR C documentation for method::
138 extern void* rmr_init(char* uproto_port, int max_msg_size, int flags)
140 This function raises an exception if the returned context is None.
144 uproto_port: c_char_p
145 Pointer to bytes built from the port number as a string; e.g., b'4550'
146 max_msg_size: integer
147 Maximum message size to receive
154 Pointer to RMR context
156 mrc = _rmr_init(uproto_port, max_msg_size, flags)
162 _rmr_ready = _wrap_rmr_function('rmr_ready', c_int, [c_void_p])
165 def rmr_ready(vctx: c_void_p) -> int:
167 Checks if a routing table has been received and installed.
168 Refer to RMR C documentation for method::
170 extern int rmr_ready(void* vctx)
174 vctx: ctypes c_void_p
175 Pointer to RMR context
181 return _rmr_ready(vctx)
184 _rmr_close = _wrap_rmr_function('rmr_close', None, [c_void_p])
187 def rmr_close(vctx: c_void_p):
189 Closes the listen socket.
190 Refer to RMR C documentation for method::
192 extern void rmr_close(void* vctx)
196 vctx: ctypes c_void_p
197 Pointer to RMR context
206 _rmr_set_stimeout = _wrap_rmr_function('rmr_set_stimeout', c_int, [c_void_p, c_int])
209 def rmr_set_stimeout(vctx: c_void_p, rloops: int) -> int:
211 Sets the configuration for how RMR will retry message send operations.
212 Refer to RMR C documentation for method::
214 extern int rmr_set_stimeout(void* vctx, int rloops)
218 vctx: ctypes c_void_p
219 Pointer to RMR context
221 Number of retry loops
225 0 on success, -1 on failure
227 return _rmr_set_stimeout(vctx, rloops)
230 _rmr_alloc_msg = _wrap_rmr_function('rmr_alloc_msg', POINTER(rmr_mbuf_t), [c_void_p, c_int])
233 def rmr_alloc_msg(vctx: c_void_p, size: int,
234 payload=None, gen_transaction_id=False, mtype=None,
235 meid=None, sub_id=None, fixed_transaction_id=None):
237 Allocates and returns a buffer to write and send through the RMR library.
238 Refer to RMR C documentation for method::
240 extern rmr_mbuf_t* rmr_alloc_msg(void* vctx, int size)
242 Optionally populates the message from the remaining arguments.
244 TODO: on next API break, clean up transaction_id ugliness. Kept for now to preserve API.
248 vctx: ctypes c_void_p
249 Pointer to RMR context
251 How much space to allocate
253 if not None, attempts to set the payload
254 gen_transaction_id: bool
255 if True, generates and sets a transaction ID.
256 Note, option fixed_transaction_id overrides this option
258 if not None, sets the sbuf's message type
260 if not None, sets the sbuf's meid
262 if not None, sets the sbuf's subscription id
263 fixed_transaction_id: bytes
264 if not None, used as the transaction ID.
265 Note, this overrides the option gen_transaction_id
270 Pointer to rmr_mbuf structure
272 sbuf = _rmr_alloc_msg(vctx, size)
274 # make sure the alloc worked
277 # set specified fields
279 set_payload_and_length(payload, sbuf)
281 if fixed_transaction_id:
282 set_transaction_id(sbuf, fixed_transaction_id)
283 elif gen_transaction_id:
284 generate_and_set_transaction_id(sbuf)
287 sbuf.contents.mtype = mtype
290 rmr_set_meid(sbuf, meid)
293 sbuf.contents.sub_id = sub_id
298 raise BadBufferAllocation
301 _rmr_realloc_payload = _wrap_rmr_function('rmr_realloc_payload', POINTER(rmr_mbuf_t), [POINTER(rmr_mbuf_t), c_int, c_int, c_int]) # new_len, copy, clone
304 def rmr_realloc_payload(ptr_mbuf: c_void_p, new_len: int, copy=False, clone=False):
306 Allocates and returns a message buffer large enough for the new length.
307 Refer to RMR C documentation for method::
309 extern rmr_mbuf_t* rmr_realloc_payload(rmr_mbuf_t*, int, int, int)
314 Pointer to rmr_mbuf structure
318 Whether to copy the original paylod
320 Whether to clone the original buffer
325 Pointer to rmr_mbuf structure
327 return _rmr_realloc_payload(ptr_mbuf, new_len, copy, clone)
330 _rmr_free_msg = _wrap_rmr_function('rmr_free_msg', None, [POINTER(rmr_mbuf_t)])
333 def rmr_free_msg(ptr_mbuf: c_void_p):
335 Releases the message buffer.
336 Refer to RMR C documentation for method::
338 extern void rmr_free_msg(rmr_mbuf_t* mbuf )
343 Pointer to rmr_mbuf structure
349 if ptr_mbuf is not None:
350 _rmr_free_msg(ptr_mbuf)
353 _rmr_payload_size = _wrap_rmr_function('rmr_payload_size', c_int, [POINTER(rmr_mbuf_t)])
356 def rmr_payload_size(ptr_mbuf: c_void_p) -> int:
358 Gets the number of bytes available in the payload.
359 Refer to RMR C documentation for method::
361 extern int rmr_payload_size(rmr_mbuf_t* msg)
366 Pointer to rmr_mbuf structure
371 Number of bytes available
373 return _rmr_payload_size(ptr_mbuf)
377 The following functions all seem to have the same interface
380 _rmr_send_msg = _wrap_rmr_function('rmr_send_msg', POINTER(rmr_mbuf_t), [c_void_p, POINTER(rmr_mbuf_t)])
383 def rmr_send_msg(vctx: c_void_p, ptr_mbuf: POINTER(rmr_mbuf_t)) -> POINTER(rmr_mbuf_t):
385 Sends the message according to the routing table and returns an empty buffer.
386 Refer to RMR C documentation for method::
388 extern rmr_mbuf_t* rmr_send_msg(void* vctx, rmr_mbuf_t* msg)
392 vctx: ctypes c_void_p
393 Pointer to RMR context
395 Pointer to rmr_mbuf structure
400 Pointer to rmr_mbuf structure
402 return _rmr_send_msg(vctx, ptr_mbuf)
405 # TODO: the old message (Send param) is actually optional, but I don't know how to specify that in Ctypes.
406 _rmr_rcv_msg = _wrap_rmr_function('rmr_rcv_msg', POINTER(rmr_mbuf_t), [c_void_p, POINTER(rmr_mbuf_t)])
409 def rmr_rcv_msg(vctx: c_void_p, ptr_mbuf: POINTER(rmr_mbuf_t)) -> POINTER(rmr_mbuf_t):
411 Waits for a message to arrive, and returns it.
412 Refer to RMR C documentation for method::
414 extern rmr_mbuf_t* rmr_rcv_msg(void* vctx, rmr_mbuf_t* old_msg)
418 vctx: ctypes c_void_p
419 Pointer to RMR context
421 Pointer to rmr_mbuf structure
426 Pointer to rmr_mbuf structure
428 return _rmr_rcv_msg(vctx, ptr_mbuf)
431 _rmr_torcv_msg = _wrap_rmr_function('rmr_torcv_msg', POINTER(rmr_mbuf_t), [c_void_p, POINTER(rmr_mbuf_t), c_int])
434 def rmr_torcv_msg(vctx: c_void_p, ptr_mbuf: POINTER(rmr_mbuf_t), ms_to: int) -> POINTER(rmr_mbuf_t):
436 Waits up to the timeout value for a message to arrive, and returns it.
437 Refer to RMR C documentation for method::
439 extern rmr_mbuf_t* rmr_torcv_msg(void* vctx, rmr_mbuf_t* old_msg, int ms_to)
443 vctx: ctypes c_void_p
444 Pointer to RMR context
446 Pointer to rmr_mbuf structure
448 Time out value in milliseconds
453 Pointer to rmr_mbuf structure
455 return _rmr_torcv_msg(vctx, ptr_mbuf, ms_to)
458 _rmr_rts_msg = _wrap_rmr_function('rmr_rts_msg', POINTER(rmr_mbuf_t), [c_void_p, POINTER(rmr_mbuf_t)])
461 def rmr_rts_msg(vctx: c_void_p, ptr_mbuf: POINTER(rmr_mbuf_t), payload=None, mtype=None) -> POINTER(rmr_mbuf_t):
463 Sends a message to the originating endpoint and returns an empty buffer.
464 Refer to RMR C documentation for method::
466 extern rmr_mbuf_t* rmr_rts_msg(void* vctx, rmr_mbuf_t* msg)
468 additional features beyond c-rmr:
469 if payload is not None, attempts to set the payload
470 if mtype is not None, sets the sbuf's message type
474 vctx: ctypes c_void_p
475 Pointer to an RMR context
476 ptr_mbuf: ctypes c_void_p
477 Pointer to an RMR message buffer
486 Pointer to rmr_mbuf structure
490 set_payload_and_length(payload, ptr_mbuf)
493 ptr_mbuf.contents.mtype = mtype
495 return _rmr_rts_msg(vctx, ptr_mbuf)
498 _rmr_call = _wrap_rmr_function('rmr_call', POINTER(rmr_mbuf_t), [c_void_p, POINTER(rmr_mbuf_t)])
501 def rmr_call(vctx: c_void_p, ptr_mbuf: POINTER(rmr_mbuf_t)) -> POINTER(rmr_mbuf_t):
503 Sends a message, waits for a response and returns it.
504 Refer to RMR C documentation for method::
506 extern rmr_mbuf_t* rmr_call(void* vctx, rmr_mbuf_t* msg)
510 ptr_mbuf: ctypes c_void_p
511 Pointer to an RMR message buffer
516 Pointer to rmr_mbuf structure
518 return _rmr_call(vctx, ptr_mbuf)
521 _rmr_bytes2meid = _wrap_rmr_function('rmr_bytes2meid', c_int, [POINTER(rmr_mbuf_t), c_char_p, c_int])
524 def rmr_set_meid(ptr_mbuf: POINTER(rmr_mbuf_t), byte_str: bytes) -> int:
526 Sets the managed entity field in the message and returns the number of bytes copied.
527 Refer to RMR C documentation for method::
529 extern int rmr_bytes2meid(rmr_mbuf_t* mbuf, unsigned char const* src, int len);
531 Caution: the meid length supported in an RMR message is 32 bytes, but C applications
532 expect this to be a nil terminated string and thus only 31 bytes are actually available.
534 Raises: exceptions.MeidSizeOutOfRang
538 ptr_mbuf: ctypes c_void_p
539 Pointer to an RMR message buffer
541 Managed entity ID value
546 number of bytes copied
548 max = _get_rmr_constant("RMR_MAX_MEID", 32)
549 if len(byte_str) >= max:
550 raise MeidSizeOutOfRange
552 return _rmr_bytes2meid(ptr_mbuf, byte_str, len(byte_str))
555 # CAUTION: Some of the C functions expect a mutable buffer to copy the bytes into;
556 # if there is a get_* function below, use it to set up and return the
559 # extern unsigned char* rmr_get_meid(rmr_mbuf_t* mbuf, unsigned char* dest);
560 # we don't provide direct access to this function (unless it is asked for) because it is not really useful to provide your own buffer.
561 # Rather, rmr_get_meid does this for you, and just returns the string.
562 _rmr_get_meid = _wrap_rmr_function('rmr_get_meid', c_char_p, [POINTER(rmr_mbuf_t), c_char_p])
565 def rmr_get_meid(ptr_mbuf: POINTER(rmr_mbuf_t)) -> bytes:
567 Gets the managed entity ID (meid) from the message header.
568 This is a python-friendly version of RMR C method::
570 extern unsigned char* rmr_get_meid(rmr_mbuf_t* mbuf, unsigned char* dest);
574 ptr_mbuf: ctypes c_void_p
575 Pointer to an RMR message buffer
582 sz = _get_rmr_constant("RMR_MAX_MEID", 32) # size for buffer to fill
583 buf = create_string_buffer(sz)
584 _rmr_get_meid(ptr_mbuf, buf)
588 _rmr_get_src = _wrap_rmr_function('rmr_get_src', c_char_p, [POINTER(rmr_mbuf_t), c_char_p])
591 def rmr_get_src(ptr_mbuf: POINTER(rmr_mbuf_t), dest: c_char_p) -> c_char_p:
593 Copies the message-source information to the buffer.
594 Refer to RMR C documentation for method::
596 extern unsigned char* rmr_get_src(rmr_mbuf_t* mbuf, unsigned char* dest);
600 ptr_mbuf: ctypes POINTER(rmr_mbuf_t)
601 Pointer to an RMR message buffer
602 dest: ctypes c_char_p
603 Pointer to a buffer to receive the message source
608 message-source information
610 return _rmr_get_src(ptr_mbuf, dest)
613 # Methods that exist ONLY in rmr-python, and are not wrapped methods
614 # In hindsight, I wish i put these in a separate module, but leaving this here to prevent api breakage.
617 def get_payload(ptr_mbuf: c_void_p) -> bytes:
619 Gets the binary payload from the rmr_buf_t*.
623 ptr_mbuf: ctypes c_void_p
624 Pointer to an rmr message buffer
631 # Logic came from the answer here: https://stackoverflow.com/questions/55103298/python-ctypes-read-pointerc-char-in-python
632 sz = ptr_mbuf.contents.len
633 CharArr = c_char * sz
634 return CharArr(*ptr_mbuf.contents.payload[:sz]).raw
637 def get_xaction(ptr_mbuf: c_void_p) -> bytes:
639 Gets the transaction ID from the rmr_buf_t*.
643 ptr_mbuf: ctypes c_void_p
644 Pointer to an rmr message buffer
651 val = cast(ptr_mbuf.contents.xaction, c_char_p).value
652 sz = _get_rmr_constant("RMR_MAX_XID", 0)
656 def message_summary(ptr_mbuf: c_void_p) -> dict:
658 Returns a dict with the fields of an RMR message.
662 ptr_mbuf: ctypes c_void_p
663 Pointer to an rmr message buffer
671 "payload": get_payload(ptr_mbuf) if ptr_mbuf.contents.state == RMR_OK else None,
672 "payload length": ptr_mbuf.contents.len,
673 "message type": ptr_mbuf.contents.mtype,
674 "subscription id": ptr_mbuf.contents.sub_id,
675 "transaction id": get_xaction(ptr_mbuf),
676 "message state": ptr_mbuf.contents.state,
677 "message status": state_to_status(ptr_mbuf.contents.state),
678 "payload max size": rmr_payload_size(ptr_mbuf),
679 "meid": rmr_get_meid(ptr_mbuf),
680 "message source": get_src(ptr_mbuf),
681 "errno": ptr_mbuf.contents.tp_state,
685 def set_payload_and_length(byte_str: bytes, ptr_mbuf: c_void_p):
687 Sets an rmr payload and content length.
692 the bytes to set the payload to
693 ptr_mbuf: ctypes c_void_p
694 Pointer to an rmr message buffer
700 if rmr_payload_size(ptr_mbuf) < len(byte_str): # existing message payload too small
701 ptr_mbuf = rmr_realloc_payload(ptr_mbuf, len(byte_str), True)
703 memmove(ptr_mbuf.contents.payload, byte_str, len(byte_str))
704 ptr_mbuf.contents.len = len(byte_str)
707 def generate_and_set_transaction_id(ptr_mbuf: c_void_p):
709 Generates a UUID and sets the RMR transaction id to it
713 ptr_mbuf: ctypes c_void_p
714 Pointer to an rmr message buffer
716 set_transaction_id(ptr_mbuf, uuid.uuid1().hex.encode("utf-8"))
719 def set_transaction_id(ptr_mbuf: c_void_p, tid_bytes: bytes):
721 Sets an RMR transaction id
722 TODO: on next API break, merge these two functions. Not done now to preserve API.
726 ptr_mbuf: ctypes c_void_p
727 Pointer to an rmr message buffer
729 bytes of the desired transaction id
731 sz = _get_rmr_constant("RMR_MAX_XID", 0)
732 memmove(ptr_mbuf.contents.xaction, tid_bytes, sz)
735 def get_src(ptr_mbuf: c_void_p) -> str:
737 Gets the message source (likely host:port)
741 ptr_mbuf: ctypes c_void_p
742 Pointer to an rmr message buffer
749 sz = _get_rmr_constant("RMR_MAX_SRC", 64) # size to fill
750 buf = create_string_buffer(sz)
751 rmr_get_src(ptr_mbuf, buf)
752 return buf.value.decode()