Operations

33 min read

25 min read

33 min read

15 min read

16 min read

13 min read

28 min read

14 min read

14 min read

An operation definition must contain a name (the operation’s name), a return type and zero or more parameter definitions.

For example:

Slice
module M
{
struct TimeOfDay
{
short hour; // 0 - 23
short minute; // 0 - 59
short second; // 0 - 59
}
interface Clock
{
TimeOfDay getTime();
void setTime(TimeOfDay time);
}
}

The getTime operation has a return type of TimeOfDay and the setTime operation has a return type of void. You must use void to indicate that an operation returns no value — there is no default return type for Slice operations.

An operation can have one or more input parameters. For example:

Slice
module M
{
interface CircadianRhythm
{
void setSleepPeriod(TimeOfDay startTime, TimeOfDay stopTime);
}
}

Note that the parameter name is mandatory. You cannot omit the parameter name, so the following is in error:

Slice
module M
{
interface CircadianRhythm
{
void setSleepPeriod(TimeOfDay, TimeOfDay); // Error!
}
}

By default, parameters are sent from the client to the server, that is, they are input parameters. To pass a value from the server to the client, you can use an output parameter, indicated by the out keyword. For example, an alternative way to define the getTime operation in the Clock interface would be:

Slice
void getTime(out TimeOfDay time);

This achieves the same thing but uses an output parameter instead of the return value. As with input parameters, you can use multiple output parameters:

Slice
module M
{
interface CircadianRhythm
{
void setSleepPeriod(TimeOfDay startTime, TimeOfDay stopTime);
void getSleepPeriod(out TimeOfDay startTime, out TimeOfDay stopTime);
}
}

If you have both input and output parameters for an operation, the output parameters must follow the input parameters:

Slice
void changeSleepPeriod(
TimeOfDay startTime,
TimeOfDay stopTime,
out TimeOfDay prevStartTime,
out TimeOfDay prevStopTime);
void changeSleepPeriod(
out TimeOfDay prevStartTime,
out TimeOfDay prevStopTime, // Error
TimeOfDay startTime,
TimeOfDay stopTime);

Slice does not support parameters that are both input and output parameters.

An operation's return value and parameters may be declared as optional to indicate that a program can leave their values unset. Parameters not declared as optional are known as required parameters; a program must supply legal values for all required parameters. In the discussion below, we use parameter to refer to input parameters, output parameters, and return values.

A unique, non-negative integer tag must be assigned to each optional parameter:

Slice
optional(3) bool example(optional(2) string name, out optional(1) int value);

The scope of a tag is limited to its operation and has no effect on other operations.

Optional fields and required fields can appear in any order in your class definition. You can also assign tags in any order.

An operation's signature can include any combination of required and optional parameters, but output parameters still must follow input parameters:

Slice
bool example(
string name,
optional(3) string referrer,
out optional(1) string promo,
out int id);

Language mappings specify an API for passing optional parameters and testing whether a parameter is present.

Slice does not support any form of overloading of operations. For example:

Slice
interface CircadianRhythm
{
void modify(TimeOfDay startTime, TimeOfDay endTime);
void modify( TimeOfDay startTime, // Error
TimeOfDay endTime,
out timeOfDay prevStartTime,
out TimeOfDay prevEndTime);
}

Operations in the same interface must have different names, regardless of what type and number of parameters they have. This restriction exists because overloaded functions cannot sensibly be mapped to languages without built-in support for overloading.

An operation is idempotent when two successive invocations have the same effect as a single invocation. Operations that do not modify the state of the object, such as getTime in the Clock interface, are idempotent; so is setTime, even though it modifies the state. You mark such operations with the idempotent keyword:

Slice
interface Clock
{
idempotent TimeOfDay getTime();
idempotent void setTime(TimeOfDay time);
}

The idempotent keyword is useful because it allows the Ice runtime to be more aggressive when performing automatic retries to recover from errors. Specifically, Ice guarantees at-most-once semantics for operation invocations:

  • For normal (not idempotent) operations, the Ice runtime has to be conservative about how it deals with errors. For example, if a client sends an operation invocation to a server and then loses connectivity, there is no way for the client-side run time to find out whether the request it sent actually made it to the server. This means that the runtime cannot attempt to recover from the error by re-establishing a connection and sending the request a second time because that could cause the operation to be invoked a second time and violate at-most-once semantics; the runtime has no option but to report the error to the application.
  • For idempotent operations, on the other hand, the client-side runtime can attempt to re-establish a connection to the server and safely send the failed request a second time. If the server can be reached on the second attempt, everything is fine and the application never notices the (temporary) failure. Only if the second attempt fails need the runtime report the error back to the application. (The number of retries can be increased with an Ice configuration parameter.)

As we saw in the Client-Side C++ Mapping for Interfaces, for each operation on an interface, the generated proxy class contains 3 member functions for this operation. To invoke an operation, you call one of these functions on the proxy. For example, let’s take the generated code from the greeter example:

Slice
module VisitorCenter
{
interface Greeter
{
string greet(string name);
}
}

The proxy class generated from the Greeter interface, after removing extra details, is as follows:

C++
namespace VisitorCenter
{
class GreeterPrx : public Ice::Proxy<GreeterPrx, Ice::ObjectPrx>
{
public:
GreeterPrx(const Ice::CommunicatorPtr& communicator,
std::string_view proxyString);
// ...
std::string greet(std::string_view name,
const Ice::Context& context = Ice::noExplicitContext) const;
std::future<std::string> greetAsync(std::string_view name,
const Ice::Context& context = Ice::noExplicitContext) const;
std::function<void()> greetAsync(std::string_view name,
std::function<void(std::string)> response,
std::function<void(std::exception_ptr)> exception = nullptr,
std::function<void(bool)> sent = nullptr,
const Ice::Context& context = Ice::noExplicitContext) const;
// ...
};
}

Given a proxy to an object of type Greeter, the client can invoke the greet operation as follows:

C++
GreeterPrx greeter{communicator, "greeter:tcp -h localhost -p 4061"};
string greeting = greeter.greet("Alice"); // Get greeting via RPC

This code calls greet on the proxy class instance, which sends the request to the server, waits until the operation is complete, and then unmarshals the return value and returns it to the caller.

Because the return value is of type string, it is safe to ignore the return value. For example, the following code contains no memory leak:

C++
GreeterPrx greeter{communicator, "greeter:tcp -h localhost -p 4061"};
greeter.greet("Alice"); // Useless, but no leak

This is true for all mapped Slice types: you can safely ignore the return value of an operation, no matter what its type — return values are always returned by value. If you ignore the return value, no memory leak occurs because the destructor of the returned value takes care of deallocating memory as needed.

For each operation, the Slice compiler generates 3 member functions on the proxy class:

  • one “sync” function with the same name as the operation. When you call this function, your thread waits synchronously until the invocation completes. A successful invocation completes with a return value (which can be void), while an unsuccessful invocation completes with an exception.
  • two overloaded “async” functions, named <operation-name>Async. When you call these functions, your thread marshals the arguments to the function synchronously, but the remainder of this invocation is asynchronous, and the function returns immediately. You get the result (return value or exception) through an std::future or a callback depending on the async overload you selected. These async functions are described in more detail in Asynchronous Method Invocation (AMI) in C++.

Any operation invocation may throw a runtime exception and, if the operation has an exception specification, may also throw user exceptions. Suppose we have the following simple interface:

Slice
exception Tantrum
{
string reason;
}
interface Child
{
void askToCleanUp() throws Tantrum;
}

Slice exceptions are thrown as C++ exceptions, so you can simply enclose one or more operation invocations in a try-catch block:

C++
ChildPrx child = ...; // Get Child proxy...
try
{
child.askToCleanUp(); // Give it a try...
}
catch (const Tantrum& t)
{
cout << "The child says: " << t.reason << endl;
}

As we saw in the Server-Side C++ Mapping for Interfaces, for each operation on an interface, the generated skeleton class contains a pure virtual function with the same name.

For example, let’s take the generated code from the greeter example:

Slice
module VisitorCenter
{
interface Greeter
{
string greet(string name);
}
}

The skeleton class generated from the Greeter interface, after removing extra details, is as follows:

C++
namespace VisitorCenter
{
class Greeter : public virtual Ice::Object
{
public:
virtual std::string greet(std::string name,
const Ice::Current& current) = 0;
};
}

The greet function takes a string name and a Current, then returns a value of type std::string. This function should be implemented in your derived servant class with something like:

C++
class Chatbot : public VisitorCenter::Greeter
{
public:
std::string greet(std::string name, const Ice::Current&) override
{
ostringstream os;
os << "Hello, " << name << "!";
return os.str();
}
};

Each operation with the ["amd"] metadata is mapped to a pure virtual function with an Async suffix in the skeleton class. The AMD mapping replaces the default “sync” mapping for the operation. See Asynchronous Method Dispatch (AMD) in C++ for details.

To throw an exception from an operation implementation, you simply construct this exception and throw it. For example:

C++
void
MFile::write(Filesystem::Lines text, const Ice::Current&)
{
// Try to write the file contents here...
// Assume we are out of space...
if (error)
{
throw Filesystem::WriteException{"file too large"};
}
}

If you throw an arbitrary C++ exception (such as a std::logic_error), the Ice runtime catches the exception and then returns an UnknownException to the client.

If you throw an Ice runtime exception, such as MarshalException, the client receives an UnknownLocalException.

The server-side Ice runtime does not validate user exceptions thrown by an operation implementation to ensure they are compatible with the operation's Slice definition. Rather, Ice returns the user exception to the client, where the client-side runtime will validate the exception as usual and throws UnknownUserException for an unexpected exception type.

Asynchronous Method Invocation (AMI) is the term used to describe the client-side support for the asynchronous programming model. AMI supports both oneway and twoway requests, but unlike their synchronous counterparts, AMI requests never block the calling thread. When a client issues an AMI request, the Ice runtime hands the message off to the local transport buffer or, if the buffer is currently full, queues the request for later delivery. The application can then continue its activities and poll or wait for completion of the invocation, or receive a callback when the invocation completes.

AMI is transparent to the server: there is no way for the server to tell whether a client sent a request synchronously or asynchronously.

If an invocation throws an exception, the exception is reported by the exception callback or by the future, even if the actual error condition for the exception was encountered during the call to the Async function ("on the way out"). The advantage of this behavior is that all exception handling is located in the same place (instead of being present twice, once where you call the Async function, and again where you retrieve the result) .

There are two exceptions to this rule:

  • if you destroy the communicator and then make an asynchronous invocation, the Async function throws CommunicatorDestroyedException. This is necessary because, once the communicator is destroyed, its client thread pool is no longer available.
  • a call to an Async function can throw TwowayOnlyException. An Async function throws this exception if you call an operation that has a return value or out-parameters on a oneway proxy.

You can invoke operations via oneway proxies asynchronously, provided the operation has void return type, does not have any out-parameters, and does not throw user exceptions. If you call an Async function on a oneway proxy for an operation that returns values or throws a user exception, the Async function throws TwowayOnlyException.

With the callback API, the Ice runtime does not call the response callback on a oneway or datagram proxy: a successful invocation completes with the sent callback (see the Sent Callbacks section below). With the future-based API, the returned future is a future<void>, and this future is made ready when the invocation is sent.

The Async function with callback parameters returns a cancel function-object (a std::function<void()>). You can use this function-object to cancel the invocation, for example:

C++
EmployeesPrx e = ... // get an Employees proxy
auto cancel = e.getNameAsync(
99,
[](string name) { cout << "Employee name is: " << name << endl; });
cancel(); // no longer interested in this name

Calling this cancel function-object prevents a queued invocation from being sent or, if the invocation has already been sent, ignores a reply if the server sends one. This cancellation is purely local and has no effect on the server.

Canceling an invocation that has already completed has no effect. Otherwise, a canceled invocation is considered to be completed, meaning the exception callback (if provided) receives an Ice::InvocationCanceledException.

The future-based Async function allow you to poll for call completion. Polling is useful in a variety of cases. As an example, consider the following simple interface to transfer files from client to server:

Slice
interface FileTransfer
{
void send(int offset, ByteSeq bytes);
}

The client repeatedly calls send to send a chunk of the file, indicating at which offset in the file the chunk belongs. A naïve way to transmit a file would be along the following lines:

C++
FileHandle file = open(...);
FileTransferPrx ft = ...;
const int chunkSize = ...;
int offset = 0;
while (!file.eof())
{
ByteSeq bs;
bs = file.read(chunkSize); // Read a chunk
ft.send(offset, bs); // Send the chunk
offset += bs.size();
}

This works, but not very well: because the client makes synchronous calls, it writes each chunk on the wire and then waits for the server to receive the data, process it, and return a reply before writing the next chunk. This means that both client and server spend much of their time doing nothing — the client does nothing while the server processes the data, and the server does nothing while it waits for the client to send the next chunk.

Using asynchronous calls, we can improve on this considerably:

