Christian Eltzschig
co-CEO, co-Founder
Christian Eltzschig is a driving force behind ekxide. He kicked off his tech journey with an apprenticeship as an IT specialist and later delved into mathematics during his academic pursuits. Notably, at TU Berlin, he developed a 3D engine for visualizing mathematical constructs - a tangible blend of theory and practice.
Since 2017, Christian has been actively involved in the development of the open-source project iceoryx, contributing his expertise at ITK Engineering, Bosch, and Apex.AI. With a Rust-centric approach, he co-founded the ekxide IO GmbH to steer the evolution of iceoryx2, rewriting iceoryx from scratch in Rust alongside his co-founder, Mathias Kraus.
Christian's commitment extends beyond code; he firmly believes in the pivotal role of open-source in safety-critical applications. He sees Rust as a promising language for the future of such systems. Furthermore, Christian has made significant contributions to zero-copy inter-process communication innovations, evident in multiple patents he has been a part of.
Beyond the technical realm, Christian envisions the iceoryx project being applied in space and aiding robots in exploration - making the once-impossible, routine. He actively collaborates with the open-source community, fostering innovation and exploring synergies with other projects.
For Christian, iceoryx is more than a project - it's a conduit for collaborative exploration, pushing the boundaries of technology. With his unique blend of technical skills and a deep commitment to open source, Christian is a key player in ekxide's mission to redefine possibilities in safety-critical software.
Christian, also known as 'elfenpiff' on GitHub, is like an elf in the coding world - you could hear the piff of an elf - elfenpiff - every time he closes a pull request!
Announcing iceoryx2 v0.4.0
Christian Eltzschig - 28/09/2024
What Is iceoryx2
iceoryx2 is a service-based inter-process communication (IPC) library designed to make communication between processes as fast as possible - like Unix domain sockets or message queues, but orders of magnitude faster and easier to use. It also comes with advanced features such as circular buffers, history, event notifications, publish-subscribe messaging, and a decentralized architecture with no need for a broker.
Release v0.4.0
With today's iceoryx2 v0.4.0 release, we've achieved many of our milestones and are now close to feature parity with its predecessor, the trusty old iceoryx.
If you're wondering why you should choose iceoryx2 over iceoryx, here are some of its next-gen features:
- No more need for a central daemon like RouDi.
- It's up to 10 times faster thanks to a new, more efficient architecture.
- More dynamic than ever—no more compile-time memory pool configuration.
- Advanced Quality of Service (QoS) settings.
- Extremely modular: every aspect of iceoryx2 can be customized, allowing future support for GPUs, FPGAs, and more.
- Completely decentralized and even more robust.
- A restructured API and resource management system that enables a zero-trust policy for true zero-copy communication in the future.
- Language bindings for C and C++ with CMake and Bazel support right out of the box. Python and other languages are coming soon.
- Upcoming gateways to enable network communication via protocols like zenoh, DDS, MQTT, and more.
With this new release, we're faster than ever. On some platforms, latency is even under 100ns! Be sure to check out our iceoryx2 benchmarks and try them out on your platform.
Highlights
Here are some of the feature highlights in v0.4.0:
-
C and C++ language bindings: We've added a range of new examples to help you get started with the supported languages.
- Plus, there's a shiny new website: https://iceoryx2.readthedocs.io, where we're building a detailed introduction to inter-process communication, true zero-copy, and iceoryx2. Whether you're just getting started or looking to fine-tune every feature to your needs, it's all in one place.
-
New build systems: C and C++ bindings come with support for:
-
Bazel & CMake
-
colcon: We're working on
iceoryx2_rmw
, which will be unveiled at ROSCon 2024 during Mathias' talk: "iceoryx2: A Journey to Becoming a First-Class RMW Alternative."
-
-
iceoryx2 nodes: Nodes are the central entity handling all process-local resources, such as ports, and are key to monitoring other processes and nodes. If a process crashes, the others will clean up resources as soon as the issue is detected.
-
Command-line debugging and introspection: Meet iox2. If you want to see which services or nodes are running—or if you're curious about the details of a service or process—this is your go-to tool.
-
Runtime-sized services: We've overcome the compile-time memory configuration limitation of iceoryx1. If you want to send a dynamic-sized typed array (like a Rust slice), you can set up the service and publisher with a runtime worst-case size. If that's insufficient, you can create a new publisher with a larger array size.
-
Advanced service and port configurations: For specialized use cases, like SIMD or FPGA, you can define custom alignments for your service's payload.
-
User-defined service attributes: You can now set custom key-value pairs to tag services with additional properties. Check out the iceoryx2 Deep Dive - Service Attributes for more details.
-
iceoryx2 Domains: Separate multiple processes into domains, ensuring they don't interfere with one another.
-
Custom User Header: There's an interface for defining a custom header that is sent with every sample.
-
32-bit support: iceoryx2 now runs on 32-bit machines, and long-term, we aim to support mixed-mode zero-copy communication between 32-bit and 64-bit processes.
-
Placement new for iceoryx2-bb-containers: Since iceoryx2 can handle gigabytes of data, we provide a mechanism to loan memory and perform in-place initialization—something akin to C++'s placement new.
Sneak Peak: Mission Control
Our upcoming Mission Control Center will provide deep introspection and debugging for your iceoryx2 system. You’ll be able to monitor the CPU, memory, and I/O load of every process. You can also view the frequency and content of message samples, inspect individual nodes with their running services, and visualize how nodes and services are connected—all in real time.
Stay tuned for its release at the end of this year! |
What’s Next?
Check out our Roadmap.
In the next release, we plan to focus on:
- Finalizing the C/C++ language bindings: Most of the Rust functionality works, but features like dynamic slice support and service attributes are still in progress.
- Event multiplexing: We’re extending
Node::wait()
for more streamlined event handling.- This will come with advanced integrated events, such as push notifications for system events like process crashes or service changes.
- Expect detailed examples and documentation.
- Services with dynamic payloads: You won’t need to define a fixed payload size for services with slices anymore. We’ll introduce an allocation algorithm that acquires more shared memory as needed, and it’ll be customizable.
- Health monitoring: With iceoryx2 nodes, we can detect dead nodes and clean up their resources. The next step is to actively notify processes when a sender or receiver dies.
- Expanded documentation: Inter-process communication can be complex, so we’re working on extending the docs to provide a gentle introduction, explain iceoryx2's features in detail, and offer a step-by-step tutorial on making the most of it.
ekxide's iceoryx2 Deep Dive - Service Attributes
Christian Eltzschig - 15/06/2024
With this article, we are starting a new series where we dive deep into the current development progress of iceoryx2. We'll explain the newest features, the problems they solve, and the cool new things you can build with iceoryx2. This series will let you see what our open-source company, ekxide IO GmbH, is currently working on. It also allows us to collect feedback from the community, plan and refine new features, and discover interesting projects using iceoryx2.
For those who are not familiar with iceoryx2: it is an open-source library that handles reliable and incredibly fast inter-process communication, suitable for applications ranging from desktops to mission-critical systems like cars or medical devices. So, if you need to send data or signal events from process A to process B, iceoryx2 is your go-to library.
https://github.com/eclipse-iceoryx/iceoryx2
The Problem
What problem does iceoryx2 solve? It is a service-oriented inter-process middleware where you can create services with a name and send data or signals to other processes.
Assume you are building a robot with multiple camera sensors. You may have several services producing video streams, publishing them on services like "camera:front," "camera:back," "camera:left," and so on. In iceoryx2, you could implement it like this:
let service = zero_copy::Service::new(ServiceName::new("camera:front")?)
.publish_subscribe::<CameraImage>()
.create()?;
let publisher = service.publisher().create()?;
loop {
let sample = publisher.loan_uninit()?;
sample.write_payload(get_camera_image()).send()?;
}
If you now write a process that requires a video stream to detect obstacles and perform an emergency brake if necessary, you could easily subscribe to such a service like this:
let service = zero_copy::Service::new(ServiceName::new("camera:front")?)
.publish_subscribe::<CameraImage>()
.open()?;
let subscriber = service.subscriber().create()?;
loop {
if let Some(image) = subscriber.receive()? {
perform_some_processing(*image);
}
}
But what if you have another service that wants to create high-quality snapshots of the scenes the robot captures? It would be advantageous if the service used a 4k camera. Or, if the robot is moving at high speed, it would be preferable to use only services where the camera produces images at a rate of 60 frames per second.
Where could we store this information for the consumers of the data? We could add this in the header of the message, but for efficiency, this is not the best place to write this information repeatedly, especially when it never changes.
The solution is service attributes.
Service Attributes
Service attributes are key-value pairs that remain constant during the service's lifetime. They can be set when the service is created and can be read by any participant and during service discovery.
let service = zero_copy::Service::new(ServiceName::new("camera:front")?)
.publish_subscribe::<CameraImage>()
.create_with_attributes(
&AttributeSpecifier::new()
.define("camera-resolution", "1920x1080")
.define("frames-per-second", "60"),
)?;
When you perform a service discovery, you immediately see what attributes are set and can select the right service that satisfies all your requirements. It also allows you to acquire additional information about the counterpart.
let service = zero_copy::Service::new(ServiceName::new("camera:front")?)
.publish_subscribe::<CameraImage>()
.open()?;
for attribute in service.attributes().iter() {
println!("{} = {}", attribute.key(), attribute.value());
}
Another option is to define the service attributes as requirements. For instance, it could be important that a specific key is defined without considering the value, or that a specific key-value pair is defined. Let's go back to our example and assume that we do not care about the camera resolution as long as it is defined as an attribute, but we need 60 frames per second to perform an emergency brake.
let service = zero_copy::Service::new(ServiceName::new("camera:front")?)
.publish_subscribe::<CameraImage>()
.open_with_attributes(
&AttributeVerifier::new()
.require_key("camera-resolution")
.require("frames-per-second", "60"),
)?;
One of our internal iceoryx2 use cases for service attributes is gateways. When you want to forward a message from iceoryx2 via the MQTT protocol, you may want to use a different service name. Sometimes it is even mandatory since the protocol does not support the iceoryx2 naming scheme, like Some/IP. With service attributes, we can now define the translation for the gateway directly in the service and specify that the "camera:front" service should map to the MQTT service "camera/front."
let service = zero_copy::Service::new(ServiceName::new("camera:front")?)
.publish_subscribe::<CameraImage>()
.create_with_attributes(
&AttributeSpecifier::new()
.define("mqtt-service-name", "camera/front"),
)?;
What's Next?
One of the things that are missing is a mechanism to make the attributes more scalable. We need to come up with a configuration file or another innovative solution that allows us to define attributes, such as the iceoryx2 service to MQTT service mapping, in a more centralized manner rather than hardcoding them in the code. Let's see what we can come up with — we'll keep you posted.
Happy Hacking...
Want to Unlock Performance and Clarity? Use Strong Types!
Christian Eltzschig - 01/06/2024
C++ and Rust are strongly typed languages, meaning whenever you declare a variable, you must either explicitly specify the variable
Rust |
C++ |
|
|
or you do it implicitly by assigning a value of a specific type.
Rust |
C++ |
|
|
The type of the variable also comes with a specific contract. An integer can
only contain numbers but not floating point values like 3.14
or a string. The
size of the integer also defines the range of numbers it can store.
An int8
can store numbers in the range of [-128; 127]
, and
an int16
offers the range of [-32768, 32767]
.
Strong Types Implementation
With strong types, we have a powerful tool in our hands.
When defining a function input argument as uint32_t
, we never
need to verify that the user accidentally gave us a negative number or a string. We never
need to test this case. All subsequent calls can rely on the fact that this is
indeed an integer, and the API of the function clearly communicates that it is
expecting an integer and nothing else.
But we can also add semantic contracts to the type. Let's take a POSIX user name for example. The POSIX standard states that it is allowed to consist of:
- lower and upper ASCII letters (
a-zA-Z
), - digits (
0-9
) - and period (
.
), underscore (_
) and hyphen (-
)
Furthermore, it is not allowed to start with a hyphen.
All of those constraints can be baked into a type called UserName
. The basic
idea is that the UserName
cannot be created directly with a constructor.
However, it comes with a static factory method called create
- the Rust
idiomatic approach is to call such a method new
- which takes a string-literal
as input argument and checks whether it meets the above requirements.
This method returns an optional value that contains either a
valid UserName
object or nothing when the user name contract is violated.
Rust |
C++ |
|
|
The UserName
type now guarantees that it always contains a semantically
correct user name since creating a UserName
with invalid characters is
impossible.
Fewer Bugs, More Expressive APIs
Let's assume we now have a collection of strong types like UserName
,
GroupName
, and FileName
, and we can use them directly in our API. We
introduce two functions. The first function do_stuff
uses the new and shiny
strong types, but the second one buggy_stuff
uses the underlying string of
those types directly.
Rust |
C++ |
|
|
The first issue of the buggy_stuff
function is that the API is not expressive.
Should the reader be a group name, a user name, or maybe it is even something completely different? This
requires some detailed documentation for the user. And if you know the saying,
"The compiler doesn't read comments and neither do I.", you understand this is not a perfect solution.
Furthermore, it can be easily misused. When either variable names are not expressive enough or the function is
called directly with values, they can be easily mixed up. Also, what happens when you refactor and swap or
replace some arguments? Maybe the storage
shall be no longer a file name but now a database name. How do
you ensure that all function usages are ported?
Additionally, the implementer of buggy_stuff
is now responsible for verifying all arguments! Whenever
this function is called, we must check that the reader
, writer
, and storage
are semantically correct.
When this is not the case, we must handle it and inform the user.
Of course, we could move this check into a free function and use it whenever we expect a type with a semantic
contract. However, this can be easily forgotten due to refactoring.
The error handling introduces further overhead! We have to write additional tests to check if the error handling works correctly and the function's users require extra logic to handle potential errors. And this extra logic needs to be tested as well!
Finally, it will cost us performance. Why? Because whenever one has a call chain where those arguments are
forwarded to other functions, especially when they are not directly under your control, the same semantical
verification has to be performed. Over and over again. And those function calls one uses to implement
buggy_stuff
may also fail for semantically incorrect values. This has to be handled and
tested again. This costs even more performance!
All of those problems, performance costs, additional tests on the user and implementer side, additional error handling, and an unexpressive API can be avoided when we integrate the semantical check into the type itself so that we have a guarantee that it always contains a valid value.
Summary
Using strong types like UserName
or FileName
comes with a bunch of benefits.
Firstly, the API becomes more expressive and we no longer require extensive documentation to convey all the semantic details.
Strong types can also prevent parameter mixups in functions with multiple arguments.
Furthermore, they also minimize the lines of code by ensuring validity through the type system. With this, they decrease the need for error handling both within the implementation and for the user's code.
Even the performance may improve when the semantic content is centrally verified and not in every function repeatedly....
Announcing iceoryx2 v0.3.0
Christian Eltzschig - 18/04/2024
Today, I am happy to announce iceoryx2 v0.3.0. The release comes with cool new features, improved documentation and additional examples.
So here we go.
Features & Improvements
Communication Between Docker Containers
With iceoryx2, you can establish zero-copy communication between multiple docker containers.
Since iceoryx2 is just using shared memory and some files stored in /tmp/iceoryx2
for communication, all you have to do is to share /tmp/iceoryx2
and /dev/shm
with all your docker containers, and everything works.
We created a docker example that explains all the little details.
Note: All paths and naming schemes can be configured via a config file. For more details and documentation, take a look at the iceoryx2 default configuration
Services Without Lifetime Parameters
In v0.2, every endpoint and payload sample in iceoryx2 had generic lifetime parameters.
The idea was that a service is, from a high-level point of view, a factory of endpoints like publishers or subscribers.
Those endpoints were again factories for samples.
For instance, a subscriber "produces" a sample when the call my_subscriber::receive()
returns the received sample.
Under the hood, the service created system resources that had to live as long as any endpoints or samples were active.
Therefore, the service must live as long as an endpoint and an endpoint at least as long as a sample.
But you run into trouble when you would like to store samples from different endpoints, with different lifetimes, in a Vec
to cache them for later.
But thanks to Arc
, which allowed us to share the ownership of those resources, the problem is gone, and the API is now much easier to use.
Sending Complex Data
Usually, you want to send more complex data than just arrays of integers via shared memory.
This is why iceoryx2-bb-containers becomes public API with this iceoryx2 release.
It comes with compile-time fixed-size versions of Queue
, Vec
, and ByteString
that can be used as building blocks for transmission types.
use iceoryx2_bb_container::{
byte_string::FixedSizeByteString, vec::FixedSizeVec,
};
#[derive(Debug, Default)]
#[repr(C)]
pub struct ComplexDataType {
text: FixedSizeByteString<8>,
vec_of_data: FixedSizeVec<u64, 4>,
}
If you would like to see a complete working example, take a look at the complex data types example
Note: I know defining the capacity at compile-time is not yet perfect. However, we are working on runtime dynamic data types based on relocatable containers that will be available with an upcoming release.
Then, you can define your transmission types without any compile-time restrictions.
use iceoryx2_bb_container::vec::RelocatableVec;
#[derive(Debug, Default)]
#[repr(C)]
pub struct ComplexDataType {
some_data: RelocatableVec<u64>,
other_data: RelocatableVec<f32>,
}
Improved Event Communication
The event messaging pattern is iceoryx2's basic building block for async operations and push notifications. The new release is based on the ported C++ iceoryx1 bitset, which solves the problem of a limited queue buffer on the listener side.
When a Notifier
sends notifications with their EventId
s in a busy loop, the buffer is filled quickly, and other Notifiers cannot send their notifications to the Listener
.
A bitset where the Notifier
flips a bit corresponding to the EventId
solves the issue.
Furthermore, we refined the API so that you can choose to take either one EventId
after another in a loop:
for event_id in listener.blocking_wait_one()? {
println!("event was triggered with id: {:?}", event_id);
}
or to acquire all received EventId
s at once
listener.blocking_wait_all(|id| {
println!("event was triggered with id: {:?}", id);
})?;
Bug Fixes
A big thanks to our first users, who started playing around with iceoryx2 and helped us refine the API and iron out the edges.
We fixed a ton of bugs!
Most bugs were connected to the decentralized nature of iceoryx2, and we encountered some races when endpoints connected and disconnected at a high frequency. However, many additional concurrent stress tests now give us the confidence that they stay fixed.
The communication mechanism did not raise bug reports, mainly because they were proven-in-use for years in iceoryx1 and were just ported to iceoryx2.
Performance Improvements
We took some time to improve the performance of iceoryx2 even further and realized that we hit a limit where the performance becomes very architecture/OS dependent. Look at the iceoryx2 readme where we provide an overview of our results.
What Comes Next
Take a look at our Roadmap.
In Q2 we want to focus:
- on our first language binding to C
- introduce advanced monitoring so that manual cleanups are no longer required when an application has crashed
- on sending serializable structs via shared memory so that any kind of type - without restriction - can be sent
Why You Should Go Open-Source
Christian Eltzschig - 09/04/2024
In this article, I would like to analyze ideas and misconceptions floating around when it comes to open-source software and provide arguments on why an open-source strategy can be a success story.
So, if you are a developer trying to convince your manager or you are the one who is currently exploring and deciding on the open-source strategy, this article is for you. It is based on the experience we had with our open-source project iceoryx, which led to the founding of our open-source company exkide.
What Is iceoryx
iceoryx is a zero-copy inter-process communication framework written in C++/Rust. With ekxide, we founded a company that backs up the open-source project, provides commercial support and products around it, and supports the community with new features, bug fixes, and tooling.
Let's Ask The Business Team To Open-Source Our Software
But What About Our Unique Selling Point?
One counterargument often used is, "When we open-source our product, we give away one unique selling point!". When you encounter this statement, it is worth digging deeper and understanding what unique thing the business people are trying to sell.
In our case, it is not zero-copy inter-process communication by itself. This is necessary for most users in our domain, and there are already multiple custom closed-source solutions. If you would like to develop software for a car, a robot, or a medical device, you most likely require fast inter-process communication to deploy a robust and efficient system.
What makes our product unique is the upcoming support for zero-copy communication on GPUs or across hypervisor partitions. So, a strategy could be to open-source the core functionality, design and architecture that supports closed-source extensions, and promote them. This is not a new idea! It is the Open-core model.
But Wouldn't It Help Our Competitors?
When a company open-sources software, other competitors can get inspired by it, use it, or fork and extend it. So they save time, create their product faster, and save money at the same time. But this is only partially true. If you develop the product, you have the expertise and the innovation on your side. Couple this with a well-defined Open-Core business strategy, and you will be ahead. The underlying issue may be something else. Question yourself, Do you rather want to succeed, or do you want your competitors to fail? If you want to succeed, what is the problem when your competitors also succeed?
If your software is well received and others start using it, you may even save money since there is a common interest in solving bugs and introducing new improvements and features. This may lead to cooperation between you and the community or they start contributing and you get new features for free. This is how iceoryx got FreeRTOS platform support - thank you, NXP Semiconductors, for the contribution.
However, the most significant benefits are that you can define the ecosystem around your open-source software, grow horizontally, and gain customers in domains you have never thought of. When you invest time in the community, you also have access to a talent pool that may support you with bug fixes and features where you lack some expertise. Big thanks goes to our community for helping with the bazel support in iceoryx where we still have some knowledge gaps.
But Wouldn't It Slow Down Development?
This argument comes more from the developer's side. The misconception is that the maintenance overhead with additional testing, documentation, and a clean code base is not worth the effort. It is required for the community to consider your project, but it is also vital for yourself. The proof-of-concept phase may be a bit slower, but as soon as this goes into production, documentation, clean code base, and test concept are not optional. Leaving out documentation, tests and code clean-ups will produce faster results only in a brief period. After that, every additional feature will be more expensive and time-consuming just because of technical debt.
With poor tests, how are you sure the new features work and do not break existing ones? With poor documentation, how can other developers (including your future self) use the software as intended and continue developing it further?
Isn't it something we all strive for as developers? Create something that is used also in the future and continues evolving. To create something that matters and persists. If you are confident that your problem is a problem worth solving, others may think the same. When the solution is closed-source, they will develop their custom solution, and your code may end up lost in some big mono-repo.
I believe the benefits you gain from open-sourcing outweigh the extra effort of additional documentation and community work. Not only does the community benefit from it, but so do you too - when others will later continue the work and ideas you have started or stumble across all the edge cases you never thought of.
But How To Sell This To Our Investors?
If you are working in a startup, your investors expect you to increase the value of your startup. How can this be achieved when providing software as open-source? So the question is: why offer software as open source? Let's explore this in more detail.
Why Offer Software As Open Source
Set The Standard
Let's go back to the unique selling point. When your software is the only open-source software that solves a particular problem, you can set the standard. When others provide proprietary solutions, they have to compete with you, and they need to offer more advanced features worth the extra costs one has to pay for their product - depending on your open-source license and business model. Furthermore, with your solution, you can steer the ecosystem and define the architecture and how your business will evolve around it.
When your project succeeds and reaches a critical mass, companies will require their supplier to be compatible with your solution to simplify their software stack. By offering an open-source solution, your product eliminates the need for a closed-source solution, and you have the advantage of knowing your solution in great detail.
Horizontal Growth
When your project flourishes, it will be used by users from different domains with use cases you never thought of in the first place. We were surprised when we noticed that fintech libraries, desktop applications, or computer-aided medical procedures are using iceoryx, which was initially developed for the embedded safety-certified automotive market.
These new use cases also moved the frontier of what we strive for with iceoryx. Support for new operating systems was requested, and they wanted to communicate via low-latency network protocols or become much more dynamic than the typical static safety-critical automotive use case. Now, we will work on language bindings for C#, Lua, Python, or Java, all things you usually do not encounter in a car but on a desktop machine. So, after some time, we gained users we didn't even think of when we started the project.
When starting a new project with a proof-of-concept phase, developers began to use our software. Since it is open source with a permissive license (Apache 2.0), they did not have to ask their manager for an evaluation license but could just use it.
When their product matured enough, they required our advanced features. But sometimes some pieces were missing, and who do you call when you have problems with the software you are using - the company behind it! In this case exkide.
Our core use case benefits from this as well. We also use those new tools and features in our workday life. Windows is not running in cars, but it is nice to have the freedom to develop on any machine you want - from Windows, macOS, Linux to FreeBSD - thanks to the platform support community members like ecal requested.
Increase Your Talentpool
Getting talented developers is a challenge many companies are facing, especially when your particular field requires detailed domain knowledge. The onboarding of new hires can be time-consuming, and the lack of experience in a specialized field may slow down the development process.
When building up a community that is welcoming and fun to interact with, you create a talent pool at your fingertips. You observe who is shining with their contributions, and you have the advantage that they do not need the entire onboarding process since they already know the product. We also received applications just because people wanted to work with us on our iceoryx project.
Even when employees leave the company, they stick around the project and are often supportive when you are in need. A good community has your back when it comes to an urgent bug fix or support in a feature where you lack the expertise.
Special Thanks
I want to conclude the article by saying thank you to the company that gave us a cozy place to start our development of iceoryx and supported us when we open-sourced our project, Robert Bosch GmbH.
In the last 3 years, Apex.AI has been our home. The atmosphere there did let us thrive and brought more features into iceoryx.
Without them, iceoryx wouldn't have been the success story it is today.
The primary goal of our new company ekxide is to focus now entirely on iceoryx, boost feature development further, and provide commercial support, training, and custom feature development for our community, users, and customers....