简体   繁体   中英

Reading C++ structs from socket in LISP

We have a application protocol defined as C++ classes which are transferred over the network. I want to connect to a server which sends data in this format. I want to write a client in lisp (sbcl is preferred) to communicate with this server. I would prefer it to be written in pure lisp instead of using CFFI to wrap around a C++ dll. The sample structures would look something like this:

class Header
{
public:
    int MsgType;
    uint64_t Length;
}

class SampleMsg
{
public:
    Header MsgHeader;
    char Field1[256];
    bool Field2;
    double Field3;
    SomeOtherClass Field4;
}

I want to know how to map these structs in lisp so that they are binary-compatible and how to read/write such structs. Is there a simpler way than packing/unpacking each field in a struct?

For example in C# you can map the binary structure like follows and read it directly from a byte array:

[StructLayout(LayoutKind.Sequential)]
public struct Header
{
    public int MsgType;
    public ulong Length;
}

[StructLayout(LayoutKind.Sequential)]
public struct SampleMsg
{
public:
    public Header MsgHeader;
    [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 256)]
    public string Field1;
    public bool Field2;
    public double Field3;
    public SomeOtherClass Field4;
}

If a similar method is possible in lisp it would be ideal. If not, I'm willing to do some plumbing as long as it's manageable.

EDIT:

Tried Svante's suggestion:

(ql:quickload "userial")
(in-package :sb-bsd-sockets)

(defun read-buffer (host port)
  (let ((socket (make-instance 'inet-socket :type :stream :protocol :tcp)))
    (socket-connect socket host port)
    (let ((buf (socket-receive socket nil 1024 :element-type '(unsigned-byte 8))))
      (socket-close socket)
      buf)))


(defstruct header
  msg-type
  length)


(userial:make-slot-serializer (:header header (make-header))
                  :int64 msg-type
                  :uint64 length)

(defvar *buffer*)
(defvar *b*)
(setq *buffer* (read-buffer #(10 1 2 75) 5003))
(setq *b* (make-array 2048 :element-type '(unsigned-byte 8) :fill-pointer 0 :adjustable t))
(map 'vector #'(lambda (x) (vector-push x *b*)) *buffer*)

(setf (fill-pointer *b*) 0)

At this point, *b* holds something like this: #(7 0 0 0 0 0 0 0 176 2 0 0 0 0 0 0 45 71 253 83 0 0 0 0 165 30 11 11 0 0 0 ...) . The first 7 corresponds to the msg type which should be 7. The length is supposed to be 688 (176 + 2*256).

Now I do (userial:with-buffer *b* (userial:unserialize :header)) . This gives me

#S(HEADER :MSG-TYPE 504403158265495552 :LENGTH 12682699500628738048)
#(7 0 0 0 0 0 0 0 176 2 0 0 0 0 0 0)

Seems like an endianness problem. How to fix this? I can't find any way to handle endianness within userial lib.

EDIT2:

Finally ended up giving up on userial and writing these (Following Practical Common Lisp book):

(defun read-64 (buf)
  (let ((u 0))
    (setf (ldb (byte 8 56) u) (aref buf 7))
    (setf (ldb (byte 8 48) u) (aref buf 6))
    (setf (ldb (byte 8 40) u) (aref buf 5))
    (setf (ldb (byte 8 32) u) (aref buf 4))
    (setf (ldb (byte 8 24) u) (aref buf 3))
    (setf (ldb (byte 8 16) u) (aref buf 2))
    (setf (ldb (byte 8 8) u) (aref buf 1))
    (setf (ldb (byte 8 0) u) (aref buf 0))
    u))

(defun read-32 (buf)
   (let ((u 0))
    (setf (ldb (byte 8 24) u) (aref buf 3))
    (setf (ldb (byte 8 16) u) (aref buf 2))
    (setf (ldb (byte 8 8) u) (aref buf 1))
    (setf (ldb (byte 8 0) u) (aref buf 0))
    u))

(defun read-16 (buf)
  (let ((u 0))
    (setf (ldb (byte 8 8) u) (aref buf 1))
    (setf (ldb (byte 8 0) u) (aref buf 0))
    u))

Now I can write (read-uint64 (subseq *buffer* 8 16)) to get length of msg. Thanks for all the help.

You could use userial , available from Quicklisp.

However, I would be looking very hard for a way to eliminate the need to keep two definition places synchronized (one on the C++, one on the Lisp side).

Edit: Here is what I had in mind. I have only made a few very shallow tests, so no guarantees. In particular, I have not tested with C++ output, and you will most likely have to adjust a lot for alignment.

(defstruct header
  msg-type
  length)

;; Msg-type might be best handled with an enum unserializer:
;; (make-enum-unserializer :msg-type (:foo :bar)), but I don't know
;; what your values are.

(defstruct sample-msg
  msg-header
  field-1
  field-2
  field-3
  field-4)

;; You might need to use a different serializer for msg-type for
;; alignment.

(make-slot-serializer (:header header (make-header))
  :int msg-type
  :uint64 length)

(make-vector-serializer :vector-256-char :uint8 256)

;; I have no idea how a boolean is serialized and aligned on the C++
;; side, so I'll just use :boolean for field-3 here as a first
;; attempt.

(make-slot-serializer (:sample-msg sample-msg (make-sample-msg))
  :header msg-header
  :vector-256-char field-1
  :boolean field-2
  :float64 field-3
  :some-other field-4)

;; You can serialize and unserialize now:

(serialize :sample-msg some-sample-msg)

(rewind-buffer)

(unserialize :sample-msg)

;; Userial operates on an adjustable vector with fill-pointer in the
;; special variable *buffer*, so you'll need to fill that with content
;; from wherever you read that from.

(with-buffer (read-my-content)
  (unserialize :sample-msg))

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM