I’m able to set up TCP and UDP to send some basic data back and forth, but I can’t figure out how to send the bits-type data from some simple immutable structs (short of sending each field separately). In C, I would just use a pointer to the_struct
and send sizeof(the_struct)
bytes. I thought it would be similar with Julia using the unsafe_convert
/unsafe_wrap
functions, but I can’t figure out how. The ultimate goal is to send structured data from Julia to C running on an embedded processor. Anyone have some pointers for me? Is there a better way to do this altogether?
Don’t use unsafe_convert
and unsafe_wrap
, they are unrelated and in particular unsafe_wrap is not allowed to be used on object pointers.
There are multiple ways to do it, the simplest way is basically replace C pointers with Ref
Server,
julia> sock = listen(ip"127.0.0.1", 9000)
Base.TCPServer(RawFD(17) active)
julia> client = accept(sock)
TCPSocket(RawFD(19) open, 0 bytes waiting)
julia> read!(client, Ref{Tuple{Int,Float64}}())
Base.RefValue{Tuple{Int64,Float64}}((1, 2.0))
client
julia> sock = connect(ip"127.0.0.1", 9000)
TCPSocket(RawFD(17) open, 0 bytes waiting)
julia> v = (1, 2.0)
(1, 2.0)
julia> write(sock, Ref(v))
16
@yuyichao, thanks! This is helpful.
Maybe there’s a version issue here (I’m v0.6): when I get to your read!
line though, it can’t find a matching read!
.
julia> read!(client, Ref{Tuple{Int,Float64}}())
ERROR: MethodError: no method matching read!(::TCPSocket, ::Base.RefValue{Tuple{Int64,Float64}})
Closest candidates are:
read!(::IO, ::BitArray) at bitarray.jl:2007
read!(::AbstractString, ::Any) at io.jl:161
read!(::IO, ::Array{UInt8,N} where N) at io.jl:387
...
I used z = (1, 1.); x = read(client, Ref(z)); payload = x[]
to get the data.
Also, do you know how this would be different with UDP? UDP doesn’t seem to support read
and write
, but uses recv
and send
, with different interfaces that don’t quite map to the way this works.
We should probably try to add those if they are not there. In the mean time you should be able to use an array to send the data. (recv
doesn’t really support that but you can copy the data)
I think the part I’m missing is how to map a struct to an array of UInt8s (and the reverse). If I had the UInt8s, then send
and recv
will work for me. I feel like I’m missing something really basic.
reinterpret
for input and unsafe_copy!
for output.
Thanks again, @yuyichao; I have it working. Dealing with pointers to immutables structs was very confusing to me as a new Julia user who’s familiar with C. For future readers, let me summarize what I got working. Perhaps others will know a cleaner way to do this!
Listener side:
sock = UDPSocket(); # Create the socket.
bind(sock, ip"127.0.0.1", 2000); # Listen on port 2000.
x = recv(sock); # Get an array of UInt8s from somewhere.
Sender side:
struct MySt; a::UInt32; b::Float64; end; # Create a super-simple immutable struct type.
m = MySt(1, 2.3); # Create an instance of the type.
sock = UDPSocket(); # Get ready to send some UDP.
x = reinterpret(UInt8, [m]); # Turn m into an array of UInt8, stored in x.
send(sock, ip"127.0.0.1", 2000, x); # Send it.
Note in the above that the call to reinterpret
requires an array of the struct! I don’t really follow why. There are several long discussions about this around the internet; the longest actually currently ends with, “Wait, so why is it fine to convert an array of one immutable struct to bytes but not a scalar?” So, I don’t know, but we need to use an array as far as I can tell.
Back over to the listener, we now have x
:
julia> x
16-element Array{UInt8,1}:
0x01
0x00
...
struct MySt; a::UInt32; b::Float64; end; # Define the same type on this end.
ma = [MySt(0, 0.)]; # Create an ARRAY to write to.
pma = reinterpret(Ptr{UInt8}, pointer(ma)); # Pretend that a pointer to ma is actually a pointer to UInt8.
unsafe_copy!(pma, pointer(x), 16); # Copy 16 units of whatever type (UInt8 in our case) to our pointer to the array from a pointer to the received data.
julia> m = ma[1] # Finally, pull out the data from our array.
MySt(0x00000001, 3.141592653589793)
It worked! Again, note that we can’t get a pointer to an instance of our struct; we have to get a pointer to the beginning of an array of our struct.
In C, the UDP part takes much more code, but the “just copy from this memory location to that one” part is far less confusing (recvfrom(sock, &m, 16, ...)
). Does anyone see a good way to clean up my code above to make this more straightforward? I’d be happy to contribute something to the codebase, but my Julia-fu is clearly not where it needs to be yet.
[Despite that credit clearly goes to @yuyichao for the help, I’m marking this reply as the solution since it contains the working answer all in one post, and I’ll edit as necessary to reflect any new additions.]
Fast forwarding to 2023 and I wonder what is the most appropriate way to do that. @yuyichao solution works nicely. But using Serialization
appears a bit less complicated:
using Sockets, Serialization
using UUIDs # use case: transfer UUIDs
# server
servsock = listen(Sockets.localhost, 9000)
sock = accept(servsock)
servid = deserialize(sock)
# client
clisock = connect(Sockets.localhost, 9000)
id = UUIDs.uuid4()
serialize(clisock, id)
However, every now and then people are saying that serialization is very sensitive to the julia version. When doing such remote stuff it’s highly likely that the julia versions are different. However I don’t see how to avoid that, outside of making a customized protocol per composite type. So for me it looks like the Serialization
solution would be the way to go but keeping in mind for julia version potential troubles.