1. Cross-language Exchange
nanonext provides a fast, reliable data interface
between different programming languages where NNG has an implementation,
including C, C++, Java, Python, Go, and Rust.
This messaging interface is lightweight, robust, and has limited points of failure. It enables:
- Communication between processes in the same or different languages
- Distributed computing across networks or on the same machine
- Real-time data pipelines where computation times exceed data frequency
- Modular software design following Unix philosophy
This example demonstrates numerical data exchange between R and Python (NumPy).
Create socket in Python using the NNG binding ‘pynng’:
Create nano object in R using nanonext, then send a
vector of ‘doubles’, specifying mode as ‘raw’:
library(nanonext)
n <- nano("pair", dial = "ipc:///tmp/nanonext.socket")
n$send(c(1.1, 2.2, 3.3, 4.4, 5.5), mode = "raw")
#> [1] 0Receive in Python as a NumPy array of ‘floats’, and send back to R:
raw = socket.recv()
array = np.frombuffer(raw)
print(array)
#> [1.1 2.2 3.3 4.4 5.5]
msg = array.tobytes()
socket.send(msg)
socket.close()Receive in R, specifying the receive mode as ‘double’:
n$recv(mode = "double")
#> [1] 1.1 2.2 3.3 4.4 5.5
n$close()2. Async and Concurrency
nanonext implements true async send and receive,
leveraging NNG as a massively-scalable concurrency framework.
send_aio() and recv_aio() return
immediately with an ‘Aio’ object that performs operations
asynchronously. Aio objects return an unresolved value while the
operation is ongoing, then automatically resolve once complete.
# async receive requested, but no messages waiting yet
msg <- recv_aio(s2)
msg
#> < recvAio | $data >
msg$data
#> 'unresolved' logi NAFor ‘sendAio’ objects, the result is stored at $result.
For ‘recvAio’ objects, the message is stored at $data.
res <- send_aio(s1, data.frame(a = 1, b = 2))
res
#> < sendAio | $result >
res$result
#> [1] 00 indicates successful send - the message has been accepted by the socket for sending but may still be buffered within the system.
# once a message is sent, the recvAio resolves automatically
msg$data
#> a b
#> 1 1 2Use unresolved() in control flow to perform actions
before or after Aio resolution without blocking.
msg <- recv_aio(s2)
# unresolved() checks resolution status
while (unresolved(msg)) {
# perform other tasks while waiting
send_aio(s1, "resolved")
cat("unresolved")
}
#> unresolved
# access resolved value
msg$data
#> [1] "resolved"Explicitly wait for completion with call_aio()
(blocking).
# wait for completion and return resolved Aio
call_aio(msg)
# access resolved value (waiting if required):
call_aio(msg)$data
#> [1] "resolved"
# or directly:
collect_aio(msg)
#> [1] "resolved"
# or user-interruptible:
msg[]
#> [1] "resolved"
close(s1)
close(s2)3. Synchronisation Primitives
nanonext implements cross-platform synchronisation
primitives from the NNG library, enabling synchronisation between NNG
events and the main R execution thread.
Condition variables can signal events such as asynchronous receive
completions and pipe events (connections established or dropped). Each
condition variable has a value (counter) and flag (boolean). Signals
increment the value; successful wait() or
until() calls decrement it. A non-zero value allows waiting
threads to continue.
This approach is more efficient than polling - consuming no resources while waiting and synchronising with zero latency.
Example 1: Wait for connection
sock <- socket("pair", listen = "inproc://nanopipe")
cv <- cv()
cv_value(cv)
#> [1] 0
pipe_notify(sock, cv = cv, add = TRUE, remove = TRUE)
# wait(cv) would block until connection established
# for illustration:
sock2 <- socket("pair", dial = "inproc://nanopipe")
cv_value(cv) # incremented when pipe created
#> [1] 1
wait(cv) # does not block as cv value is non-zero
cv_value(cv) # decremented by wait()
#> [1] 0
close(sock2)
cv_value(cv) # incremented when pipe destroyed
#> [1] 1
close(sock)Example 2: Wait for message or disconnection
sock <- socket("pair", listen = "inproc://nanosignal")
sock2 <- socket("pair", dial = "inproc://nanosignal")
cv <- cv()
cv_value(cv)
#> [1] 0
pipe_notify(sock, cv = cv, add = FALSE, remove = TRUE, flag = TRUE)
send(sock2, "this message will wake waiting thread")
#> [1] 0
r <- recv_aio(sock, cv = cv)
# wakes when async receive completes
wait(cv) || stop("peer disconnected")
#> [1] TRUE
r$data
#> [1] "this message will wake waiting thread"
close(sock)
close(sock2)When flag = TRUE is set for pipe notifications,
wait() returns FALSE for pipe events (rather than TRUE for
message events). This distinguishes between disconnections and
successful receives, something not possible using
call_aio() alone.
This mechanism enables waiting simultaneously on multiple events
while distinguishing between them. pipe_notify() can signal
up to two condition variables per event for additional flexibility in
concurrent applications.
