First commit.

This commit is contained in:
Philippe Pittoli 2023-02-07 09:20:13 +01:00
commit 448b56ee3a
9 changed files with 403 additions and 0 deletions

7
.gitignore vendored Normal file
View File

@ -0,0 +1,7 @@
/docs/
/lib/
/.shards/
*.dwarf
old
shard.lock
*.log

8
makefile Normal file
View File

@ -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)

20
shard.yml Normal file
View File

@ -0,0 +1,20 @@
name: ipc
version: 0.1.0
authors:
- Philippe Pittoli <karchnu@karchnu.fr>
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

37
src/bindings.cr Normal file
View File

@ -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

60
src/cbor.cr Normal file
View File

@ -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

134
src/high-level-bindings.cr Normal file
View File

@ -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

59
src/json.cr Normal file
View File

@ -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

3
src/main.cr Normal file
View File

@ -0,0 +1,3 @@
require "./bindings.cr"
require "./message.cr"
require "./high-level-bindings.cr"

75
src/message.cr Normal file
View File

@ -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