C++
FileHandle file = open(...);
FileTransferPrx ft = ...;
const int chunkSize = ...;
int offset = 0;
deque<future<void>> results;
const int numRequests = 5;
while (!file.eof())
{
ByteSeq bs;
bs = file.read(chunkSize);
// Send up to numRequests + 1 chunks asynchronously.
auto fut = ft.sendAsync(offset, bs);
offset += bs.size();
results.push_back(std::move(fut));
// Once there are more than numRequests, wait for the least
// recent one to complete.
while (results.size() > numRequests)
{
results.front().get();
results.pop_front();
}
}
// Wait for any remaining requests to complete.
while (!results.empty())
{
results.front().get();
results.pop_front();
}

With this code, the client sends up to numRequests + 1 chunks before it waits for the least recent one of these requests to complete. In other words, the client sends the next request without waiting for the preceding request to complete, up to the limit set by numRequests. In effect, this allows the client to "keep the pipe to the server full of data": the client keeps sending data, so both client and server continuously do work.

Obviously, the correct chunk size and value of numRequests depend on the bandwidth of the network as well as the amount of time taken by the server to process each request. However, with a little testing, you can quickly zoom in on the point where making the requests larger or queuing more requests no longer improves performance. With this technique, you can realize the full bandwidth of the link to within a percent or two of the theoretical bandwidth limit of a native socket connection.

When you call an Async function, the Ice runtime attempts to write the request to the client-side transport. If the transport can accept the request, it is sent synchronously, in the calling thread. Otherwise, the Ice runtime queues the request internally and sends it later, in the background.

With the callback API, you can pass a sent callback — a std::function<void(bool)> — as an additional argument. The Ice runtime calls this function when the request is accepted by the transport:

  • When the request is accepted synchronously, the Ice runtime calls the sent callback from the thread calling the Async function, and passes true as argument. Note that in this case the sent callback executes during the call to the Async function, before this function returns.
  • When the request is accepted asynchronously, the Ice runtime calls the sent callback from an Ice thread pool thread, and passes false as argument.

This is unlike the response and exception callbacks, which the Ice runtime always calls from an Ice thread pool thread.

If you set a custom executor in Ice::InitializationData::executor, this executor determines the thread that executes the response and exception callbacks, as well as the sent callback when the request is accepted asynchronously. The executor has no effect on a sent callback executed synchronously: the Ice runtime calls it from the thread calling the Async function, as described above.

For example:

C++
EmployeesPrx e = ...; // get an Employees proxy
e.getNameAsync(
99,
[](string name) { ... handle name ... },
[](exception_ptr ex) { ... handle exception ... },
[](bool sentSynchronously) { ... sent ... });

Since the sent callback can execute in the calling thread before the Async function returns, it must not attempt to acquire a lock that this thread already holds, as this would result in a deadlock with a non-recursive mutex.

Asynchronous method invocations never block the thread that calls the Async function: if the local transport cannot accept the request immediately, the Ice runtime queues the request internally for later transmission in the background.

This creates a potential problem: if a client sends many asynchronous requests at the time the server is too busy to keep up with them, the requests pile up in the client-side runtime until, eventually, the client runs out of memory.

The sent callback described in the previous section provides a way for you to implement flow control, by counting the number of requests that are queued: if that number exceeds some threshold, the client stops invoking more operations until some of the queued operations have drained out of the local transport.

For example:

C++
EmployeesPrx e = ...; // get an Employees proxy
e.getNameAsync(
99,
[](string name) { ... handle name ... },
[](exception_ptr ex) { ... handle exception ... },
[](bool) { ... increase sent counter ... });

The number of simultaneous synchronous requests a server is capable of supporting is determined by the number of threads in the server's thread pool. If all of the threads are busy dispatching long-running operations, then no threads are available to process new requests and therefore clients may experience an unacceptable lack of responsiveness.

Asynchronous Method Dispatch (AMD), the server-side equivalent of AMI, addresses this scalability issue. Using AMD, a server can receive a request but then suspend its processing in order to release the dispatch thread as soon as possible. When processing resumes and the results are available, the server sends a response explicitly using a callback object provided by the Ice runtime.

AMD is transparent to the client, that is, there is no way for a client to distinguish a request that, in the server, is processed synchronously from a request that is processed asynchronously.

In practical terms, an AMD operation typically queues the request data (i.e., the callback object and operation arguments) for later processing by an application thread (or thread pool). In this way, the server minimizes the use of dispatch threads and becomes capable of efficiently supporting thousands of simultaneous clients.

An alternate use case for AMD is an operation that requires further processing after completing the client's request. In order to minimize the client's delay, the operation returns the results while still in the dispatch thread, and then continues using the dispatch thread for additional work.

The easiest way to use AMD in C++ is to make your servant class derive from the async skeleton class generated by the Slice compiler. For example:

C++
// This servant uses AMD
class Chatbot : public VisitorCenter::AsyncGreeter
{
public:
// your implementation here
};

If you prefer to implement some operations asynchronously (with AMD) and other operations synchronously, you can add the ["amd"] metadata directive to the operations you want to implement with AMD and use the default skeleton class.

The metadata directive replaces synchronous dispatch on the default skeleton, that is, a particular operation implementation must use synchronous or asynchronous dispatch and cannot use both.

Consider the following Slice definitions:

Slice
interface Controller
{
["amd"] void startProcess();
int endProcess();
}

In this example, the startProcess of the default skeleton class uses asynchronous dispatch while endProcess uses synchronous dispatch.

With AMD, the skeleton’s pure virtual function is named <operation-name>Async. This function returns void and accepts the operation's in-parameters, followed by two callback parameters provided by the Ice runtime.

For example, suppose we have defined the following operation:

Slice
interface Example
{
string op(short s, out long l);
}

Operation op is mapped as follows on the async skeleton (AsyncExample):

C++
virtual void opAsync(
std::int16_t s,
std::function<void(std::string_view returnValue, std::int64_t l)> response,
std::function<void(std::exception_ptr)> exception,
const Ice::Current& current) = 0;

You would get the same signature on the default skeleton (Example) if you decorate op with ["amd"].

There are two processing contexts in which the logical implementation of an AMD operation may need to report an exception: the dispatch thread (the thread that receives the request), and the response thread (the thread that sends the response).

The implementation of the Async function in your servant class can throw an exception synchronously: it’s equivalent to calling the exception callback with this exception.

Since the asynchronous proxy callback API and the asynchronous dispatch API are similar, it is possible to implement an asynchronous dispatch by sending an asynchronous request to a proxy.

Continuing our example from the previous section, suppose our servant also holds a proxy to another object of the same type:

C++
class ExampleServant : public AsyncExample
{
public:
ExampleServant(ExamplePrx prx) : _other{std::move(prx)}
{
}
void opAsync(
std::int16_t s,
std::function<void(std::string_view, std::int64_t)> response,
std::function<void(std::exception_ptr)> exception,
const Ice::Current&) override
{
// Ice-supplied AMD response and exception callbacks are passed
// as AMI callbacks.
_other.opAsync(s, std::move(response), std::move(exception));
}
private:
const ExamplePrx _other;
};

An in parameter is mapped to a C++ parameter with the same name.

The type of the mapped C++ parameter depends on the Slice type and on the direction of the parameter: are you giving this parameter to Ice for marshaling (an outgoing value), or is Ice giving you this parameter after unmarshaling it (an incoming value)?

For incoming values, the mapped C++ type is always “by value”: Ice transfers these arguments to you, and you get full ownership. For outgoing values, Ice only needs to “borrow” the arguments while it marshals them synchronously into the payload of the request.

Slice Parameter TypeMapped C++ Parameter Type (Outgoing)Mapped C++ Parameter Type (Incoming, Always by Value)
byte, bool, int, short, long, float, double, enum E
By value: std::uint8_t, bool, std::int32_t, etc.
std::uint8_t, bool, std::int32_t, etc.
string
“view”: std::string_view or std::wstring_view
std::string or std::wstring
struct S, sequence<T> Seq, dictionary<K, V> Dict
Const reference: const S&
S, Seq, Dict
class C
Const reference of shared pointer: const CPtr&
CPtr (a shared pointer by value)
Greeter* (a proxy)
Const reference of optional: const std::optional<GreeterPrx>&
std::optional<GreeterPrx>

A Slice parameter is mapped to a parameter with the same name in synchronous proxy and skeleton functions. The type of the mapped C++ parameter is a non-const reference.

Consider the following example:

Slice
struct NumberAndString
{
int x;
string str;
}
sequence<string> StringSeq;
dictionary<long, StringSeq> StringTable;
interface ServerToClient
{
void op1(out int i, out float f, out bool b, out string s);
void op2(out NumberAndString ns, out StringSeq ss, out StringTable st);
void op3(out ServerToClient* proxy);
}

The Slice compiler generates a proxy class for this definition (we omit the async overloads):

C++
class ServerToClientPrx : public Ice::Proxy<ServerToClientPrx, Ice::ObjectPrx>
{
public:
void op1(int& i, float& f, bool& b, std::string& s, const Ice::Context& = Ice::noExplicitContext);
void op2(NumberAndString& ns, StringSeq& ss, StringTable& st, const Ice::Context& = Ice::noExplicitContext);
void op3(std::optional<ServerToClientPrx>& proxy, const Ice::Context& = Ice::noExplicitContext);
};

A Slice return value is mapped to a C++ return value in synchronous proxy and skeleton functions. The C++ type is naturally returned “by value”.

One of the two overloaded proxy member functions <operation-name>Async returns a std::future<T>.

When the Slice operation returns something – either through a return value or one or more out parameters – the future holds the return value and/or out parameters, in order of declaration (the return value, if any, is first). If there is a single return value or out parameter, the future holds the mapped C++ type. Otherwise, the future holds a std::tuple.

These parameters are all mapped “by value”, like in the Incoming column of In Parameters, since you’re receiving these values from Ice.

The other overloaded proxy member functions <operation-name>Async accepts a response callback function that consumes the return value and out parameters (if any). This callback function is provided by you (the application), and is called by Ice.

These parameters are all mapped “by value”, like in the Incoming column of In Parameters, since you’re receiving these values from Ice.

When the operation has a return value and one ore more out parameters, the return value is mapped to a parameter named returnValue int the C++ response callback.

On the server-side, when you use AMD, the pure virtual function <operation-name>Async on the generated skeleton class provides a response callback that accepts the return value and out parameters (if any). This callback function is provided by Ice, and you (the application) call this function in your implementation of <operation-name>Async.

The return value and out parameters are all mapped like in the Outgoing column of In Parameters, since you’re loaning these values to Ice for marshaling.

The mapping for optional parameters is the same as for required parameters, except each mapped C++ type is enclosed in a std::optional.

Consider the following operation:

Slice
optional(1) int execute(optional(2) string params, out optional(3) float value);

The corresponding C++ proxy function is:

C++
// Synchronous variant
std::optional<std::int32_t> execute(std::optional<std::string_view> params, std::optional<float>& value, ...);

and the corresponding C++ skeleton function is:

C++
// Synchronous variant
virtual std::optional<std::int32_t> execute(std::optional<std::string> params, std::optional<float>& value, ...);

As we saw in the Client-Side C# Mapping for Interfaces, for each operation on an interface, the generated proxy class contains two methods for this operation. To invoke an operation, you call one of these methods on the proxy. For example, let’s take the generated code from the greeter example:

Slice
module VisitorCenter
{
interface Greeter
{
["cs:identifier:Greet"]
string greet(string name);
}
}

The proxy interface generated from the Greeter interface, after removing extra details, is as follows:

C#
namespace VisitorCenter
{
public partial interface GreeterPrx : Ice.ObjectPrx
{
string Greet(
string name,
Dictionary<string, string>? context = null);
Tasks.Task<string> GreetAsync(
string name,
Dictionary<string, string>? context = null,
IProgress<bool>? progress = null,
CancellationToken cancel = default);
}
}

Given a proxy to an object of type Greeter, the client can invoke the greet operation as follows:

C#
GreeterPrx greeter = GreeterPrxHelper.createProxy(
communicator, "greeter:tcp -h localhost -p 4061");
string greeting = await greeter.GreetAsync("Alice"); // Get greeting via RPC

For each operation, the Slice compiler generates 2 methods on the proxy class:

  • a “sync” method with the same name as the operation. When you call this method, your thread waits synchronously until the invocation completes. A successful invocation completes with a return value (which can be void), while an unsuccessful invocation completes with an exception.

  • an “async” method, named <operation-name>Async. When you call this method, your thread marshals the arguments to the method synchronously, but the remainder of this invocation is asynchronous, and the method returns immediately. You get the result (return value or exception) through a Task. These async methods are described in more detail in Asynchronous Method Invocation (AMI) in C#.

Any operation invocation may throw a runtime exception and, if the operation has an exception specification, may also throw user exceptions. Suppose we have the following simple interface:

Slice
exception Tantrum
{
["cs:identifier:Reason"]
string reason;
}
interface Child
{
["cs:identifier:AskToCleanUp"]
void askToCleanUp() throws Tantrum;
}

Slice exceptions are thrown as C# exceptions, so you can simply enclose one or more operation invocations in a try-catch block:

