From 448b56ee3aff9cfa9c429b39ced63a1087389edb Mon Sep 17 00:00:00 2001 From: Philippe Pittoli Date: Tue, 7 Feb 2023 09:20:13 +0100 Subject: [PATCH] First commit. --- .gitignore | 7 ++ makefile | 8 +++ shard.yml | 20 ++++++ src/bindings.cr | 37 ++++++++++ src/cbor.cr | 60 +++++++++++++++++ src/high-level-bindings.cr | 134 +++++++++++++++++++++++++++++++++++++ src/json.cr | 59 ++++++++++++++++ src/main.cr | 3 + src/message.cr | 75 +++++++++++++++++++++ 9 files changed, 403 insertions(+) create mode 100644 .gitignore create mode 100644 makefile create mode 100644 shard.yml create mode 100644 src/bindings.cr create mode 100644 src/cbor.cr create mode 100644 src/high-level-bindings.cr create mode 100644 src/json.cr create mode 100644 src/main.cr create mode 100644 src/message.cr diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f95ce6e --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +/docs/ +/lib/ +/.shards/ +*.dwarf +old +shard.lock +*.log diff --git a/makefile b/makefile new file mode 100644 index 0000000..129cfbf --- /dev/null +++ b/makefile @@ -0,0 +1,8 @@ +doc: + crystal docs + +DOC_HTTPD_ACCESS_LOGS ?= /tmp/access.log +DOC_HTTPD_ADDRESS ?= 127.0.0.1 +DOC_HTTPD_PORT ?= 9000 +serve-doc: + darkhttpd docs/ --addr $(DOC_HTTPD_ADDRESS) --port $(DOC_HTTPD_PORT) --log $(DOC_HTTPD_ACCESS_LOGS) diff --git a/shard.yml b/shard.yml new file mode 100644 index 0000000..7759e92 --- /dev/null +++ b/shard.yml @@ -0,0 +1,20 @@ +name: ipc +version: 0.1.0 + +authors: + - Philippe Pittoli + +description: | + High-level Crystal bindings to libipc. + +crystal: 1.7.1 + +dependencies: + cbor: + git: https://git.baguette.netlib.re/Baguette/crystal-cbor + branch: master + +libraries: + libipc: ">= 0.1" + +license: ISC diff --git a/src/bindings.cr b/src/bindings.cr new file mode 100644 index 0000000..444062d --- /dev/null +++ b/src/bindings.cr @@ -0,0 +1,37 @@ +@[Link("ipc")] +lib LibIPC + enum EventType + Error # Self explanatory. + Connection # New user. + Disconnection # User disconnected. + MessageRx # Message received. + MessageTx # Message sent. + Timer # Timeout in the poll(2) function. + External # Message received from a non IPC socket. + SwitchRx # Switch subsystem: message received. + SwitchTx # Switch subsystem: message send. + end + + fun init = ipc_context_init(Void**) : LibC::Int + fun deinit = ipc_context_deinit(Void**) : Void + + fun service_init = ipc_service_init(Void*, LibC::Int*, LibC::Char*, LibC::UInt16T) : LibC::Int + fun connect_service = ipc_connect_service(Void*, LibC::Int*, LibC::Char*, LibC::UInt16T) : LibC::Int + + # Context EventType index fd buffer buflen + fun wait = ipc_wait_event(Void*, UInt8*, LibC::UInt64T*, LibC::Int*, UInt8*, LibC::UInt64T*) : LibC::Int + + # Sending a message NOW. + # WARNING: doesn't wait the fd to become available. + fun write = ipc_write(Void*, LibC::Int, UInt8*, LibC::UInt64T) : LibC::Int + # Sending a message (will wait the fd to become available for IO operations). + fun schedule = ipc_schedule(Void*, LibC::Int, UInt8*, LibC::UInt64T) : LibC::Int + + fun read = ipc_read_fd(Void*, LibC::Int, UInt8*, LibC::UInt64T*) + fun timer = ipc_context_timer(Void*, LibC::Int) + + # Closing connections. + fun close = ipc_close(Void*, LibC::UInt64T) : LibC::Int + fun close_fd = ipc_close_fd(Void*, LibC::Int) : LibC::Int + fun close_all = ipc_close_all(Void*) : LibC::Int +end diff --git a/src/cbor.cr b/src/cbor.cr new file mode 100644 index 0000000..188f632 --- /dev/null +++ b/src/cbor.cr @@ -0,0 +1,60 @@ +require "cbor" +require "./main.cr" + +# IPC::CBOR is the root class for all exchanged messages (using CBOR). +# IPC::CBOR inherited classes have a common 'type' class attribute, +# which enables to find the right IPC::CBOR+ class given a TypedMessage's type. + +# All transfered messages are typed messages. +# TypedMessage = u8 type (= IPC::CBOR+ class type) + CBOR content. +# 'CBOR content' being a serialized IPC::CBOR+ class. + +# Conventionally, IPC::CBOR+ classes have a 'handle' method to process incoming messages. +class IPC::CBOR + include ::CBOR::Serializable + + class_property type = -1 + property id : ::CBOR::Any? + + def type + @@type + end + + macro message(id, type, &block) + class {{id}} < ::IPC::CBOR + include ::CBOR::Serializable + + @@type = {{type}} + + {{yield}} + end + end +end + +class IPC + # Schedule messages contained into IPC::CBOR+. + def schedule(fd : Int32, message : IPC::CBOR) + typed_msg = IPCMessage::TypedMessage.new message.type.to_u8, message.to_cbor + schedule fd, typed_msg + end + + def write(fd : Int32, message : IPC::CBOR) + typed_msg = IPCMessage::TypedMessage.new message.type.to_u8, message.to_cbor + write fd, typed_msg + end +end + +# CAUTION: only use this method on an Array(IPC::CBOR.class). +class Array(T) + def parse_ipc_cbor(message : IPCMessage::TypedMessage) : IPC::CBOR? + message_type = find &.type.==(message.type) + + payload = message.payload + + if message_type.nil? + raise "invalid message type (#{message.type})" + end + + message_type.from_cbor payload + end +end diff --git a/src/high-level-bindings.cr b/src/high-level-bindings.cr new file mode 100644 index 0000000..8a1dcff --- /dev/null +++ b/src/high-level-bindings.cr @@ -0,0 +1,134 @@ +class IPC + # Reception buffer with a big capacity. + # Allocated once. + @reception_buffer = Array(UInt8).new 2_000_000 + @reception_buffer_len : LibC::UInt64T = 2_000_000 + + class Event + property type : LibIPC::EventType + property index : LibC::UInt64T + property fd : Int32 + property message : Array(UInt8)? = nil + + def initialize(t : UInt8, @index, @fd, buffer, buflen) + @type = LibIPC::EventType.new t + if buflen > 0 + # Array -> Pointer -> Slice -> Array + @message = buffer.to_unsafe.to_slice(buflen).to_a + end + end + end + + def initialize + @context = Pointer(Void).null + LibIPC.init(pointerof(@context)) + at_exit { deinit } + end + + # Closes all connections then remove the structure from memory. + def deinit + LibIPC.deinit(pointerof(@context)) + end + + def service_init(name : String) : Int + fd = uninitialized Int32 + if LibIPC.service_init(@context, pointerof(fd), name, name.size) != 0 + raise "oh noes, 'service_init' iz brkn" + end + fd + end + + def connect(name : String) : Int32 + fd = uninitialized Int32 + if LibIPC.connect_service(@context, pointerof(fd), name, name.size) != 0 + raise "oh noes, 'connect_service' iz brkn" + end + fd + end + + def timer(value : LibC::Int) + LibIPC.timer(@context, value) + end + + def write(fd : Int, string : String) + self.write(fd, string.to_unsafe, string.size.to_u64) + end + + def write(fd : Int, buffer : UInt8*, buflen : UInt64) + if LibIPC.write(@context, fd, buffer, buflen) != 0 + raise "oh noes, 'write' iz brkn" + end + end + + def write(fd : Int32, buffer : Bytes) + self.write(fd, buffer.to_unsafe, buffer.size.to_u64) + end + + def read(fd : Int32) : Slice(UInt8) + buffer : Bytes = Bytes.new 2000000 + size = buffer.size.to_u64 + LibIPC.read(@context, fd, buffer.to_unsafe, pointerof(size)) + buffer[0..size - 1] + end + + def schedule(fd : Int32, string : String) + self.schedule(fd, string.to_unsafe, string.size.to_u64) + end + + def schedule(fd : Int32, buffer : Array(UInt8), buflen : Int32) + self.schedule(fd, buffer.to_unsafe, buflen.to_u64) + end + + def schedule(fd : Int32, buffer : Bytes) + self.schedule(fd, buffer.to_unsafe, buffer.size.to_u64) + end + + def schedule(fd : Int32, buffer : UInt8*, buflen : UInt64) + if LibIPC.schedule(@context, fd, buffer, buflen) != 0 + raise "oh noes, 'schedule' iz brkn" + end + end + + def close(index : LibC::UInt64T) + if LibIPC.close(@context, index) != 0 + raise "Oh noes, 'close index' iz brkn" + end + end + + def close(fd : LibC::Int) + if LibIPC.close_fd(@context, fd) != 0 + raise "Oh noes, 'close fd' iz brkn" + end + end + + def close + if LibIPC.close_all(@context) != 0 + raise "Oh noes, 'close all' iz brkn" + end + end + + def wait : IPC::Event + eventtype : UInt8 = 0 + index : LibC::UInt64T = 0 + fd : Int32 = 0 + buflen = @reception_buffer_len + ret = LibIPC.wait(@context, + pointerof(eventtype), + pointerof(index), + pointerof(fd), + @reception_buffer.to_unsafe, + pointerof(buflen)) + + if ret != 0 + raise "Oh noes, 'wait' iz brkn" + end + + Event.new(eventtype, index, fd, @reception_buffer, buflen) + end + + def loop(&block : Proc(IPC::Event, Nil)) + ::loop do + yield wait + end + end +end diff --git a/src/json.cr b/src/json.cr new file mode 100644 index 0000000..3513461 --- /dev/null +++ b/src/json.cr @@ -0,0 +1,59 @@ +require "json" + +# IPC::JSON is the root class for all exchanged messages (using JSON). +# IPC::JSON inherited classes have a common 'type' class attribute, +# which enables to find the right IPC::JSON+ class given a TypedMessage's type. + +# All transfered messages are typed messages. +# TypedMessage = u8 type (= IPC::JSON+ class type) + JSON content. +# 'JSON content' being a serialized IPC::JSON+ class. + +# Conventionally, IPC::JSON+ classes have a 'handle' method to process incoming messages. +class IPC::JSON + include ::JSON::Serializable + + class_property type = -1 + property id : ::JSON::Any? + + def type + @@type + end + + macro message(id, type, &block) + class {{id}} < ::IPC::JSON + include ::JSON::Serializable + + @@type = {{type}} + + {{yield}} + end + end +end + +class IPC + # Schedule messages contained into IPC::JSON+. + def schedule(fd : Int32, message : IPC::JSON) + typed_msg = IPCMessage::TypedMessage.new message.type.to_u8, message.to_json + schedule fd, typed_msg + end + + def write(fd : Int32, message : IPC::JSON) + typed_msg = IPCMessage::TypedMessage.new message.type.to_u8, message.to_json + write fd, typed_msg + end +end + +# CAUTION: only use this method on an Array(IPC::JSON.class). +class Array(T) + def parse_ipc_json(message : IPCMessage::TypedMessage) : IPC::JSON? + message_type = find &.type.==(message.type) + + payload = String.new message.payload + + if message_type.nil? + raise "invalid message type (#{message.type})" + end + + message_type.from_json payload + end +end diff --git a/src/main.cr b/src/main.cr new file mode 100644 index 0000000..56c9695 --- /dev/null +++ b/src/main.cr @@ -0,0 +1,3 @@ +require "./bindings.cr" +require "./message.cr" +require "./high-level-bindings.cr" diff --git a/src/message.cr b/src/message.cr new file mode 100644 index 0000000..88ac6b7 --- /dev/null +++ b/src/message.cr @@ -0,0 +1,75 @@ +# TODO: tests. + +# Serialization (and deserialization) doesn't refer to IPC format. +# IPC serialization format: 'length + value' +# IPCMessage serialization: 'value' +# 'Value' is: +# - simply the message payload for UntypedMessage +# - type (u8) + payload for TypedMessage +module IPCMessage + class UntypedMessage + property payload : Bytes + + def initialize(string : String) + @payload = Bytes.new string.to_unsafe, string.size + end + + def initialize(@payload) + end + + def self.deserialize(payload : Bytes) : UntypedMessage + IPCMessage::UntypedMessage.new payload + end + + def serialize + @payload + end + end + + # WARNING: you can only have up to 256 types. + class TypedMessage < UntypedMessage + property type : UInt8? = nil + + def initialize(@type, string : String) + super string + end + + def initialize(@type, payload) + super payload + end + + def initialize(payload) + super payload + end + + def self.deserialize(bytes : Bytes) : TypedMessage? + if bytes.size == 0 + nil + else + type = bytes[0] + IPCMessage::TypedMessage.new type, bytes[1..] + end + end + + def serialize + bytes = Bytes.new(1 + @payload.size) + type = @type + bytes[0] = type.nil? ? 0.to_u8 : type + bytes[1..].copy_from @payload + bytes + end + end +end + +# Send both typed and untyped messages. +class IPC + def schedule(fd : Int32, m : (IPCMessage::TypedMessage | IPCMessage::UntypedMessage)) + payload = m.serialize + schedule fd, payload + end + + def write(fd : Int32, m : (IPCMessage::TypedMessage | IPCMessage::UntypedMessage)) + payload = m.serialize + write fd, payload + end +end