99# Uses relative paths (./in, ./out) for local execution.
1010# For Docker containers, use concoredocker.jl instead.
1111#
12- # No external dependencies -- only Julia stdlib .
12+ # No hard external dependencies; ZMQ.jl is optional .
1313
1414module Concore
1515
1616using Mmap
1717
18+ const HAS_ZMQ = try
19+ @eval import ZMQ
20+ true
21+ catch
22+ false
23+ end
24+
1825# -----------------------------------------------------------------------------
1926# Backend selection
2027# -----------------------------------------------------------------------------
@@ -35,9 +42,13 @@ struct MmapBackend <: AbstractBackend
3542 end
3643end
3744
45+ """ ZeroMQ transport marker backend."""
46+ struct ZmqBackend <: AbstractBackend end
47+
3848# Compatibility name for proposal/docs wording.
3949const FileTransport = FileBackend
4050const MmapTransport = MmapBackend
51+ const ZmqTransport = ZmqBackend
4152
4253# -----------------------------------------------------------------------------
4354# Path configuration
@@ -97,10 +108,189 @@ _backend_inpath(::FileBackend) = inpath
97108_backend_outpath (:: FileBackend ) = outpath
98109_backend_inpath (:: MmapBackend ) = inpath
99110_backend_outpath (:: MmapBackend ) = outpath
111+ _backend_inpath (:: ZmqBackend ) = " zmq://in"
112+ _backend_outpath (:: ZmqBackend ) = " zmq://out"
100113
101114_input_dir (port:: Int ) = _backend_inpath (_backend) * string (port)
102115_output_dir (port:: Int ) = _backend_outpath (_backend) * string (port)
103116
117+ mutable struct ZeroMQPort
118+ context:: Any
119+ socket:: Any
120+ port_type:: String
121+ address:: String
122+ socket_type:: String
123+ end
124+
125+ const zmq_ports = Dict {String, ZeroMQPort} ()
126+ const _zmq_context = Ref {Any} (nothing )
127+ const _zmq_cleanup_registered = Ref (false )
128+
129+ function _require_zmq ()
130+ HAS_ZMQ && return nothing
131+ error (" ZMQ.jl is not installed. Install it with: using Pkg; Pkg.add(\" ZMQ\" )" )
132+ end
133+
134+ function _zmq_socket_type (socket_type:: AbstractString )
135+ _require_zmq ()
136+ upper = uppercase (String (socket_type))
137+ for name in (" REQ" , " REP" , " PUB" , " SUB" , " PUSH" , " PULL" , " PAIR" )
138+ upper == name && return getfield (ZMQ, Symbol (name))
139+ end
140+ error (" unknown ZMQ socket type '$socket_type '" )
141+ end
142+
143+ function _register_zmq_cleanup ()
144+ if ! _zmq_cleanup_registered[]
145+ atexit (terminate_zmq)
146+ _zmq_cleanup_registered[] = true
147+ end
148+ end
149+
150+ function _get_zmq_context ()
151+ _require_zmq ()
152+ if _zmq_context[] === nothing
153+ _zmq_context[] = ZMQ. Context ()
154+ end
155+ return _zmq_context[]
156+ end
157+
158+ function _configure_zmq_socket (socket)
159+ ZMQ. set_rcvtimeo (socket, 2000 )
160+ ZMQ. set_sndtimeo (socket, 2000 )
161+ ZMQ. set_linger (socket, 0 )
162+ return nothing
163+ end
164+
165+ function _zmq_send (socket, payload:: AbstractString ; max_retries:: Int = 5 )
166+ _require_zmq ()
167+ max_retries > 0 || throw (ArgumentError (" max_retries must be positive" ))
168+ for attempt in 1 : max_retries
169+ try
170+ ZMQ. send (socket, String (payload))
171+ return nothing
172+ catch
173+ attempt == max_retries && rethrow ()
174+ sleep (0.5 )
175+ end
176+ end
177+ end
178+
179+ function _zmq_recv (socket; max_retries:: Int = 5 ):: String
180+ _require_zmq ()
181+ max_retries > 0 || throw (ArgumentError (" max_retries must be positive" ))
182+ for attempt in 1 : max_retries
183+ try
184+ return String (ZMQ. recv (socket))
185+ catch
186+ attempt == max_retries && rethrow ()
187+ sleep (0.5 )
188+ end
189+ end
190+ error (" ZMQ receive failed" )
191+ end
192+
193+ function _zmq_payload (val:: AbstractVector{<:Real} , delta:: Real ):: String
194+ return _format_wire (Float64[simtime + Float64 (delta); Float64 .(val)])
195+ end
196+
197+ function _zmq_payload (val:: AbstractString , delta:: Real ):: String
198+ return String (val)
199+ end
200+
201+ function _close_zmq_port (port:: ZeroMQPort )
202+ try
203+ ZMQ. close (port. socket)
204+ catch
205+ end
206+ return nothing
207+ end
208+
209+ function init_zmq_port (
210+ port_name:: AbstractString ,
211+ port_type:: AbstractString ,
212+ address:: AbstractString ,
213+ socket_type:: AbstractString ,
214+ )
215+ _require_zmq ()
216+ _register_zmq_cleanup ()
217+
218+ ptype = lowercase (String (port_type))
219+ (ptype == " bind" || ptype == " connect" ) || error (" port_type must be bind or connect" )
220+
221+ if haskey (zmq_ports, String (port_name))
222+ return nothing
223+ end
224+
225+ ctx = _get_zmq_context ()
226+ socket = ZMQ. Socket (ctx, _zmq_socket_type (socket_type))
227+ _configure_zmq_socket (socket)
228+
229+ if ptype == " bind"
230+ ZMQ. bind (socket, String (address))
231+ else
232+ ZMQ. connect (socket, String (address))
233+ end
234+
235+ zmq_ports[String (port_name)] = ZeroMQPort (
236+ ctx,
237+ socket,
238+ ptype,
239+ String (address),
240+ uppercase (String (socket_type)),
241+ )
242+ return nothing
243+ end
244+
245+ function init_zmq_port (port_name, port_type:: Symbol , address, socket_type:: Symbol )
246+ init_zmq_port (string (port_name), string (port_type), string (address), string (socket_type))
247+ end
248+
249+ function _zmq_read (port_name:: AbstractString , initstr:: AbstractString ):: Vector{Float64}
250+ _require_zmq ()
251+ port = get (zmq_ports, String (port_name), nothing )
252+ port === nothing && error (" ZMQ port '$port_name ' not registered" )
253+
254+ global simtime
255+ try
256+ msg = _zmq_recv (port. socket)
257+ vals = safe_parse_list (msg)
258+ simtime = max (simtime, vals[1 ])
259+ return vals[2 : end ]
260+ catch
261+ return initval (initstr)
262+ end
263+ end
264+
265+ function _zmq_write (
266+ port_name:: AbstractString ,
267+ val:: Union{AbstractVector{<:Real},AbstractString} ,
268+ delta:: Real ,
269+ )
270+ _require_zmq ()
271+ port = get (zmq_ports, String (port_name), nothing )
272+ port === nothing && error (" ZMQ port '$port_name ' not registered" )
273+
274+ payload = _zmq_payload (val, delta)
275+ _zmq_send (port. socket, payload)
276+ return nothing
277+ end
278+
279+ function terminate_zmq ()
280+ for port in values (zmq_ports)
281+ _close_zmq_port (port)
282+ end
283+ empty! (zmq_ports)
284+ if _zmq_context[] != = nothing
285+ try
286+ ZMQ. close (_zmq_context[])
287+ catch
288+ end
289+ _zmq_context[] = nothing
290+ end
291+ return nothing
292+ end
293+
104294const _mmap_segments = Dict {String, Tuple{IOStream, Vector{UInt8}}} ()
105295const _mmap_cleanup_registered = Ref (false )
106296
@@ -498,6 +688,21 @@ function concore_read(
498688 return val[2 : end ]
499689end
500690
691+ function concore_read (
692+ port_identifier:: AbstractString ,
693+ name:: AbstractString ,
694+ initstr:: AbstractString ,
695+ ):: Vector{Float64}
696+ if haskey (zmq_ports, String (port_identifier))
697+ return _zmq_read (port_identifier, initstr)
698+ end
699+
700+ file_port = tryparse (Int, port_identifier)
701+ file_port != = nothing && return concore_read (file_port, name, initstr)
702+
703+ error (" ZMQ port '$port_identifier ' not registered" )
704+ end
705+
501706"""
502707 concore_write(port::Int, name::AbstractString, val::AbstractVector{<:Real}; delta::Real=0)
503708
@@ -525,6 +730,22 @@ function concore_write(
525730 return nothing
526731end
527732
733+ function concore_write (
734+ port_identifier:: AbstractString ,
735+ name:: AbstractString ,
736+ val:: AbstractVector{<:Real} ;
737+ delta:: Real = 0 ,
738+ )
739+ if haskey (zmq_ports, String (port_identifier))
740+ return _zmq_write (port_identifier, val, delta)
741+ end
742+
743+ file_port = tryparse (Int, port_identifier)
744+ file_port != = nothing && return concore_write (file_port, name, val; delta= delta)
745+
746+ error (" ZMQ port '$port_identifier ' not registered" )
747+ end
748+
528749"""
529750 concore_write(port::Int, name::AbstractString, val::AbstractString; delta::Int=0)
530751
@@ -547,6 +768,22 @@ function concore_write(
547768 return nothing
548769end
549770
771+ function concore_write (
772+ port_identifier:: AbstractString ,
773+ name:: AbstractString ,
774+ val:: AbstractString ;
775+ delta:: Int = 0 ,
776+ )
777+ if haskey (zmq_ports, String (port_identifier))
778+ return _zmq_write (port_identifier, val, delta)
779+ end
780+
781+ file_port = tryparse (Int, port_identifier)
782+ file_port != = nothing && return concore_write (file_port, name, val; delta= delta)
783+
784+ error (" ZMQ port '$port_identifier ' not registered" )
785+ end
786+
550787"""
551788 initval(simtime_val::AbstractString) -> Vector{Float64}
552789
@@ -617,6 +854,7 @@ export load_iport!, load_oport!, load_params!, concore_init!
617854export load_iport, load_oport, load_params, default_maxtime, concore_init
618855export AbstractBackend, FileBackend, FileTransport
619856export MmapBackend, MmapTransport, mmap_cleanup
857+ export ZmqBackend, ZmqTransport, HAS_ZMQ, init_zmq_port, terminate_zmq, zmq_ports
620858
621859# -----------------------------------------------------------------------------
622860# Auto-initialize on load
0 commit comments