C#
ChildPrx child = ...; // Get child proxy...
try
{
await child.AskToCleanUpAsync();
}
catch (Tantrum t)
{
Console.WriteLine($"The child says: {t.Reason}");
}

As we saw in the Server-Side C# Mapping for Interfaces, for each operation on an interface, the generated skeleton class contains an abstract method with the same name.

For example, let’s take the generated code from the greeter example:

Slice
module VisitorCenter
{
interface Greeter
{
["cs:identifier:Greet"]
string greet(string name);
}
}

The skeleton class generated from the Greeter interface, after removing extra details, is as follows:

C#
namespace VisitorCenter
{
public partial interface Greeter : Ice.Object
{
string Greet(string name, Ice.Current current);
}
public abstract partial class GreeterDisp_ : Greeter
{
public abstract string Greet(string name, Ice.Current current);
}
}

The Greet method takes a string name and a Current, then returns a value of type string. This method should be implemented in your derived servant class with something like:

C#
internal class Chatbot : VisitorCenter.GreeterDisp_
{
public override string Greet(string name, Ice.Current current)
=> $"Hello, {name}!";
}

Each operation with the ["amd"] metadata is mapped to a method with an Async suffix in the skeleton class. The AMD mapping replaces the default “sync” mapping for the operation. See Asynchronous Method Dispatch (AMD) in C# for details.

To throw an exception from an operation implementation, you simply construct the exception and throw it. For example:

C#
public override void Write(string[] text, Ice.Current current)
{
// Try to write the file contents here...
// Assume we are out of space...
if (error)
{
throw new Filesystem.WriteException("file too large");
}
}

If you throw an arbitrary C# exception (such as a ArgumentException), the Ice runtime catches the exception and then returns an UnknownException to the client.

If you throw an Ice runtime exception, such as MarshalException, the client receives an UnknownLocalException.

The server-side Ice runtime does not validate user exceptions thrown by an operation implementation to ensure they are compatible with the operation's Slice definition. Rather, Ice returns the user exception to the client, where the client-side runtime will validate the exception as usual and throws UnknownUserException for an unexpected exception type.

Asynchronous Method Invocation (AMI) is the term used to describe the client-side support for the asynchronous programming model. AMI supports both oneway and twoway requests, but unlike their synchronous counterparts, AMI requests never block the calling thread. When a client issues an AMI request, the Ice runtime hands the message off to the local transport buffer or, if the buffer is currently full, queues the request for later delivery. The application can then continue its activities and poll or wait for completion of the invocation, or receive a callback when the invocation completes.

AMI is transparent to the server: there is no way for the server to tell whether a client sent a request synchronously or asynchronously.

Consider the following Slice definition:

Slice
module Demo
{
interface Employees
{
["cs:identifier:GetName"]
string getName(int number);
}
}

slice2cs generates the following asynchronous proxy method:

C#
public partial interface EmployeesPrx : Ice.ObjectPrx
{
Task<string> GetNameAsync(
int number,
Dictionary<string, string>? context = null,
IProgress<bool>? progress = null,
CancellationToken cancel = default);
...
}

As you can see, the getName operation generates a GetNameAsync method that accepts several optional parameters:

The GetNameAsync method sends (or queues) an invocation of getName. This method does not block the calling thread. It returns a Task that you typically await. Here's an example that calls getNameAsync:

C#
EmployeesPrx e = ...;
string name = await e.GetNameAsync(99);

If an invocation throws an exception, the exception can be obtained from the task.

The exception is provided by the task, even if the actual error condition for the exception was encountered during the call to the Async method ("on the way out"). The advantage of this behavior is that all exception handling is located with the code that handles the task (instead of being present twice, once where the Async method is called, and again where the task is handled).

There are two exceptions to this rule:

  • if you destroy the communicator and then make an asynchronous invocation, the Async method throws CommunicatorDestroyedException directly.
  • a call to an Async method can throw TwowayOnlyException. An Async method throws this exception if you call an operation that has a return value or out-parameters on a oneway proxy.

You can invoke operations via oneway proxies asynchronously, provided the operation has void return type, does not have any out-parameters, and does not throw user exceptions. If you call an asynchronous method on a oneway proxy for an operation that returns values or throws a user exception, the proxy method throws TwowayOnlyException.

The task returned for a oneway invocation completes as soon as the request is successfully written to the client-side transport. The task completes with an exception if an error occurs before the request is successfully written.

Asynchronous method invocations never block the thread that calls the asynchronous proxy method. The Ice runtime checks to see whether it can write the request to the local transport. If it can, it does so immediately in the caller's thread. Alternatively, if the local transport does not have sufficient buffer space to accept the request, the Ice runtime queues the request internally for later transmission in the background.

This creates a potential problem: if a client sends many asynchronous requests at the time the server is too busy to keep up with them, the requests pile up in the client-side runtime until, eventually, the client runs out of memory.

The API provides a way for you to implement flow control by counting the number of requests that are queued so, if that number exceeds some threshold, the client stops invoking more operations until some of the queued operations have drained out of the local transport. One of the optional arguments to every asynchronous proxy invocation is a System.IProgress<bool>. If you provide one, the Ice runtime calls its Report method when the request has been sent, with a boolean argument indicating whether the request was sent synchronously. This argument is true if the entire request could be transferred to the local transport in the caller's thread without blocking, otherwise the argument is false. Furthermore, a value of true indicates that Ice is calling Report recursively from the calling thread, whereas a value of false indicates that Ice is calling Report from an Ice thread pool thread.

Here's a simple example to demonstrate the flow control feature:

C#
ExamplePrx proxy = ...;
proxy.DoSomethingAsync(progress: new SentCallback());
class SentCallback : IProgress<bool>
{
public void Report(bool sentSynchronously)
{
if (sentSynchronously)
{
// Entire request was accepted by the transport,
// called recursively from this thread
}
else
{
// Request was queued but has now been sent,
// called from a separate thread
}
}
}

Using this feature, you can limit the number of queued requests by counting the number of requests that are queued and decrementing the count when the Ice runtime passes a request to the local transport.

Every asynchronous proxy method accepts an optional CancellationToken argument. Its default value is default, which is equivalent to passing CancellationToken.None.

Cancellation prevents a queued invocation from being sent or, if the invocation has already been sent, ignores a reply if the server sends one. Cancellation is a local operation and has no effect on the server. The result of a canceled invocation is an Ice.InvocationCanceledException.

The number of simultaneous synchronous requests a server is capable of supporting is determined by the number of threads in the server's thread pool. If all of the threads are busy dispatching long-running operations, then no threads are available to process new requests and therefore clients may experience an unacceptable lack of responsiveness.

Asynchronous Method Dispatch (AMD), the server-side equivalent of AMI, addresses this scalability issue. Using AMD, a server can receive a request but then suspend its processing in order to release the dispatch thread as soon as possible. When processing resumes and the results are available, the server can provide its results to the Ice runtime for delivery to the client.

AMD is transparent to the client, that is, there is no way for a client to distinguish a request that, in the server, is processed synchronously from a request that is processed asynchronously.

In practical terms, an AMD operation typically queues the request data for later processing by an application thread (or thread pool). In this way, the server minimizes the use of dispatch threads and becomes capable of efficiently supporting thousands of simultaneous clients.

The easiest way to use AMD in C# is to make your servant class derive from the async skeleton class generated by the Slice compiler. For example:

C#
// This servant uses AMD
public class Chatbot : VisitorCenter.AsyncGreeterDisp_
{
// your implementation here
};

If you prefer to implement some operations asynchronously (with AMD) and other operations synchronously, you can add the ["amd"] metadata directive to the operations you want to implement with AMD and use the default skeleton class.

The metadata directive replaces synchronous dispatch on the default skeleton, that is, a particular operation implementation must use synchronous or asynchronous dispatch and cannot use both.

Consider the following Slice definitions:

Slice
interface Controller
{
["amd"] void startProcess();
int endProcess();
}

In this example, the startProcess of the default skeleton class uses asynchronous dispatch while endProcess uses synchronous dispatch.

With AMD, the skeleton’s abstract method is named <operation-name>Async. This method returns a Task and accepts the operation's in-parameters.

For example, suppose we have defined the following operation:

Slice
interface Example
{
["cs:identifier:Op"]
string op(short s, out long l);
}

Operation op is mapped as follows in the async skeleton class:

C#
public record struct Example_OpResult(string returnValue, long l);
public abstract partial class AsyncExampleDisp_ : AsyncExample
{
public abstract Task<Example_OpResult> OpAsync(short s, Ice.Current current);
...
}

You would get the same signature on the default skeleton (Example) if you decorate op with ["amd"].

There are two processing contexts in which the logical implementation of an AMD operation may need to report an exception: the dispatch thread (the thread that receives the request), and the response thread (the thread that completes the task).

The implementation of the Async method in your servant class can throw an exception synchronously: it’s equivalent to returning a task completed with this exception.

An in parameter is mapped to a C# parameter with the same name; its type is the mapped C# type.

For example, a Slice parameter string name is mapped to a C# parameter string name.

An out parameter is mapped to an out parameter with the same name in synchronous proxy and skeleton methods; the type of the parameter is the mapped C# type.

Consider the following example:

Slice
struct NumberAndString
{
int x;
string str;
}
sequence<string> StringSeq;
dictionary<long, StringSeq> StringTable;
interface ServerToClient
{
["cs:identifier:Op1"]
void op1(out int i, out float f, out bool b, out string s);
["cs:identifier:Op2"]
void op2(out NumberAndString ns, out StringSeq ss, out StringTable st);
["cs:identifier:Op3"]
void op3(out ServerToClient* proxy);
}

The Slice compiler generates a proxy interface for these definitions (we omit the async overloads):

C#
public partial interface ServerToClientPrx : Ice.ObjectPrx
{
void Op1(
out int i,
out float f,
out bool b,
out string s,
Dictionary<string, string>? context = null);
void Op2(
out NumberAndString ns,
out string[] ss,
out Dictionary<long, string[]> st,
Dictionary<string, string>? context = null);
void Op3(
out ServerToClientPrx? proxy,
Dictionary<string, string>? context = null);
}

A Slice return value is mapped to a C# return value in synchronous proxy and skeleton methods.

Out parameters and return values are mapped to C# Task return values.

The returned Task depends on how many values an operation returns, including out parameters and a non-void return value:

  • Zero values The corresponding C# method returns a plain Task.
  • One value The corresponding C# method returns a Task<T>, where T is the mapped C# type.
  • Two or more values The corresponding C# method returns a Task<Interface_OpResult>, where Interface_OpResult is a generated record struct that holds the return value and out parameters.

Consider this example:

Slice
interface Example
{
["cs:identifier:Op"]
double op(int inp1, string inp2, out bool outp1, out long outp2);
}

The Slice compiler generates the following C# code for this interface:

C#
public record struct Example_OpResult(double returnValue, bool outp1, long outp2);
public partial interface ExamplePrx : Ice.ObjectPrx
{
Task<Example_OpResult> OpAsync(
int inp1,
string inp2,
Dictionary<string, string>? context = null,
...);
}

The mapping for optional parameters is the same as for required parameters, except each mapped C# type is nullable, where null represents “not set”.

Consider the following operation:

Slice
["cs:identifier:Execute"]
optional(1) int execute(optional(2) string parameters, out optional(3) float value);

The corresponding C# proxy method is:

C#
// Asynchronous variant
Task<Example_ExecuteResult> ExecuteAsync(
string? parameters,
Dictionary<string, string>? context = null, ...);

and the corresponding C# skeleton method is:

C#
// Synchronous variant
int? Execute(string? parameters, out float? value, Ice.Current current);

As we saw in the Client-Side Java Mapping for Interfaces, for each operation on an interface, the generated proxy interface contains 4 methods for this operation. To invoke an operation, you call one of these methods on the proxy. For example, let’s take the generated code from the greeter example:

Slice
["java:identifier:com.example.visitorcenter"]
module VisitorCenter
{
interface Greeter
{
string greet(string name);
}
}

The proxy interface generated from the Greeter interface, after removing extra details, is as follows:

Java
package com.example.visitorcenter;
public interface GreeterPrx extends com.zeroc.Ice.ObjectPrx {
default String greet(String name) { ... }
default String greet(String name, Map<String, String> context) { ... }
default CompletableFuture<String> greetAsync(String name) { ... }
default CompletableFuture<String> greetAsync(
String name, Map<String, String> context) { ... }
// ...
static GreeterPrx createProxy(com.zeroc.Ice.Communicator communicator,
String proxyString) { ... }
// ...
}

Given a proxy to an object of type Greeter, the client can invoke the greet operation as follows:

Java
GreeterPrx greeter = GreeterPrx.createProxy(
communicator, "greeter:tcp -h localhost -p 4061");
String greeting = greeter.greet("Alice"); // Get name via RPC

For each operation, the Slice compiler generates 4 methods on the proxy interface:

  • two overloaded “sync” methods with the same name as the operation. When you call these methods, your thread waits synchronously until the invocation completes. A successful invocation completes with a return value (which can be void), while an unsuccessful invocation completes with an exception.
  • two overloaded “async” methods, named <operation-name>Async. When you call these methods, your thread marshals the arguments to the method synchronously, but the remainder of this invocation is asynchronous, and the method returns a CompletableFuture immediately. These async methods are described in more detail in Asynchronous Method Invocation (AMI) in Java.

