// Requests sent by an EventLoopHandle to be serviced by // the handle's associated EventLoop. enum Request { // See EventLoop::add_connection
AddConnection(
sys::Pipe, Box<dyn Driver + Send>,
Option<mpsc::Sender<Result<Token>>>,
), // See EventLoop::shutdown
Shutdown, // See EventLoop::wake_connection
WakeConnection(Token),
}
// EventLoopHandle is a cloneable external reference // to a running EventLoop, allowing registration of // new client and server connections, in addition to // requesting the EventLoop shut down cleanly. #[derive(Clone, Debug)] pubstruct EventLoopHandle {
waker: Arc<Waker>,
requests: RequestQueueSender<Request>,
}
impl EventLoopHandle { pubfn bind_client<C: Client + 'static>(
&self,
connection: sys::Pipe,
) -> Result<Proxy<<C as Client>::ServerMessage, <C as Client>::ClientMessage>> where
<C as Client>::ServerMessage: Serialize + Debug + AssociateHandleForMessage + Send,
<C as Client>::ClientMessage: DeserializeOwned + Debug + AssociateHandleForMessage + Send,
{ let (handler, mut proxy) = make_client::<C>()?; let driver = Box::new(FramedDriver::new(handler)); let r = self.add_connection(connection, driver);
trace!("EventLoop::bind_client {r:?}");
r.map(|token| {
proxy.connect_event_loop(self.clone(), token);
proxy
})
}
// Register a server connection and wait until the EventLoop has installed // it. This preserves a registration barrier for callers that may issue // dependent RPCs immediately after binding. pubfn bind_server<S: Server + Send + 'static>(
&self,
server: S,
connection: sys::Pipe,
) -> Result<()> where
<S as Server>::ServerMessage: DeserializeOwned + Debug + AssociateHandleForMessage + Send,
<S as Server>::ClientMessage: Serialize + Debug + AssociateHandleForMessage + Send,
{ let handler = make_server::<S>(server); let driver = Box::new(FramedDriver::new(handler)); let r = self.add_connection(connection, driver);
trace!("EventLoop::bind_server {r:?}");
r.map(|_| ())
}
// Non-blocking server bind. The pipe pair is fully connected at creation, // so the client can write immediately — data is buffered by the OS kernel // until the event loop registers the server end. If registration fails, // the server pipe is dropped and the client gets a broken-pipe error. pubfn bind_server_async<S: Server + Send + 'static>(
&self,
server: S,
connection: sys::Pipe,
) -> Result<()> where
<S as Server>::ServerMessage: DeserializeOwned + Debug + AssociateHandleForMessage + Send,
<S as Server>::ClientMessage: Serialize + Debug + AssociateHandleForMessage + Send,
{ let handler = make_server::<S>(server); let driver = Box::new(FramedDriver::new(handler)); let r = self.add_connection_async(connection, driver);
trace!("EventLoop::bind_server_async {r:?}");
r
}
// Fire-and-forget connection registration. Returns immediately without // waiting for the event loop to process the request. fn add_connection_async(
&self,
connection: sys::Pipe,
driver: Box<dyn Driver + Send>,
) -> Result<()> {
assert_not_in_event_loop_thread(); self.requests
.push(Request::AddConnection(connection, driver, None))
.map_err(|_| {
debug!("EventLoopHandle::add_connection_async send failed");
io::ErrorKind::ConnectionAborted
})?; self.waker.wake()
}
// Register a new connection, blocking until the event loop acknowledges it. fn add_connection(
&self,
connection: sys::Pipe,
driver: Box<dyn Driver + Send>,
) -> Result<Token> {
assert_not_in_event_loop_thread(); let (tx, rx) = mpsc::channel(); self.requests
.push(Request::AddConnection(connection, driver, Some(tx)))
.map_err(|_| {
debug!("EventLoopHandle::add_connection send failed");
io::ErrorKind::ConnectionAborted
})?; self.waker.wake()?;
rx.recv().map_err(|_| {
debug!("EventLoopHandle::add_connection recv failed");
io::ErrorKind::ConnectionAborted
})?
}
// Signal EventLoop to shutdown. Causes EventLoop::poll to return Ok(false). fn shutdown(&self) -> Result<()> { self.requests.push(Request::Shutdown).map_err(|_| {
debug!("EventLoopHandle::shutdown send failed");
io::ErrorKind::ConnectionAborted
})?; self.waker.wake()
}
// Signal EventLoop to wake connection specified by `token` for processing. pub(crate) fn wake_connection(&self, token: Token) { ifself.requests.push(Request::WakeConnection(token)).is_ok() { iflet Err(e) = self.waker.wake() {
error!("wake_connection: waker.wake() failed: {e}");
}
}
}
}
// EventLoop owns all registered connections, and is responsible for calling each connection's // `handle_event` or `handle_wake` any time a readiness or wake event associated with that connection is // produced. struct EventLoop {
poll: Poll,
events: Events,
waker: Arc<Waker>,
name: String,
connections: Slab<Connection>,
requests: Arc<RequestQueue<Request>>,
}
const EVENT_LOOP_INITIAL_CLIENTS: usize = 64; // Initial client allocation, exceeding this will cause the connection slab to grow. const EVENT_LOOP_EVENTS_PER_ITERATION: usize = 256; // Number of events per poll() step, arbitrary limit.
// Return a cloneable handle for controlling the EventLoop externally. fn handle(&mutself) -> EventLoopHandle {
EventLoopHandle {
waker: self.waker.clone(),
requests: self.requests.new_sender(),
}
}
// Register a connection and driver. fn add_connection(
&mutself,
connection: sys::Pipe,
driver: Box<dyn Driver + Send>,
) -> Result<Token> { ifself.connections.len() == self.connections.capacity() {
trace!("{}: connection slab full, insert will allocate", self.name);
} let entry = self.connections.vacant_entry(); let token = Token(entry.key());
assert_ne!(token, WAKE_TOKEN); let connection = Connection::new(connection, token, driver, self.poll.registry())?;
debug!("{}: {:?}: new connection", self.name, token);
entry.insert(connection);
Ok(token)
}
// Step EventLoop once. Call this in a loop from a dedicated thread. // Returns false if EventLoop is shutting down. // Each step may call `handle_event` on any registered connection that // has received readiness events from the poll wakeup. fn poll(&mutself) -> Result<bool> { loop { let r = self.poll.poll(&mutself.events, None); match r {
Ok(()) => break,
Err(ref e) if interrupted(e) => continue,
Err(e) => return Err(e),
}
}
for event inself.events.iter() { match event.token() {
WAKE_TOKEN => {
debug!("{}: WAKE: wake event, will process requests", self.name);
}
token => {
debug!("{}: {:?}: connection event: {:?}", self.name, token, event); let done = iflet Some(connection) = self.connections.get_mut(token.0) { match connection.handle_event(event, self.poll.registry()) {
Ok(done) => {
debug!("{}: connection {:?} done={}", self.name, token, done);
done
}
Err(e) => {
debug!("{}: {:?}: connection error: {:?}", self.name, token, e); true
}
}
} else { // Spurious event, log and ignore.
debug!( "{}: {:?}: token not found in slab: {:?}", self.name, token, event
); false
}; if done {
debug!("{}: {:?}: done, removing", self.name, token); letmut connection = self.connections.remove(token.0); iflet Err(e) = connection.shutdown(self.poll.registry()) {
debug!( "{}: EventLoop drop - closing connection for {:?} failed: {:?}", self.name, token, e
);
}
}
}
}
}
// If the waker was signalled there may be pending requests to process. whilelet Some(req) = self.requests.pop() { match req {
Request::AddConnection(pipe, driver, tx) => {
debug!("{}: EventLoop: handling add_connection", self.name); let r = self.add_connection(pipe, driver); iflet Some(tx) = tx {
tx.send(r).expect("EventLoop::add_connection");
} elseiflet Err(e) = r {
debug!( "{}: EventLoop: async add_connection failed: {:?}", self.name, e
);
}
}
Request::Shutdown => {
debug!("{}: EventLoop: handling shutdown", self.name); return Ok(false);
}
Request::WakeConnection(token) => {
debug!( "{}: EventLoop: handling wake_connection {:?}", self.name, token
); let done = iflet Some(connection) = self.connections.get_mut(token.0) { match connection.handle_wake(self.poll.registry()) {
Ok(done) => done,
Err(e) => {
debug!("{}: {:?}: connection error: {:?}", self.name, token, e); true
}
}
} else { // Spurious wake, log and ignore.
debug!( "{}: {:?}: token not found in slab: wake_connection", self.name, token
); false
}; if done {
debug!("{}: {:?}: done (wake), removing", self.name, token); letmut connection = self.connections.remove(token.0); iflet Err(e) = connection.shutdown(self.poll.registry()) {
debug!( "{}: EventLoop drop - closing connection for {:?} failed: {:?}", self.name, token, e
);
}
}
}
}
}
Ok(true)
}
}
impl Drop for EventLoop { fn drop(&mutself) {
debug!("{}: EventLoop drop", self.name); for (token, connection) in &mutself.connections {
debug!( "{}: EventLoop drop - closing connection for {:?}", self.name, token
); iflet Err(e) = connection.shutdown(self.poll.registry()) {
debug!( "{}: EventLoop drop - closing connection for {:?} failed: {:?}", self.name, token, e
);
}
}
debug!("{}: EventLoop drop done", self.name);
}
}
// Connection wraps an interprocess connection (Pipe) and manages // receiving inbound and sending outbound buffers (and associated handles, if any). // The associated driver is responsible for message framing and serialization. struct Connection {
io: sys::Pipe,
token: Token,
interest: Option<Interest>,
inbound: sys::ConnectionBuffer,
outbound: sys::ConnectionBuffer,
driver: Box<dyn Driver + Send>,
}
// Connections are always interested in READABLE. clear_readable is only // called when the connection is in the process of shutting down. fn clear_readable(&mutself, registry: &Registry) -> Result<()> { self.update_registration(
registry, self.interest.and_then(|i| i.remove(Interest::READABLE)),
)
}
// Connections toggle WRITABLE based on the state of the `outbound` buffer. fn set_writable(&mutself, registry: &Registry) -> Result<()> { self.update_registration(
registry,
Some( self.interest
.map_or_else(|| Interest::WRITABLE, |i| i.add(Interest::WRITABLE)),
),
)
}
// Update connection registration with the current readiness event interests. fn update_registration(
&mutself,
registry: &Registry,
new_interest: Option<Interest>,
) -> Result<()> { // Note: Updating registration always triggers a writable event with NamedPipes, so // it's important to skip updating registration when the set of interests hasn't changed. if new_interest != self.interest {
trace!( "{:?}: updating readiness registration old={:?} new={:?}", self.token, self.interest,
new_interest
); self.interest = new_interest; iflet Some(interest) = self.interest {
registry.reregister(&mutself.io, self.token, interest)?;
} else {
registry.deregister(&mutself.io)?;
}
}
Ok(())
}
// Handle readiness event. Errors returned are fatal for this connection, resulting in removal from the EventLoop connection list. // The EventLoop will call this for any connection that has received an event. fn handle_event(&mutself, event: &Event, registry: &Registry) -> Result<bool> {
debug!("{:?}: handling event {:?}", self.token, event);
assert_eq!(self.token, event.token()); let done = if event.is_readable() { self.recv_inbound()?
} else {
trace!("{:?}: not readable", self.token); false
}; self.flush_outbound()?; ifself.send_outbound(registry)? { // Hit EOF during send return Ok(true);
}
debug!( "{:?}: handling event done (recv done={}, outbound={})", self.token,
done, self.outbound.is_empty()
); let done = done && self.outbound.is_empty(); // If driver is done and outbound is clear, unregister connection. if done {
trace!("{:?}: driver done, clearing read interest", self.token); self.clear_readable(registry)?;
}
Ok(done)
}
// Handle wake event. Errors returned are fatal for this connection, resulting in removal from the EventLoop connection list. // The EventLoop will call this to clear the outbound buffer for any connection that has received a wake event. fn handle_wake(&mutself, registry: &Registry) -> Result<bool> {
debug!("{:?}: handling wake", self.token); self.flush_outbound()?; ifself.send_outbound(registry)? { // Hit EOF during send return Ok(true);
}
debug!("{:?}: handling wake done", self.token);
Ok(false)
}
fn recv_inbound(&mutself) -> Result<bool> { // If the connection is readable, read into inbound and pass to driver for processing until all ready data // has been consumed. loop {
trace!("{:?}: pre-recv inbound: {:?}", self.token, self.inbound); let r = self.io.recv_msg(&mutself.inbound); match r {
Ok(0) => {
trace!( "{:?}: recv EOF unprocessed inbound={}", self.token, self.inbound.is_empty()
); return Ok(true);
}
Ok(n) => {
trace!("{:?}: recv bytes: {}, process_inbound", self.token, n); let r = self.driver.process_inbound(&mutself.inbound);
trace!("{:?}: process_inbound done: {:?}", self.token, r); match r {
Ok(done) => { if done { return Ok(true);
}
}
Err(e) => {
debug!( "{:?}: process_inbound error: {:?} unprocessed inbound={}", self.token,
e, self.inbound.is_empty()
); return Err(e);
}
}
}
Err(ref e) if would_block(e) => {
trace!("{:?}: recv would_block: {:?}", self.token, e); return Ok(false);
}
Err(ref e) if interrupted(e) => {
trace!("{:?}: recv interrupted: {:?}", self.token, e); continue;
}
Err(e) => {
debug!("{:?}: recv error: {:?}", self.token, e); return Err(e);
}
}
}
}
fn flush_outbound(&mutself) -> Result<()> { // Enqueue outbound messages to the outbound buffer, then try to write out to connection. // There may be outbound messages even if there was no inbound processing, so always attempt // to enqueue and flush.
trace!("{:?}: flush_outbound", self.token); let r = self.driver.flush_outbound(&mutself.outbound);
trace!("{:?}: flush_outbound done: {:?}", self.token, r); iflet Err(e) = r {
debug!("{:?}: flush_outbound error: {:?}", self.token, e); return Err(e);
}
Ok(())
}
fn send_outbound(&mutself, registry: &Registry) -> Result<bool> { // Attempt to flush outbound buffer. If the connection's write buffer is full, register for WRITABLE // and complete flushing when associated notitication arrives later. while !self.outbound.is_empty() { let r = self.io.send_msg(&mutself.outbound); match r {
Ok(0) => {
trace!("{:?}: send EOF", self.token); return Ok(true);
}
Ok(n) => {
trace!("{:?}: send bytes: {}", self.token, n);
}
Err(ref e) if would_block(e) => {
trace!( "{:?}: send would_block: {:?}, setting write interest", self.token,
e
); // Register for write events. self.set_writable(registry)?; break;
}
Err(ref e) if interrupted(e) => {
trace!("{:?}: send interrupted: {:?}", self.token, e); continue;
}
Err(e) => {
debug!("{:?}: send error: {:?}", self.token, e); return Err(e);
}
}
trace!("{:?}: post-send: outbound {:?}", self.token, self.outbound);
} // Outbound buffer flushed, clear registration for WRITABLE. ifself.outbound.is_empty() {
trace!("{:?}: outbound empty, clearing write interest", self.token); self.clear_writable(registry)?;
}
Ok(false)
}
}
impl Drop for Connection { fn drop(&mutself) {
debug!("{:?}: Connection drop", self.token);
}
}
// Driver only has a single implementation, but must be hidden behind a Trait object to // hide the varying FramedDriver sizes (due to different `T` values). trait Driver { // Handle inbound messages. Returns true if Driver is done; this will trigger Connection removal and cleanup. fn process_inbound(&mutself, inbound: &mut sys::ConnectionBuffer) -> Result<bool>;
// Length-delimited connection framing and (de)serialization is handled by the inbound and outbound processing. // Handlers can then process message Requests and Responses without knowledge of serialization or // handle remoting. impl<T> Driver for FramedDriver<T> where
T: Handler,
T::In: DeserializeOwned + Debug + AssociateHandleForMessage,
T::Out: Serialize + Debug + AssociateHandleForMessage,
{ // Caller passes `inbound` data, this function will trim any complete messages from `inbound` and pass them to the handler for processing. fn process_inbound(&mutself, inbound: &mut sys::ConnectionBuffer) -> Result<bool> {
debug!("process_inbound: {inbound:?}");
// Repeatedly call `decode` as long as it produces items, passing each produced item to the handler to action. #[allow(unused_mut)] whilelet Some(mut item) = self.codec.decode(&mut inbound.buf)? { if item.has_associated_handle() { // On Unix, dequeue a handle from the connection and update the item's handle. #[cfg(unix)]
{ let new = inbound
.pop_handle()
.expect("inbound handle expected for item"); unsafe { item.set_local_handle(new.take()) };
} // On Windows, the deserialized item contains the correct handle value, so // convert it to an owned handle on the item. #[cfg(windows)]
{
assert!(inbound.pop_handle().is_none()); unsafe { item.set_local_handle() };
}
}
self.handler.consume(item)?;
}
Ok(false)
}
// Caller will try to write `outbound` to associated connection, queuing any data that can't be transmitted immediately. fn flush_outbound(&mutself, outbound: &mut sys::ConnectionBuffer) -> Result<()> {
debug!("flush_outbound: {:?}", outbound.buf);
// Repeatedly grab outgoing items from the handler, passing each to `encode` for serialization into `outbound`. whilelet Some(mut item) = self.handler.produce()? { let handle = if item.has_associated_handle() { #[allow(unused_mut)] letmut handle = item.take_handle(); // On Windows, the handle is transferred by duplicating it into the target remote process. #[cfg(windows)] unsafe {
item.set_remote_handle(handle.send_to_target()?);
}
Some(handle)
} else {
None
};
self.codec.encode(item, &mut outbound.buf)?; iflet Some(handle) = handle { // `outbound` retains ownership of the handle until the associated // encoded item in `outbound.buf` is sent to the remote process.
outbound.push_handle(handle);
}
}
Ok(())
}
}
// Client setup and registration. let client = EventLoopThread::new("test-client".to_string(), None, || {}, || {})
.expect("client EventLoopThread"); let client_handle = client.handle();
let client_pipe = unsafe { sys::Pipe::from_raw_handle(client_pipe) }; let client_proxy = client_handle
.bind_client::<TestClientImpl>(client_pipe)
.expect("client bind_client");
(server, client, client_proxy)
}
// Verify basic EventLoopThread functionality works. Create a server and client EventLoopThread, then send // a single message from the client to the server and wait for the expected response. #[test] fn basic() {
init(); let (server, client, client_proxy) = setup();
// RPC message from client to server. let response = client_proxy.call(TestServerMessage::TestRequest); let response = response.expect("client response");
assert_eq!(response, TestClientMessage::TestResponse);
// Same as `basic`, but shut down server before client. #[test] fn basic_reverse_drop_order() {
init(); let (server, client, client_proxy) = setup();
// RPC message from client to server. let response = client_proxy.call(TestServerMessage::TestRequest); let response = response.expect("client response");
assert_eq!(response, TestClientMessage::TestResponse);
let clone = client_proxy.clone(); let response = clone.call(TestServerMessage::TestRequest);
response.expect_err("sending to a dropped ClientHandler");
}
#[test] fn basic_event_loop_thread_callbacks() {
init(); let (start_tx, start_rx) = mpsc::channel(); let (stop_tx, stop_rx) = mpsc::channel();
// Verify client can send before the server event loop processes the // async AddConnection request. Data is buffered by the OS kernel. #[test] fn async_bind_server() {
init();
// Gate the server event loop: it blocks in after_start until we release it. let (gate_tx, gate_rx) = mpsc::channel(); let server = EventLoopThread::new( "test-server".to_string(),
None, move || {
gate_rx.recv().ok();
},
|| {},
)
.expect("server EventLoopThread"); let server_handle = server.handle();
let (server_pipe, client_pipe) = sys::make_pipe_pair().expect("make_pipe_pair");
server_handle
.bind_server_async(TestServerImpl {}, server_pipe)
.expect("bind_server_async");
// Client setup — bind_client is synchronous and uses its own event loop. let client = EventLoopThread::new("test-client".to_string(), None, || {}, || {})
.expect("client EventLoopThread"); let client_handle = client.handle(); let client_pipe = unsafe { sys::Pipe::from_raw_handle(client_pipe) }; let client_proxy = client_handle
.bind_client::<TestClientImpl>(client_pipe)
.expect("client bind_client");
// Spawn call() on a background thread — it blocks waiting for // the response, which can't arrive until the gate is released. let call_thread = thread::spawn(move || client_proxy.call(TestServerMessage::TestRequest));
// Wait for the client event loop to serialize the request and // write it to the pipe. The client event loop is running // freely and should flush within microseconds; 200ms is a // generous margin. There is no portable, non-intrusive way // to observe the write without racing mio's registration.
thread::sleep(std::time::Duration::from_millis(200));
// Release the server gate. The server event loop now processes // AddConnection (registering the pipe with mio) and immediately // sees the buffered client data as a readability event.
gate_tx.send(()).ok();
// Model the Linux AudioIPC startup ordering: // // * The client callback event loop runs a startup callback before it // polls. On Linux that startup callback performs a remote // PromoteThreadToRealTime RPC over the main AudioIPC connection. // * Stream init then binds a callback server on that same callback // event loop. // * Stream start may synchronously invoke a cubeb data callback on the // server, which performs an RPC to the client's callback server. // // If callback bind returns before the callback event loop has really // registered the server connection, start can race ahead while the // callback event loop is still in its startup promotion RPC. The server // then waits for a callback response and can no longer service the // queued promotion response, producing a deadlock. `bind_server` must // remain a registration barrier; callers that can tolerate fire-and- // forget semantics should use `bind_server_async` explicitly. let server_callback =
EventLoopThread::new("test-server-callback".to_string(), None, || {}, || {})
.expect("server callback EventLoopThread"); let (server_callback_pipe, client_callback_pipe) =
sys::make_pipe_pair().expect("callback make_pipe_pair"); let callback_proxy = server_callback
.handle()
.bind_client::<StartupRaceCallbackClient>(server_callback_pipe)
.expect("server callback bind_client");
for call in calls { let response = call
.join()
.expect("call thread panicked")
.expect("response");
assert_eq!(response, TestClientMessage::TestResponse);
}
drop(clients);
drop(server);
}
// Verify that dropping the server before processing the async // AddConnection propagates as a transport error to the client. #[test] fn async_bind_server_drops_before_processing() {
init(); let server = EventLoopThread::new("test-server".to_string(), None, || {}, || {})
.expect("server EventLoopThread"); let server_handle = server.handle();
let (server_pipe, client_pipe) = sys::make_pipe_pair().expect("make_pipe_pair");
server_handle
.bind_server_async(TestServerImpl {}, server_pipe)
.expect("bind_server_async");
// Drop the server immediately — queued AddConnection may not be processed.
drop(server);
let client = EventLoopThread::new("test-client".to_string(), None, || {}, || {})
.expect("client EventLoopThread"); let client_handle = client.handle();
let client_pipe = unsafe { sys::Pipe::from_raw_handle(client_pipe) }; let client_proxy = client_handle
.bind_client::<TestClientImpl>(client_pipe)
.expect("client bind_client");
let response = client_proxy.call(TestServerMessage::TestRequest);
response.expect_err("server dropped, should get transport error");
drop(client);
}
// Verify that an async AddConnection that is never processed // (server shuts down before reaching it in the queue) propagates // as a transport error to the client. #[test] fn async_bind_server_never_processed() {
init();
// Gate the server event loop so we can control the order of // requests in the queue. let (gate_tx, gate_rx) = mpsc::channel(); let server = EventLoopThread::new( "test-server".to_string(),
None, move || {
gate_rx.recv().ok();
},
|| {},
)
.expect("server EventLoopThread"); let server_handle = server.handle().clone();
// Queue Shutdown BEFORE AddConnection. The event loop processes // requests in FIFO order, so it will shut down without ever // handling AddConnection. The unprocessed server pipe is // dropped with the queue, and the client sees a broken-pipe error.
server_handle.shutdown().expect("shutdown");
let (server_pipe, client_pipe) = sys::make_pipe_pair().expect("make_pipe_pair");
server_handle
.bind_server_async(TestServerImpl {}, server_pipe)
.expect("bind_server_async");
let client = EventLoopThread::new("test-client".to_string(), None, || {}, || {})
.expect("client EventLoopThread"); let client_handle = client.handle();
let client_pipe = unsafe { sys::Pipe::from_raw_handle(client_pipe) }; let client_proxy = client_handle
.bind_client::<TestClientImpl>(client_pipe)
.expect("client bind_client");
let response = client_proxy.call(TestServerMessage::TestRequest);
response.expect_err("server never processed AddConnection, should get transport error");
drop(client);
}
}
Messung V0.5 in Prozent
¤ Dauer der Verarbeitung: 0.17 Sekunden
(vorverarbeitet am 2026-08-27)
¤
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.