Any operation invocation may throw a runtime exception and, if the operation has an exception specification, may also throw user exceptions. Suppose we have the following simple interface:

Slice
exception Tantrum
{
string reason;
}
interface Child
{
void askToCleanUp() throws Tantrum;
}

Slice exceptions are thrown as Java exceptions, so you can simply enclose one or more operation invocations in a try-catch block:

Java
ChildPrx child = ...; // Get child proxy...
try {
child.askToCleanUp();
} catch (Tantrum t) {
System.out.print("The child says: ");
System.out.println(t.reason);
}

As we saw in the Server-Side Java Mapping for Interfaces, for each operation on an interface, the generated skeleton interface contains an abstract method with the same name.

For example, let’s take the generated code from the greeter example:

Slice
["java:identifier:com.example.visitorcenter"]
module VisitorCenter
{
interface Greeter
{
string greet(string name);
}
}

The skeleton interface generated from the Greeter interface, after removing extra details, is as follows:

Java
package com.example.visitorcenter;
public interface Greeter extends com.zeroc.Ice.Object {
String greet(String name, com.zeroc.Ice.Current current);
// ...
}

The greet method takes a String name and a Current, then returns a value of type String. This method should be implemented in your derived servant class with something like:

Java
class Chatbot implements Greeter {
@Override
public String greet(String name, Current current) {
return "Hello, " + name + "!";
}
}

Each operation with the ["amd"] metadata is mapped to a method with an Async suffix in the skeleton interface. The AMD mapping replaces the default “sync” mapping for the operation. See Asynchronous Method Dispatch (AMD) in Java for details.

To throw an exception from an operation implementation, you simply construct the exception and throw it. For example:

Java
@Override
public void write(String[] text, Current current) throws WriteException {
// Try to write the file contents here...
// Assume we are out of space...
if (error) {
throw new WriteException("file too large");
}
}

Like on the client-side, the Slice exception specification maps to an exception specification on the corresponding Java method.

If you throw an arbitrary Java exception (such as a IllegalArgumentException), the Ice runtime catches the exception and then returns an UnknownException to the client.

If you throw an Ice runtime exception, such as MarshalException, the client receives an UnknownLocalException.

The server-side Ice runtime does not validate user exceptions thrown by an operation implementation to ensure they are compatible with the operation's Slice definition. Rather, Ice returns the user exception to the client, where the client-side runtime will validate the exception as usual and throws UnknownUserException for an unexpected exception type.

Asynchronous Method Invocation(AMI) is the term used to describe the client-side support for the asynchronous programming model. AMI supports both oneway and twoway requests, but unlike their synchronous counterparts, AMI requests never block the calling thread. When a client issues an AMI request, the Ice runtime hands the message off to the local transport buffer or, if the buffer is currently full, queues the request for later delivery. The application can then continue its activities and poll or wait for completion of the invocation, or receive a callback when the invocation completes.

AMI is transparent to the server: there is no way for the server to tell whether a client sent a request synchronously or asynchronously.

If an invocation throws an exception, the exception can be obtained from the future in several ways:

  • Call get on the future; get throws CompletionException with the actual exception available via getCause()
  • Call join on the future; join throws ExecutionException with the actual exception available via getCause()
  • Use chaining methods such as exceptionally, handle or whenComplete to execute custom actions

The exception is provided by the future, even if the actual error condition for the exception was encountered during the call to the Async method ("on the way out"). The advantage of this behavior is that all exception handling is located with the code that handles the future (instead of being present twice, once where the Async method is called, and again where the future is handled).

There are two exceptions to this rule:

  • if you destroy the communicator and then make an asynchronous invocation, the Async method throws CommunicatorDestroyedException directly.
  • a call to an Async method can throw TwowayOnlyException. An Async method throws this exception if you call an operation that has a return value or out-parameters on a oneway proxy.

The CompletableFuture<T> object that is returned by asynchronous proxy methods can be down-casted to InvocationFuture<T> when an application requires more control over an invocation.

The InvocationFuture methods allow you to poll for call completion. Polling is useful in a variety of cases. As an example, consider the following simple interface to transfer files from client to server:

Slice
interface FileTransfer
{
void send(int offset, ByteSeq bytes);
}

The client repeatedly calls send to send a chunk of the file, indicating at which offset in the file the chunk belongs. A naïve way to transmit a file would be along the following lines:

Java
FileHandle file = open(...);
FileTransferPrx ft = ...;
int chunkSize = ...;
int offset = 0;
while (!file.eof()) {
byte[] bs;
bs = file.read(chunkSize); // Read a chunk
ft.send(offset, bs); // Send the chunk
offset += bs.length;
}

This works, but not very well: because the client makes synchronous calls, it writes each chunk on the wire and then waits for the server to receive the data, process it, and return a reply before writing the next chunk. This means that both client and server spend much of their time doing nothing — the client does nothing while the server processes the data, and the server does nothing while it waits for the client to send the next chunk.

Using asynchronous calls, we can improve on this considerably:

Java
FileHandle file = open(...);
FileTransferPrx ft = ...;
int chunkSize = ...;
int offset = 0;
var results = new LinkedList<InvocationFuture<Void>>();
int numRequests = 5;
while (!file.eof()) {
byte[] bs;
bs = file.read(chunkSize);
// Send up to numRequests + 1 chunks asynchronously.
CompletableFuture<Void> f = ft.sendAsync(offset, bs);
offset += bs.length;
// Wait until this request has been passed to the transport.
var i = (InvocationFuture<Void>)f;
i.waitForSent();
results.add(i);
// Once there are more than numRequests, wait for the least recent one to
// complete.
while (results.size() > numRequests) {
i = results.getFirst();
results.removeFirst();
i.join();
}
}
// Wait for any remaining requests to complete.
while (results.size() > 0) {
InvocationFuture<Void> i = results.getFirst();
results.removeFirst();
i.join();
}

With this code, the client sends up to numRequests + 1 chunks before it waits for the least recent one of these requests to complete. In other words, the client sends the next request without waiting for the preceding request to complete, up to the limit set by numRequests. In effect, this allows the client to "keep the pipe to the server full of data": the client keeps sending data, so both client and server continuously do work.

Obviously, the correct chunk size and value of numRequests depend on the bandwidth of the network as well as the amount of time taken by the server to process each request. However, with a little testing, you can quickly zoom in on the point where making the requests larger or queuing more requests no longer improves performance. With this technique, you can realize the full bandwidth of the link to within a percent or two of the theoretical bandwidth limit of a native socket connection.

You can invoke operations via oneway proxies asynchronously, provided the operation has void return type, does not have any out-parameters, and does not throw user exceptions. If you call an asynchronous proxy method on a oneway proxy for an operation that returns values or throws a user exception, the Async method throws TwowayOnlyException.

The future returned for a oneway invocation completes as soon as the request is successfully written to the client-side transport. The future completes exceptionally if an error occurs before the request is successfully written.

Asynchronous method invocations never block the thread that calls the asynchronous proxy method. The Ice runtime checks to see whether it can write the request to the local transport. If it can, it does so immediately in the caller's thread. (In that case, InvocationFuture.sentSynchronously returns true.) Alternatively, if the local transport does not have sufficient buffer space to accept the request, the Ice runtime queues the request internally for later transmission in the background. (In that case, InvocationFuture.sentSynchronously returns false.)

This creates a potential problem: if a client sends many asynchronous requests at the time the server is too busy to keep up with them, the requests pile up in the client-side run time until, eventually, the client runs out of memory.

The InvocationFuture class provides a way for you to implement flow control by counting the number of requests that are queued so, if that number exceeds some threshold, the client stops invoking more operations until some of the queued operations have drained out of the local transport:

Java
ExamplePrx proxy = ...;
CompletableFuture<Result> f = proxy.doSomethingAsync();
var i = (InvocationFuture<Result>)f;
i.whenSent((sentSynchronously, ex) -> {
if (ex != null) {
// handle errors...
} else {
// this request was sent, send another!
}
});

The whenSent method has the following semantics:

  • If the Ice runtime was able to pass the entire request to the local transport immediately, the action will be invoked from the current thread and the sentSynchronously argument will be true.
  • If Ice wasn't able to write the entire request without blocking, the action will eventually be invoked from an Ice thread pool thread and the sentSynchronously argument will be false.

If you need more control over the execution environment of your action, you can use one of the whenSentAsync methods instead. The sentSynchronously argument still behaves as described above, but your executor's implementation will determine the threading behavior.

CompletableFuture provides a cancel method that you can call to cancel an invocation. If the future hasn't already completed either successfully or exceptionally, canceling the future causes it to complete with an instance of java.util.concurrent.CancellationException.

Cancellation prevents a queued invocation from being sent or, if the invocation has already been sent, ignores a reply if the server sends one. Cancellation is a local operation and has no effect on the server.

When an invocation completes, the Ice runtime calls complete or completeExceptionally on the future from an Ice thread pool thread. The thread in which your own action executes depends on the completion status of the future and the manner in which you registered the action. Here are some examples:

  • Suppose you configure an action using whenComplete. If the future is already complete at the time you call whenComplete, the action will execute immediately in the calling thread. If the future is not yet complete when you call whenComplete, the action will eventually execute in an Ice thread pool thread.
  • Now suppose you configure an action using one of the whenCompleteAsync methods. Regardless of the thread in which Ice completes the future, your executor's implementation will determine the thread context in which the action is invoked. The Ice thread pool can be used as an executor; you can obtain the executor by calling the ice_executor proxy method. With the Ice thread pool executor, the action is always queued to be executed by the Ice thread pool.

The number of simultaneous synchronous requests a server is capable of supporting is determined by the number of threads in the server's thread pool. If all of the threads are busy dispatching long-running operations, then no threads are available to process new requests and therefore clients may experience an unacceptable lack of responsiveness.

Asynchronous Method Dispatch (AMD), the server-side equivalent of AMI, addresses this scalability issue. Using AMD, a server can receive a request but then suspend its processing in order to release the dispatch thread as soon as possible. When processing resumes and the results are available, the server can provide its results to the Ice runtime for delivery to the client.

AMD is transparent to the client, that is, there is no way for a client to distinguish a request that, in the server, is processed synchronously from a request that is processed asynchronously.

In practical terms, an AMD operation typically queues the request data for later processing by an application thread (or thread pool). In this way, the server minimizes the use of dispatch threads and becomes capable of efficiently supporting thousands of simultaneous clients.

The easiest way to use AMD in Java is to make your servant class implement the async skeleton interface generated by the Slice compiler. For example:

Java
// This servant uses AMD
class Chatbot implements AsyncGreeter {
// your implementation here
}

If you prefer to implement some operations asynchronously (with AMD) and other operations synchronously, you can add the ["amd"] metadata directive to the operations you want to implement with AMD and use the default skeleton interface.

The metadata directive replaces synchronous dispatch on the default skeleton, that is, a particular operation implementation must use synchronous or asynchronous dispatch and cannot use both.

Consider the following Slice definitions:

Slice
interface Controller
{
["amd"] void startProcess();
int endProcess();
}

In this example, the startProcess of the default skeleton interface uses asynchronous dispatch while endProcess uses synchronous dispatch.

With AMD, the skeleton’s abstract method is named <operation-name>Async. This method returns an java.util.concurrent.CompletionStage<T> and accepts the operation’s in-parameters.

The implementation of the operation, which typically returns an instance of the derived class java.util.concurrent.CompletableFuture<T>, must eventually complete the future by supplying either the results or an exception.

For example, suppose we have defined the following operation:

Slice
interface Example
{
string op(short s, out long count);
}

Operation op is mapped as follows in the skeleton interface:

Java
public interface Example extends com.zeroc.Ice.Object {
public static class OpResult {
public String returnValue;
public long count;
...
}
// synchronous dispatch methods
}
public interface AsyncExample extends com.zeroc.Ice.Object {
CompletionStage<Example.OpResult> opAsync(
short s, com.zeroc.Ice.Current current);
}

You would get the same opAsync method on the default skeleton (Example) if you decorate op with ["amd"].

There are two processing contexts in which the logical implementation of an AMD operation may need to report an exception: the dispatch thread (the thread that receives the request), and the response thread (the thread that sends the response).

The implementation of the Async method in your servant class can throw an exception synchronously: it’s equivalent to returning a future completed with this exception.

Since the asynchronous proxy API and the asynchronous dispatch API both use CompletionStage, it is possible to implement an asynchronous dispatch by sending an asynchronous request to a proxy.

Continuing our example from the previous section, suppose our servant also holds a proxy to another object of the same type and derives its response from that of the other object:

Java
class ExampleServant implements AsyncExample {
private final ExamplePrx _other;
@Override
public CompletionStage<Example.OpResult> opAsync(short s, Current current) {
var result = _other.opAsync(s);
// ... some other work while op is executing
return result;
}
}

An in parameter is mapped to a Java parameter with the same name; its type is the mapped Java type.

For example, a Slice parameter string name is mapped to a Java parameter String name.

The return value of a mapped method depends on how many values the corresponding Slice operation returns, including out parameters and a non-void return value:

  • Zero values The corresponding Java method returns void. For the purposes of this discussion, we're not interested in these operations.

  • One value The corresponding Java method returns the mapped type, regardless of whether the Slice definition of the operation declared it as a return value or as an out parameter. Consider this example:

    Slice
    interface I
    {
    string op1();
    void op2(out string name);
    }

    The mapping generates corresponding methods with identical signatures:

    Java
    interface IPrx extends ObjectPrx {
    String op1();
    String op2();
    }
  • Two or more values The Slice compiler generates an extra nested class to hold the results of an operation that returns multiple values. The class is nested in the mapped interface (not the proxy interface) and has the name OpResult, where Op represents the name of the operation. The leading character of the class name for a "result class" is always capitalized. The values of out parameters are provided in corresponding public fields of the same names. If the operation declares a return value, its value is provided in the field named returnValue.The result class defines an empty constructor as well as a primary constructor that accepts and assigns a value for each of its fields. The corresponding Java method returns the result class type.

Consider this example:

Slice
interface Example
{
double op(int inp1, string inp2, out bool outp1, out long outp2);
}

The generated code looks like this:

Java
// Server-side skeleton
public interface Example extends com.zeroc.Ice.Object {
public static class OpResult {
public double returnValue;
public boolean outp1;
public long outp2;
...
}
Example.OpResult op(int inp1, String inp2, com.zeroc.Ice.Current current);
...
}
// Client-side proxy
public interface ExamplePrx extends com.zeroc.Ice.ObjectPrx {
default Example.OpResult op(int inp1, String inp2) {
...
}
default Example.OpResult op(
int inp1, String inp2, java.util.Map<String, String> context) {
...
}
default CompletableFuture<Example.OpResult> opAsync(int inp1, String inp2) {
...
}
default CompletableFuture<Example.OpResult> opAsync(
int inp1, String inp2, java.util.Map<String, String> context) {
...
}
}

Some Slice types naturally have "empty" or "not there" semantics. Specifically, sequences, dictionaries, and strings all can be null, but the corresponding Slice types do not have the concept of a null value. To make life with these types easier, whenever you pass null as a parameter or return value of type sequence, dictionary, or string, the Ice run time automatically sends an empty sequence, dictionary, or string to the receiver.

This behavior is useful as a convenience feature: especially for deeply-nested data types, fields that are sequences, dictionaries, or strings automatically arrive as an empty value at the receiving end. This saves you having to explicitly initialize, for example, every string element in a large sequence before sending the sequence in order to avoid NullPointerException. Note that using null parameters in this way does not create null semantics for Slice sequences, dictionaries, or strings. As far as the object model is concerned, these do not exist (only empty sequences, dictionaries, and strings do). For example, whether you send a string as null or as an empty string makes no difference to the receiver: either way, the receiver sees an empty string.

The mapping uses standard Java types to encapsulate optional parameters:

  • java.util.OptionalDouble The mapped type for an optional double.
  • java.util.OptionalInt The mapped type for an optional int.
  • java.util.OptionalLong The mapped type for an optional long.
  • java.util.Optional<T> The mapped type for all other Slice types.

Optional return values and output parameters are mapped to instances of the above classes, depending on their types. For operations with optional in parameters, the proxy provides a set of overloaded methods that accept them as optional values, and another set of methods that accept them as required values. Consider the following operation:

Slice
optional(1) int execute(optional(2) string parameters);

The mapping for this operation is shown below:

Java
// With String in-parameter
java.util.OptionalInt execute(String parameters);
java.util.OptionalInt execute(
String parameters, java.util.Map<String, String> context);
// With Optional<String> in-parameter
java.util.OptionalInt execute(java.util.Optional<String> parameters);
java.util.OptionalInt execute(
java.util.Optional<String> parameters, java.util.Map<String, String> context);
// Async with String in-parameter
CompletableFuture<java.util.OptionalInt> executeAsync(String parameters);
CompletableFuture<java.util.OptionalInt> executeAsync(
String parameters, java.util.Map<String, String> context);
// Async with Optional<String> in-parameter
CompletableFuture<java.util.OptionalInt> executeAsync(
java.util.Optional<String> parameters);
CompletableFuture<java.util.OptionalInt> executeAsync(
java.util.Optional<String> parameters, java.util.Map<String, String> context);

For cases where you are passing values for all of the optional in parameters, it is more efficient to use the required mapping and avoid creating temporary optional values.

A client can invoke execute as shown below:

Java
java.util.OptionalInt i;
i = proxy.execute("--file log.txt"); // required mapping
i = proxy.execute(java.util.Optional.of("--file log.txt")); // optional mapping
i = proxy.execute(java.util.Optional.empty()); // params is unset
if (i.isPresent()) {
System.out.println("value = " + i.getAsInt());
}

Passing null where an optional value is expected is equivalent to passing an instance whose value is unset.

For each Slice operation defined on an interface, the generated proxy class provides a method with the same name. To invoke an operation, you call this method on the proxy.

For example, consider the following Slice definition:

Slice
module VisitorCenter
{
interface Greeter
{
string greet(string name);
}
}

The generated proxy class (simplified) looks like this:

JavaScript
class GreeterPrx extends Ice.ObjectPrx {
constructor(communicator, proxyString) { ... }
greet(name, context) { ... }
// ...
}

And the TypeScript declaration:

TypeScript
export namespace VisitorCenter {
export class GreeterPrx extends Ice.ObjectPrx {
constructor(communicator: Ice.Communicator, proxyString: string);
greet(name: string, context?: Map<string, string>): Ice.AsyncResult<string>;
// ...
}
}

Given a proxy to a Greeter object, a client can invoke greet as follows:

TypeScript
const greeter = new VisitorCenter.GreeterPrx(
communicator,
"greeter:tcp -h localhost -p 4061");
const greeting = await greeter.greet("Alice"); // Get name via RPC

Ice for JavaScript supports only asynchronous method invocation (AMI). The JavaScript runtime does not provide a blocking I/O model in order to keep the event loop responsive.

The arguments passed to the promise resolution depend on the operation signature:

  • If the operation has a single return value, the promise is fulfilled with that value.
  • If the operation has a return value and/or out parameters, the promise is fulfilled with an array: the return value (if any) followed by the out parameters.

Any operation invocation may throw a local exception and, if the operation has an exception specification, may also throw user exceptions. Suppose we have the following simple interface:

Slice
exception Tantrum
{
string reason;
}
interface Child
{
void askToCleanUp() throws Tantrum;
}

Slice exceptions are thrown as JavaScript exceptions, so you can simply enclose one or more operation invocations in a try-catch block:

TypeScript
const child = ... // Get child proxy...
try {
await child.askToCleanUp();
} catch (error: unknown) {
if (error instanceof Tantrum) {
console.log("The child says:", error.reason);
} else {
throw error;
}
}

For each Slice operation defined on an interface, the generated skeleton class includes a corresponding abstract member function with the same name.

For example, consider the following Slice definition:

Slice
module VisitorCenter
{
interface Greeter
{
string greet(string name);
}
}

The generated JavaScript skeleton class is:

JavaScript
VisitorCenter.Greeter = class extends Ice.Object {};

Since JavaScript does not support abstract methods, the generated class does not provide an actual abstract declaration. Instead, the servant class that derives from this skeleton must add the operation implementations, as if the methods were abstract.

This is clearer in the generated TypeScript declarations, which can use abstract methods:

TypeScript
export abstract class Greeter extends Ice.Object {
abstract greet(
name: string,
current: Ice.Current): PromiseLike<string> | string;
...

The servant implementation can choose how to provide the result:

  • Return the result directly (synchronous implementation).
  • Return a promise-like object that will be fulfilled with the result.
  • Declare the method as async and use await inside the implementation.

Synchronous version:

TypeScript
greet(name: string, current: Ice.Current): PromiseLike<string> | string {
return `Hello, ${name}`;
}

Asynchronous version:

TypeScript
async greet(name: string, current: Ice.Current): Promise<string> {
// Nested async invocation
return await this._target.greet(name);
}

The parameter passing rules for the JavaScript mapping are very simple: parameters are passed either by value (for simple types) or by reference (for complex types). Semantically, the two ways of passing parameters are identical: it is guaranteed that the value of a parameter will not be changed by the invocation.

Here is an interface with operations that pass parameters of various types from client to server:

Slice
struct NumberAndString
{
int x;
string str;
}
sequence<string> StringSeq;
dictionary<long, StringSeq> StringTable;
interface ClientToServer
{
void op1(int i, float f, bool b, string s);
void op2(NumberAndString ns, StringSeq ss, StringTable st);
void op3(ClientToServer* proxy);
}

The Slice compiler generates the following proxy methods for these definitions:

JavaScript
class ClientToServerPrx extends Ice.ObjectPrx {
op1(i, f, b, s, context) { ... }
op2(ns, ss, st, context) { ... }
op3(proxy, context) { ... }
}
TypeScript
abstract class ClientToServerPrx extends Ice.ObjectPrx {
op1(
i:number,
f:number,
b:boolean,
s:string,
context?:Map<string, string>):Ice.AsyncResult<void>;
op2(
ns:NumberAndString,
ss:StringSeq,
st:StringTable,
context?:Map<string, string>):Ice.AsyncResult<void>;
op3(
proxy:ClientToServerPrx,
context?:Map<string, string>):Ice.AsyncResult<void>;
}

Given a proxy to a ClientToServer interface, the client code can pass parameters as in the following example:

JavaScript
const p = ...; // Get ClientToServerPrx proxy...
await p.op1(42, 3.14, true, "Hello world!"); // Pass simple literals
const i = 42;
const f = 3.14;
const b = true;
const s = "Hello world!";
await p.op1(i, f, b, s); // Pass simple variables
const ns = new NumberAndString();
ns.x = 42;
ns.str = "The Answer";
const ss = [];
ss.push("Hello world!");
const st = new StringTable();
st.set(0, ss);
await p.op2(ns, ss, st); // Pass complex variables
await p.op3(p); // Pass proxy

Some Slice types naturally have "empty" or "not there" semantics. Specifically, sequences, dictionaries, and strings all can be null, but the corresponding Slice types do not have the concept of a null value. To make life with these types easier, whenever you pass null as a parameter or return value of type sequence, dictionary, or string, the Ice run time automatically sends an empty sequence, dictionary, or string to the receiver.

This behavior is useful as a convenience feature: especially for deeply-nested data types, members that are sequences, dictionaries, or strings automatically arrive as an empty value at the receiving end. This saves you having to explicitly initialize, for example, every string element in a large sequence before sending the sequence in order to avoid a run-time error. Note that using null parameters in this way does not create null semantics for Slice sequences, dictionaries, or strings. As far as the object model is concerned, these do not exist (only empty sequences, dictionaries, and strings do). For example, whether you send a string as null or as an empty string makes no difference to the receiver: either way, the receiver sees an empty string.

Optional parameters use the same mapping as required parameters. The only difference is that undefined can be passed as the value of an optional parameter or return value to indicate an "unset" condition. Consider the following operation:

Slice
optional(1) int execute(optional(2) string params, out optional(3) float value);

The corresponding proxy method is:

TypeScript
execute(
params?: string | undefined,
context?: Map<string, string>):
Ice.AsyncResult<[number | undefined, number | undefined]>;

As we saw in the Client-Side MATLAB Mapping for Interfaces, for each operation on an interface, the generated proxy class contains 2 methods for this operation. To invoke an operation, you call one of these methods on the proxy. For example, let’s take the generated code from the greeter example:

Slice
["matlab:identifier:visitorcenter"]
module VisitorCenter
{
interface Greeter
{
string greet(string name);
}
}

The proxy class generated from the Greeter interface, after removing extra details, is as follows:

MATLAB
classdef GreeterPrx < Ice.ObjectPrx
methods
function returnValue = greet(obj, name, context)
% ...
end
function future = greetAsync(obj, name, context)
% ...
end
end
end

Given a proxy to an object of type Greeter, the client can invoke the greet operation as follows:

MATLAB
greeter = visitorcenter.GreeterPrx( ...
communicator, 'greeter:tcp -h localhost -p 4061');
greeting = greeter.greet('Alice'); % Get name via RPC

For each operation, the Slice compiler generates 2 methods on the proxy class:

  • a “sync” method with the same name as the operation. When you call this method, your program waits synchronously until the invocation completes. A successful invocation completes with a return value (which can be void), while an unsuccessful invocation completes with an exception.
  • an “async” method, named <operation-name>Async. When you call this method, your program marshals the arguments to the method synchronously, but the remainder of this invocation is asynchronous, and the method returns a future immediately. These async methods are described in more detail in Asynchronous Method Invocation (AMI) in MATLAB.

Any operation invocation may throw a runtime exception and, if the operation has an exception specification, may also throw user exceptions. Suppose we have the following simple interface:

Slice
exception Tantrum
{
string reason;
}
interface Child
{
void askToCleanUp() throws Tantrum;
}

Slice exceptions are thrown as MATLAB exceptions, so you can simply enclose one or more operation invocations in a try-catch block:

MATLAB
child = ...; % Get child proxy...
try
child.askToCleanUp();
catch ex
if isa(ex, 'Tantrum')
fprintf('The child says: %s\n', ex.reason);
else
rethrow(ex);
end
end

Asynchronous Method Invocation(AMI) is the term used to describe the client-side support for the asynchronous programming model. AMI supports both oneway and twoway requests, but unlike their synchronous counterparts, AMI requests never block the application. When a client issues an AMI request, the Ice runtime hands the message off to the local transport buffer or, if the buffer is currently full, queues the request for later delivery. The application can then continue its activities and poll or wait for completion of the invocation, or receive a callback when the invocation completes.

AMI is transparent to the server: there is no way for the server to tell whether a client sent a request synchronously or asynchronously.

Asynchronous invocations return an instance of the Ice.Future class – the future object. Its API is similar to MATLAB's parallel.future class, in particular, you can call wait and fetchOutputs on this future object.

If an invocation throws an exception, the exception will be thrown when the application calls fetchOutputs on the future. The exception is provided by the future, even if the actual error condition for the exception was encountered during the call to the Async method ("on the way out"). The advantage of this behavior is that all exception handling is located with the code that handles the future (instead of being present twice, once where the Async method is called, and again where the future is handled).

There are two exceptions to this rule:

  • if you destroy the communicator and then make an asynchronous invocation, the Async method throws Ice.CommunicatorDestroyedException directly.
  • a call to an Async method can throw Ice.TwowayOnlyException. An Async method throws this exception if you call an operation that has a return value or out-parameters on a oneway proxy.

You can invoke operations via oneway proxies asynchronously, provided the operation has void return type, does not have any out-parameters, and does not throw user exceptions. If you call an asynchronous proxy method on a oneway proxy for an operation that returns values or throws a user exception, the Async method throws Ice.TwowayOnlyException.

The future returned for a oneway invocation completes as soon as the request is successfully written to the client-side transport. The future completes exceptionally if an error occurs before the request is successfully written.

Asynchronous method invocations never block the thread that calls the Async function : the Ice runtime checks to see whether it can write the request to the local transport. If it can, it does so immediately in the caller's thread. Alternatively, if the local transport does not have sufficient buffer space to accept the request, the Ice runtime queues the request internally for later transmission in the background.

This creates a potential problem: if a client sends many asynchronous requests at the time the server is too busy to keep up with them, the requests pile up in the client-side runtime until, eventually, the client runs out of memory.

You can use future.State to check if a request was sent and implement flow-control for your application.

You can call cancel on the future returned by an async invocation to cancel this invocation. For example:

MATLAB
futureGreeting = slowGreeter.greetAsync('bob');
pause(4);
futureGreeting.cancel();

Calling this cancel method prevents a queued invocation from being sent or, if the invocation has already been sent, ignores a reply if the server sends one. This cancellation is purely local and has no effect on the server.

Canceling an invocation that has already completed has no effect. Otherwise, a canceled invocation is considered to be completed, meaning the future completed with an Ice.InvocationCanceledException.

An in parameter is mapped to a MATLAB parameter with the same name; its type is the mapped MATLAB type.

For example, a Slice parameter string name is mapped to a MATLAB parameter name with type char and size (1 :). The rules are the same as for Fields.

The MATLAB mapping uses the conventional language mechanism for returning one or more result values.

Consider the following Slice definitions:

Slice
struct NumberAndString
{
["matlab:identifier:X"]
int x;
["matlab:identifier:Str"]
string str;
}
sequence<string> StringSeq;
dictionary<long, StringSeq> StringTable;
interface ServerToClient
{
void op1(out int i, out float f, out bool b, out string s);
void op2(out NumberAndString ns,
out StringSeq ss,
out StringTable st);
void op3(out ServerToClient* proxy);
}

The Slice compiler generates the following code:

MATLAB
classdef ServerToClientPrx < Ice.ObjectPrx
methods
function [i, f, b, s] = op1(obj, context)
...
end
function [ns, ss, st] = op2(obj, context)
...
end
function proxy = op3(obj, context)
...
end
function future = op1Async(obj, context)
...
end
function future = op2Async(obj, context)
...
end
function future = op3Async(obj, context)
...
end
end
end

Optional parameters use the same mapping as required parameters, with one difference: the parameter accepts Ice.Unset as a valid value.

Consider the following operation:

Slice
optional(1) int execute(optional(2) string p, out optional(3) float value);

A client can invoke this operation as shown below:

MATLAB
[i, v] = proxy.execute('--file log.txt');
[i, v] = proxy.execute(Ice.Unset);
if v ~= Ice.Unset
fprintf('value = %f\n', v); % v is set to a value
end

A well-behaved program must always test an optional parameter prior to using its value. Keep in mind that the Ice.Unset marker value has different semantics than an empty array. Since an empty array is a legal value for certain Slice types, the Ice runtime requires a separate marker value so that it can determine whether an optional parameter is set. An optional parameter set to an empty array is considered to be set.

As we saw in the Client-Side PHP Mapping for Interfaces, for each operation on an interface, a proxy object narrowed to that interface’s type supports a method with the same name. To invoke an operation, you call it via the proxy. For example, here is our definition from the greeter example:

Slice
module VisitorCenter
{
interface Greeter
{
string greet(string name);
}
}

Given a proxy to an object of type Greeter, the client can invoke the greet operation as follows:

PHP
$greeter = VisitorCenter\GreeterPrxHelper::createProxy(
$communicator, 'greeter:tcp -h localhost -p 4061');
$greeting = $greeter->greet('Alice'); // Get name via RPC

Any operation invocation may throw a runtime exception and, if the operation has an exception specification, may also throw user exceptions. Suppose we have the following simple interface:

Slice
exception Tantrum
{
string reason;
}
interface Child
{
void askToCleanUp() throws Tantrum;
}

Slice exceptions are thrown as PHP exceptions, so you can simply enclose one or more operation invocations in a try-catch block:

PHP
$child = ... // Get child proxy...
try {
$child->askToCleanUp();
} catch(Tantrum $t) {
echo "The child says: " . $t->reason . "\n";
}

The PHP mapping for in parameters guarantees that the value of a parameter will not be changed by the invocation.

Here is an interface with operations that pass parameters of various types from client to server:

Slice
struct NumberAndString
{
int x;
string str;
}
sequence<string> StringSeq;
dictionary<long, StringSeq> StringTable;
interface ClientToServer
{
void op1(int i, float f, bool b, string s);
void op2(NumberAndString ns, StringSeq ss, StringTable st);
void op3(ClientToServer* proxy);
}

The Slice compiler generates the following methods for this definition:

PHP
function op1($i, $f, $b, $s, $context=null);
function op2($ns, $ss, $st, $context=null);
function op3($proxy, $context=null);

Given a proxy to a ClientToServer object, the client code can pass parameters as in the following example:

PHP
$p = ... // Get proxy...
$p->op1(42, 3.14, true, "Hello world!"); // Pass simple literals
$i = 42;
$f = 3.14;
$b = true;
$s = "Hello world!";
$p->op1($i, $f, $b, $s); // Pass simple variables
$ns = new NumberAndString;
$ns->x = 42;
$ns->str = "The Answer";
$ss = array("Hello world!");
$st = array();
$st[0] = $ss;
$p->op2($ns, $ss, $st); // Pass complex variables
$p->op3($p); // Pass proxy

Out parameters are passed by reference. Here is the same Slice definition we saw earlier, but this time with all parameters being passed in the out direction:

Slice
struct NumberAndString
{
int x;
string str;
}
sequence<string> StringSeq;
dictionary<long, StringSeq> StringTable;
interface ServerToClient
{
void op1(out int i, out float f, out bool b, out string s);
void op2(out NumberAndString ns,
out StringSeq ss,
out StringTable st);
void op3(out ServerToClient* proxy);
}

The PHP mapping looks the same as it did for the in parameters version:

PHP
function op1($i, $f, $b, $s, $context=null);
function op2($ns, $ss, $st, $context=null);
function op3($proxy, $context=null);

Given a proxy to a ServerToClient object, the client code can receive the results as in the following example:

PHP
$p = ... // Get proxy...
$p->op1($i, $f, $b, $s);
$p->op2($ns, $ss, $st);
$p->op3($stcp);

Note that it is not necessary to use the reference operator (&) before each argument because the Ice runtime forces each out parameter to have reference semantics.

Ice validates the arguments to a proxy invocation at runtime and reports any type mismatches as a InvalidArgumentException exception.

Some Slice types naturally have "empty" or "not there" semantics. Specifically, sequences, dictionaries, and strings all can be null, but the corresponding Slice types do not have the concept of a null value. To make life with these types easier, whenever you pass null as a parameter or return value of type sequence, dictionary, or string, the Ice runtime automatically sends an empty sequence, dictionary, or string to the receiver.

This behavior is useful as a convenience feature: especially for deeply-nested data types, members that are sequences, dictionaries, or strings automatically arrive as an empty value at the receiving end. This saves you having to explicitly initialize, for example, every string element in a large sequence before sending the sequence in order to avoid a run-time error. Note that using null parameters in this way does not create null semantics for Slice sequences, dictionaries, or strings. As far as the object model is concerned, these do not exist (only empty sequences, dictionaries, and strings do). For example, it makes no difference to the receiver whether you send a string as null or as an empty string: either way, the receiver sees an empty string.

Optional parameters use the same mapping as required parameters. The only difference is that Ice\None can be passed as the value of an optional parameter or return value. Consider the following operation:

Slice
optional(1) int execute(optional(2) string params, out optional(3) float value);

A client can invoke this operation as shown below:

PHP
$i = $proxy->execute("--file log.txt", $v);
$i = $proxy->execute(\Ice\None, $v);
if($v != \Ice\None)
{
echo "value = " . $v . "\n";
}

A well-behaved program must always compare an optional parameter to \Ice\None prior to using its value. Keep in mind that the \Ice\None marker value has different semantics than null. Since null is a legal value for certain Slice types, the Ice runtime requires a separate marker value so that it can determine whether an optional parameter is set. An optional parameter set to null is considered to be set.

As we saw in the Client-Side Python Mapping for Interfaces, for each operation on an interface, the generated proxy class contains 2 methods for this operation. To invoke an operation, you call one of these methods on the proxy. For example, let’s take the generated code from the greeter example:

Slice
module VisitorCenter
{
interface Greeter
{
string greet(string name);
}
}

The proxy class generated from the Greeter interface, after removing extra details, is as follows:

Python
class GreeterPrx(ObjectPrx):
def greet(self, name: str, context: dict[str, str] | None = None) -> str:
# ...
def greetAsync(self, name: str, context: dict[str, str] | None = None) -> Awaitable[str]:
# ...

Given a proxy to an object of type Greeter, the client can invoke the greet operation as follows:

Python
greeter = VisitorCenter.GreeterPrx(communicator, "greeter:tcp -h localhost -p 4061")
greeting = await greeter.greetAsync("Alice") # Get name via RPC

For each operation, the Slice compiler generates 2 methods on the proxy class:

  • a “sync” method with the same name as the operation. When you call this method, your program waits synchronously until the invocation completes. A successful invocation completes with a return value (which can be void), while an unsuccessful invocation completes with an exception.
  • an “async” method, named <operation-name>Async. When you call this method, your program marshals the arguments to the method synchronously, but the remainder of this invocation is asynchronous, and the method returns a future immediately. These async methods are described in more detail in Asynchronous Method Invocation (AMI) in Python.

Any operation invocation may throw a runtime exception and, if the operation has an exception specification, may also throw user exceptions. Suppose we have the following simple interface:

Slice
exception Tantrum
{
string reason;
}
interface Child
{
void askToCleanUp() throws Tantrum;
}

Slice exceptions are thrown as Python exceptions, so you can simply enclose one or more operation invocations in a try-except block:

Python
child = ... # Get child proxy...
try:
await child.askToCleanUpAsync()
except Tantrum as t:
print(f"The child says: {t.reason}")

As we saw in the Server-Side Python Mapping for Interfaces, for each operation on an interface, the generated skeleton class contains an abstract method with the same name.

For example, let’s take the generated code from the greeter example:

Slice
module VisitorCenter
{
interface Greeter
{
string greet(string name);
}
}

The skeleton class generated from the Greeter interface, after removing extra details, is as follows:

Python
class Greeter(Object, ABC):
@abstractmethod
def greet(self, name: str, current: Current) -> str | Awaitable[str]:
pass

The greet method takes a name: str and a Current, then returns a value of type string directly or held in an Awaitable. This method should be implemented in your derived servant class with something like:

Python
class Chatbot(VisitorCenter.Greeter):
def greet(self, name: str, current: Ice.Current) -> str:
return f"Hello, {name}!"

The ["amd"] metadata has no effect in Python: you can implement the mapped method either synchronously (as in the example above) or asynchronously, as discussed in Asynchronous Method Dispatch (AMD) in Python.

To throw an exception from an operation implementation, you simply construct the exception and throw it. For example:

Python
def write(self, text: list[str], current: Ice.Current) -> None:
# Try to write the file contents here...
# Assume we are out of space...
if error:
raise Filesystem.WriteException("file too large")

If you throw an arbitrary Python exception (such as a ValueError), the Ice runtime catches the exception and then returns an UnknownException to the client.

If you throw an Ice runtime exception, such as MarshalException, the client receives an UnknownLocalException.

The server-side Ice runtime does not validate user exceptions thrown by an operation implementation to ensure they are compatible with the operation's Slice definition. Rather, Ice returns the user exception to the client, where the client-side runtime will validate the exception as usual and throws UnknownUserException for an unexpected exception type.

Asynchronous Method Invocation (AMI) is the term used to describe the client-side support for the asynchronous programming model. AMI supports both oneway and twoway requests, but unlike their synchronous counterparts, AMI requests never block the calling thread. When a client issues an AMI request, the Ice runtime hands the message off to the local transport buffer or, if the buffer is currently full, queues the request for later delivery. The application can then continue its activities and poll or wait for completion of the invocation, or receive a callback when the invocation completes.

AMI is transparent to the server: there is no way for the server to tell whether a client sent a request synchronously or asynchronously.

Consider the following Slice definition:

Slice
module VisitorCenter
{
interface Greeter
{
string greet(string name);
}
}

slice2py generates the following asynchronous proxy method:

Python
def greetAsync(
self, name: str, context: dict[str, str] | None = None) -> Awaitable[str]:
...

As you can see, the greet operation generates a greetAsync method that accepts an optional per-invocation request context.

The greetAsync sends (or queues) an invocation of greet. This method does not block the calling thread. It returns an awaitable object that you typically await.

For example:

Python
# On asyncio event loop thread.
async with Ice.initialize(
sys.argv, eventLoop=asyncio.get_running_loop()) as communicator:
greeter = VisitorCenter.GreeterPrx(
communicator, "greeter:tcp -h localhost -p 4061")
# Invoke the greetAsync method and await the result in the event loop thread.
greeting = await greeter.greetAsync(getpass.getuser())

If an asynchronous invocation throws an exception, the exception can be obtained from the awaitable.

The exception is provided by the awaitable, even if the actual error condition for the exception was encountered during the call to the Async method ("on the way out"). The advantage of this behavior is that all exception handling is located with the code that awaits the result.

There are two exceptions to this rule:

  • if you destroy the communicator and then make an asynchronous invocation, the Async method throws CommunicatorDestroyedException directly.
  • a call to an Async method can throw TwowayOnlyException. An Async method throws this exception if you call an operation that has a return value or out-parameters on a oneway proxy.

This distinction is only relevant if you are using the Future APIs directly, when using await you handle exceptions throw synchronously and asynchronously with the same except block.

asyncio.Future, Ice.Future, and future types created by a custom event loop adapter are all awaitable objects—meaning they can be used as the target of the await keyword.

The type of awaitable object returned by Ice’s asynchronous APIs and generated asynchronous methods depends on the configured event loop adapter:

  • Default(no event loop adapter configured) Ice returns Ice.Future objects, including Ice.InvocationFuture for invocations.
  • With an asyncio event loop Ice returns asyncio.Future objects when the communicator is initialized with an asyncio event loop.
  • With a custom event loop adapter Ice returns custom awaitable objects provided by the application’s EventLoopAdapter implementation.

Ice 3.8 provides seamless integration with Python’s asyncio library.

If you supply an asyncio event loop during communicator initialization using the eventLoop parameter of Ice.initialize, asynchronous operations will return standard asyncio.Future objects instead of Ice’s own future types. This allows you to await asynchronous invocations directly within the asyncio event loop.

Python
async with Ice.initialize(
sys.argv,
eventLoop=asyncio.get_running_loop()) as communicator:
greeter = VisitorCenter.GreeterPrx(
communicator,
"greeter:tcp -h localhost -p 4061")
# Send a request to the remote object and get the response.
greeting = await greeter.greetAsync(getpass.getuser())

The same mechanism can be used to integrate Ice with other asynchronous event loop frameworks. Instead of passing an asyncio loop directly, you must implement the Ice.EventLoopAdapter abstract base class for your event loop of choice and provide it during communicator initialization via the InitializationData.eventLoopAdapter member.

You can only await a future from the event loop that created it:

  • Ice.Future and Ice.InvocationFuture These are tied to the Ice thread pool. You cannot normally await them from a regular Python thread or from within asyncio.

    Example (❌ does not work):

    Python
    with Ice.initialize(sys.argv) as communicator:
    greeter = VisitorCenter.GreeterPrx(
    communicator,
    "greeter:tcp -h localhost -p 4061")
    # Will fail because the returned Ice.InvocationFuture
    # cannot be awaited from a regular Python thread
    greeting = await greeter.greetAsync(getpass.getuser())

    However, you can await an Ice.InvocationFuture from inside an asynchronous dispatch (AMD), since these coroutines run on the Ice thread pool:

    Example (✅ works inside AMD with Ice futures):

    Python
    async def greet(self, name: str, current: Ice.Current) -> str:
    # Using await here is fine because the async dispatch
    # runs in the Ice thread pool
    return await self.target.greetAsync(name)
  • asyncio.Future These belong to the asyncio event loop supplied during communicator initialization. Since asynchronous dispatches also run in this loop, it is safe to await asyncio.Future objects inside an asyncio-based dispatch.

    Example (✅ works in asyncio client):

    Python
    async with Ice.initialize(
    sys.argv,
    eventLoop=asyncio.get_running_loop()) as communicator:
    greeter = VisitorCenter.GreeterPrx(
    communicator, "greeter:tcp -h localhost -p 4061")
    # Fine: we are running in the asyncio event loop
    # and greetAsync returns an asyncio.Future
    greeting = await greeter.greetAsync(getpass.getuser())

    Example (✅ works in asyncio-based AMD):

    Python
    async def greet(self, name: str, current: Ice.Current) -> str:
    # Using await here is also fine because the async dispatch
    # runs in the configured event loop (asyncio in this case)
    return await self.target.greetAsync(name)

You can invoke operations via oneway proxies asynchronously, provided the operation has void return type, does not have any out-parameters, and does not raise user exceptions. If you call an asynchronous proxy method on a oneway proxy for an operation that returns values or raises a user exception, the method throws TwowayOnlyException.

Oneway invocation completes as soon as the request is successfully written to the client-side transport. Exceptions are only reported if an error occurs before the request is successfully written.

Asynchronous Method Dispatch (AMD) is the server-side equivalent of AMI. With AMD, you can process dispatches asynchronously, allowing the server to optimize resource usage and serve more clients compared to processing all dispatches synchronously.

In Python, however, concurrency has an additional restriction: only one Python thread can execute at a time because of the Global Interpreter Lock (GIL). This makes it especially important to avoid synchronous blocking calls in dispatch operations.

For example, consider the following synchronous dispatch:

Python
def greet(self, name: str, current: Ice.Current) -> str:
return self._db.getGreet(name)

If _db.getGreet blocks while waiting for the database, the thread handling this dispatch cannot perform other work until the call returns.

With AMD, you can avoid this blocking:

Python
async def greet(self, name: str, current: Ice.Current) -> str:
return await self._db.getGreetAsync(name)

In this version, the thread does not remain blocked while getGreetAsync runs. Instead, it can execute other tasks, and the coroutine resumes on the appropriate thread once the database result becomes available.

Annotating operations with ["amd"] metadata directives has no effect in the Python mapping. The mappings for synchronous and asynchronous dispatch are nearly identical—the only difference is the return type:

  • An operation has asynchronous semantics if it is implemented as an async method or if it returns an awaitable object.
  • Otherwise, the operation has synchronous semantics.

The parameter passing rules for in parameters are the same in both cases.

Consider the Greeter example:

Slice
module VisitorCenter
{
interface Greeter
{
string greet(string name);
}
}

The server can choose to implement the operation synchronously or asynchronously.

Synchronous version:

Python
def greet(self, name: str, current: Ice.Current) -> str:
print(f"Dispatching greet request {{ name = '{name}' }}")
return f"Hello, {name}!"

Asynchronous version (coroutine):

Python
async def greet(self, name: str, current: Ice.Current) -> str:
await asyncio.sleep(1)
print(f"Dispatching greet request {{ name = '{name}' }}")
return f"Hello, {name}!"

Ice provides seamless integration with Python’s asyncio library.

If you supply an asyncio event loop during communicator initialization using the eventLoop parameter of Ice.initialize, asynchronous dispatch will run on the asyncio event loop. This allows you to await asynchronous invocations directly within the dispatch implementation.

The same mechanism can be used to integrate Ice with other asynchronous event loop frameworks. Instead of passing an asyncio loop directly, you must implement the Ice.EventLoopAdapter abstract base class for your event loop of choice and provide it during communicator initialization via the InitializationData.eventLoopAdapter member.

Because proxy invocations return awaitables and asynchronous dispatch methods may also return awaitables, it’s straightforward to chain calls—provided the operations have the same result type and compatible user-exception sets.

Continuing with the Greeter example, the servant can delegate directly to another Greeter:

Python
def greet(self, name: str, current: Ice.Current) -> str:
return self._greeter.greetAsync(name)

Or, using async/await:

Python
# Coroutine implementation (AMD semantics)
async def greet(self, name: str, current: Ice.Current) -> str:
return await self._greeter.greetAsync(name)

The greet dispatch is implemented by delegating to another Greeter server, and we directly return the result from the nested async invocation.

All parameters are passed by reference in the Python mapping; it is guaranteed that the value of a parameter will not be changed by the invocation.

Here is an interface with operations that pass parameters of various types from client to server:

Slice
struct NumberAndString
{
int x;
string str;
}
sequence<string> StringSeq;
dictionary<long, StringSeq> StringTable;
interface ClientToServer
{
void op1(int i, float f, bool b, string s);
void op2(NumberAndString ns, StringSeq ss, StringTable st);
void op3(ClientToServer* proxy);
}

The Slice compiler generates the following proxy for this definition:

Python
class ClientToServerPrx(Ice.ObjectPrx):
def op1(self, i, f, b, s, context=None):
# ...
def op2(self, ns, ss, st, context=None):
# ...
def op3(self, proxy, context=None):
# ...

Given a proxy to a ClientToServer interface, the client code can pass parameters as in the following example:

Python
p = ... # Get proxy...
p.op1(42, 3.14, True, "Hello world!") # Pass simple literals
i = 42
f = 3.14
b = True
s = "Hello world!"
p.op1(i, f, b, s) # Pass simple variables
ns = NumberAndString()
ns.x = 42
ns.str = "The Answer"
ss = [ "Hello world!" ]
st = {}
st[0] = ss
p.op2(ns, ss, st) # Pass complex variables
p.op3(p) # Pass proxy

As in Java, Python functions do not support reference arguments. That is, it is not possible to pass an uninitialized variable to a Python function in order to have its value initialized by the function.

The semantics of out parameters in the Python mapping depend on whether the operation returns one value or multiple values. An operation returns multiple values when it has declared multiple out parameters, or when it has declared a non-void return type and at least one out parameter.

If an operation returns multiple values, the client receives them in the form of a result tuple. A non-void return value, if any, is always the first element in the result tuple, followed by the out parameters in the order of declaration.

If an operation returns only one value, the client receives the value itself.

Here again are the same Slice definitions we saw earlier, but this time with all parameters being passed in the out direction:

Slice
struct NumberAndString
{
int x;
string str;
}
sequence<string> StringSeq;
dictionary<long, StringSeq> StringTable;
interface ServerToClient
{
int op1(out float f, out bool b, out string s);
void op2(out NumberAndString ns,
out StringSeq ss,
out StringTable st);
void op3(out ServerToClient* proxy);
}

The Python mapping generates the following code for this definition:

Python
class ServerToClientPrx(Ice.ObjectPrx):
def op1(self, context=None):
# ...
def op2(self, context=None):
# ...
def op3(self, context=None):
# ...

Given a proxy to a ServerToClient interface, the client code can receive the results as in the following example:

Python
p = ... # Get proxy...
i, f, b, s = p.op1()
ns, ss, st = p.op2()
stcp = p.op3()

The operations have no in parameters, therefore no arguments are passed to the proxy methods. Since op1 and op2 return multiple values, their result tuples are unpacked into separate values, whereas the return value of op3 requires no unpacking.

Although the Python compiler cannot check the types of arguments passed to a function, the Ice run time does perform validation on the arguments to a proxy invocation and reports any type mismatches as a ValueError exception.

Some Slice types naturally have "empty" or "not there" semantics. Specifically, sequences, dictionaries, and strings all can be None, but the corresponding Slice types do not have the concept of a null value. To make life with these types easier, whenever you pass None as a parameter or return value of type sequence, dictionary, or string, the Ice run time automatically sends an empty sequence, dictionary, or string to the receiver.

This behavior is useful as a convenience feature: especially for deeply-nested data types, members that are sequences, dictionaries, or strings automatically arrive as an empty value at the receiving end. This saves you having to explicitly initialize, for example, every string element in a large sequence before sending the sequence in order to avoid a run-time error. Note that using null parameters in this way does not create null semantics for Slice sequences, dictionaries, or strings. As far as the object model is concerned, these do not exist (only empty sequences, dictionaries, and strings do). For example, it makes no difference to the receiver whether you send a string as None or as an empty string: either way, the receiver sees an empty string.

Optional parameters use the same mapping as required parameters. The only difference is that None can be passed as the value of an optional parameter or return value. Consider the following operation:

Slice
optional(1) int execute(optional(2) string params, out optional(3) float value);

The corresponding Python proxy method is:

Python
def execute(
self,
params: str | None = None,
context: dict[str, str] | None = None) -> tuple[int | None, float | None]:
...
def executeAsync(
self,
params: str | None = None,
context: dict[str, str] | None = None) -> Awaitable[
tuple[int | None, float | None]]:
...

and the corresponding Python skeleton method is:

Python
@abstractmethod
def execute(
self,
params: str | None,
current: Current) -> tuple[
int | None, float | None] | Awaitable[tuple[int | None, float | None]]:
...

As we saw in the Client-Side Ruby Mapping for Interfaces, for each operation on an interface, the generated proxy class contains a method with the same name. To invoke an operation, you call this method on the proxy. For example, let’s take the generated code from the greeter example:

Slice
module VisitorCenter
{
interface Greeter
{
string greet(string name);
}
}

The proxy class generated from the Greeter interface, after removing extra details, is as follows:

Ruby
module ::VisitorCenter
module GreeterPrx_mixin
def greet(name, context=nil)
GreeterPrx_mixin::OP_greet.invoke(self, [name], context)
end
end
class GreeterPrx < Ice::ObjectPrx
include GreeterPrx_mixin
end
# ...
end

Given a proxy to an object of type Greeter, the client can invoke the greet operation as follows:

Ruby
greeter = VisitorCenter::GreeterPrx.new(
communicator, "greeter:tcp -h localhost -p 4061")
greeting = greeter.greet("Alice") # Get name via RPC

Any operation invocation may throw a runtime exception and, if the operation has an exception specification, may also throw user exceptions. Suppose we have the following simple interface:

Slice
exception Tantrum
{
string reason;
}
interface Child
{
void askToCleanUp() throws Tantrum;
}

Slice exceptions are thrown as Ruby exceptions, so you can simply enclose one or more operation invocations in a begin-rescue block:

Ruby
child = ... # Get child proxy...
begin
child.askToCleanUp()
rescue Tantrum => t
puts "The child says: #{t.reason}"
end

All parameters are passed by reference in the Ruby mapping; it is guaranteed that the value of a parameter will not be changed by the invocation.

Here is an interface with operations that pass parameters of various types from client to server:

Slice
struct NumberAndString
{
int x;
string str;
}
sequence<string> StringSeq;
dictionary<long, StringSeq> StringTable;
interface ClientToServer
{
void op1(int i, float f, bool b, string s);
void op2(NumberAndString ns, StringSeq ss, StringTable st);
void op3(ClientToServer* proxy);
}

The Slice compiler generates the following proxy for this definition:

Ruby
module ClientToServerPrx_mixin
def op1(i, f, b, s, context=nil)
...
end
def op2(ns, ss, st, context=nil)
...
end
def op3(proxy, context=nil)
...
end
end
class ClientToServerPrx < Ice::ObjectPrx
include ClientToServerPrx_mixin
end

Given a proxy to a ClientToServer interface, the client code can pass parameters as in the following example:

Ruby
p = ... # Get proxy...
p.op1(42, 3.14, true, "Hello world!") # Pass simple literals
i = 42
f = 3.14
b = true
s = "Hello world!"
p.op1(i, f, b, s) # Pass simple variables
ns = NumberAndString.new()
ns.x = 42
ns.str = "The Answer"
ss = [ "Hello world!" ]
st = {}
st[0] = ss
p.op2(ns, ss, st) # Pass complex variables
p.op3(p) # Pass proxy

The return value of a mapped method depends on how many values the corresponding Slice operation returns, including out parameters and a non-void return value:

  • Zero values The corresponding Ruby method returns void. For the purposes of this discussion, we're not interested in these operations.
  • One value The client receives a single return value, regardless of whether the Slice definition of the operation declared it as a return value or as an out parameter.
  • Two or more value The client receives them in the form of a result array. A non-void return value, if any, is always the first element in the result array, followed by the out parameters in the order of declaration.

Here again are the same Slice definitions we saw earlier, but this time with all parameters being passed in the out direction:

Slice
struct NumberAndString
{
int x;
string str;
}
sequence<string> StringSeq;
dictionary<long, StringSeq> StringTable;
interface ServerToClient
{
int op1(out float f, out bool b, out string s);
void op2(out NumberAndString ns,
out StringSeq ss,
out StringTable st);
void op3(out ServerToClient* proxy);
}

The Ruby mapping generates the following code for this definition:

Ruby
def op1(context=nil)
def op2(context=nil)
def op3(context=nil)

Given a proxy to a ServerToClient interface, the client code can receive the results as in the following example:

Ruby
p = ... # Get proxy...
i, f, b, s = p.op1()
ns, ss, st = p.op2()
stcp = p.op3()

The operations have no in parameters, therefore no arguments are passed to the proxy methods. Since op1 and op2 return multiple values, their result arrays are unpacked into separate values, whereas the return value of op3 requires no unpacking.

Ice validates the arguments to a proxy invocation at runtime and reports any type mismatches as a TypeError exception.

Some Slice types naturally have "empty" or "not there" semantics. Specifically, sequences, dictionaries, and strings all can be nil, but the corresponding Slice types do not have the concept of a null value. To make life with these types easier, whenever you pass nil as a parameter or return value of type sequence, dictionary, or string, the Ice runtime automatically sends an empty sequence, dictionary, or string to the receiver.

This behavior is useful as a convenience feature: especially for deeply-nested data types, members that are sequences, dictionaries, or strings automatically arrive as an empty value at the receiving end. This saves you having to explicitly initialize, for example, every string element in a large sequence before sending the sequence in order to avoid a run-time error. Note that using null parameters in this way does not create null semantics for Slice sequences, dictionaries, or strings. As far as the object model is concerned, these do not exist (only empty sequences, dictionaries, and strings do). For example, it makes no difference to the receiver whether you send a string as nil or as an empty string: either way, the receiver sees an empty string.

Optional parameters use the same mapping as required parameters. The only difference is that Ice::Unset can be passed as the value of an optional parameter or return value. Consider the following operation:

Slice
optional(1) int execute(optional(2) string p, out optional(3) float value);

A client can invoke this operation as shown below:

Ruby
i, v = proxy.execute("--file log.txt")
i, v = proxy.execute(Ice::Unset)
if v != Ice::Unset
puts "value = " + v.to_s
end

A well-behaved program must always compare an optional parameter to Ice::Unset prior to using its value. Keep in mind that the Ice::Unset marker value has different semantics than nil. Since nil is a legal value for certain Slice types, the Ice runtime requires a separate marker value so that it can determine whether an optional parameter is set. An optional parameter set to nil is considered to be set.

As we saw in the Client-Side Swift Mapping for Interfaces, for each operation on an interface, the generated proxy protocol extension contains a method with the same name. To invoke an operation, you call this method on the proxy. For example, let’s take the generated code from the greeter example:

Slice
module VisitorCenter
{
interface Greeter
{
string greet(string name);
}
}

The proxy protocol generated from the Greeter interface, after removing extra details, is as follows:

Swift
public protocol GreeterPrx: Ice.ObjectPrx {}
public extension GreeterPrx {
func greet(
_ iceP_name: Swift.String,
context: Ice.Context? = nil
) async throws -> Swift.String {
// ...
}
}
public extension NodePrx {
func name(context: Ice.Context? = nil) async throws -> String {
...
}
}

Given a proxy to an object of type Greeter, the client can invoke the greet operation as follows:

Swift
let greeter = try makeProxy(
communicator: communicator, proxyString: "greeter:tcp -h localhost -p 4061",
type: GreeterPrx.self)
let greeting = try await greeter.greet("Alice") // Get name via RPC

This code asynchronously calls greet on the proxy, which sends the request to the server, waits until the operation is complete, and then unmarshals the return value and returns it to the caller.

All proxy methods generated by the Slice compiler are async which align with Swift's structured concurrency and async/await model. Therefore, synchronous invocations are not supported.

Any operation invocation may throw a runtime exception and, if the operation has an exception specification, may also throw user exceptions. Suppose we have the following simple interface:

Slice
exception Tantrum
{
string reason;
}
interface Child
{
void askToCleanUp() throws Tantrum;
}

Slice exceptions are thrown as Swift exceptions, so you can simply enclose one or more operation invocations in a do-catch block:

Swift
let child: ChildPrx = ... // Get child proxy...
do {
try await child.askToCleanUp()
} catch let t as Tantrum {
print("The child says: \(t.reason)")
}

As we saw in the Server-Side Swift Mapping for Interfaces, for each operation on an interface, the generated skeleton protocol contains an abstract method with the same name.

For example, let’s take the generated code from the greeter example:

Slice
module VisitorCenter
{
interface Greeter
{
string greet(string name);
}
}

The skeleton protocol generated from the Greeter interface, after removing extra details, is as follows:

Swift
public protocol Greeter: Ice.Dispatcher {
func greet(name: String, current: Ice.Current) async throws -> String
}

The greet method takes a name: String and a Current, then returns a value of type String. This method should be implemented in your derived servant class with something like:

Swift
struct Chatbot: Greeter {
func greet(name: String, current _: Ice.Current) -> String {
return "Hello, \(name)!"
}
}

Since our implementation dot perform call any asynchronous operations or throw any exception, we can omit the async and throws keywords from the function declaration.

The ["amd"] metadata has no effect in Swift. All dispatch methods generated by the Slice compiler are async which align with Swift's structured concurrency and async/await model.

To throw an exception from an operation implementation, you simply construct the exception and throw it. For example:

Swift
func write(text: [String], current _: Ice.Current) throws {
...
if (error) {
throw WriteException(reason: "file too large")
}
}

If you throw an arbitrary Swift exception, the Ice runtime catches the exception and then returns an UnknownException to the client.

If you throw an Ice runtime exception, such as MarshalException, the client receives an UnknownLocalException.

The server-side Ice runtime does not validate user exceptions thrown by an operation implementation to ensure they are compatible with the operation's Slice definition. Rather, Ice returns the user exception to the client, where the client-side runtime will validate the exception as usual and throws UnknownUserException for an unexpected exception type.

An in parameter is mapped to a Swift parameter with the same name; its type is the mapped Swift type.

For example, a Slice parameter string name is mapped to a Swift parameter name with type String. The rules are the same as for Fields.

The mapped method (in the proxy and skeleton) always uses the Slice parameter names as parameter labels except in one situation: when the operation has a single parameter, the mapped proxy method doesn’t use any label for this sole parameter. (But the mapped skeleton method does, as usual.).

Consider the following Slice interface:

Slice
interface Greeter
{
string greet(string name);
}

The generated code is as follows:

Swift
// Client-side
public protocol GreeterPrx: Ice.ObjectPrx {}
public extension GreeterPrx {
func greet(
_ iceP_name: String, context: Ice.Context? = nil) async throws ->
Swift.String {
...
}
}
// Server-side
public protocol Greeter: Ice.Dispatcher {
func greet(name: String, current: Ice.Current) async throws -> Swift.String
}

The mapping for an operation depends on how many values it returns, including out parameters and a non-void return value:

  • Zero values The corresponding Swift method returns nothing. For the purposes of this discussion, we're not interested in these operations.
  • One value The corresponding Swift method returns the mapped type, regardless of whether the Slice definition of the operation declared it as a return value or as an out parameter.
  • Multiple values The mapped Swift method returns a tuple. If the operation declares a return value, this value is provided as the first element of the tuple with the name returnValue.

Consider this example:

Slice
interface Example
{
double op(int inp1, string inp2, out bool outp1, out long outp2);
}

The Slice compiler generates the following Swift code for this interface:

Swift
// Client-side
public protocol ExamplePrx: Ice.ObjectPrx {}
public extension ExamplePrx {
func op(inp1: Int32, inp2: String, context: Ice.Context? = nil) async throws ->
(returnValue: Double, outp1: Bool, outp2: Int64) {
...
}
}
// Server-side
public protocol Example: Ice.Dispatcher {
func op(inp1: Int32, inp2: String, current: Ice.Current) async throws ->
(returnValue: Double, outp1: Bool, outp2: Int64)
}

An optional parameter is mapped to a Swift parameter with the corresponding Swift optional type.

Consider the following operation:

Slice
optional(1) int execute(optional(2) string p);

The corresponding proxy function is:

Swift
func execute(_ iceP_p: String? = nil, context: Ice.Context? = nil) async throws ->
Int32? {
...
}

Mapped optional parameters get a default value (nil). As a result, if you don’t specify an argument for an optional parameter when making an invocation, the target object receives “not set” for this parameter.