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
Operation Syntax
An operation definition must contain a name (the operation’s name), a return type and zero or more parameter definitions.
For example:
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:
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:
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:
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:
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:
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.
Optional Parameters and Return Values
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:
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:
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.
Overloading Operations
Slice does not support any form of overloading of operations. For example:
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.
Idempotent Operations
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:
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
idempotentoperations, 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.)
Client-Side Mapping for Operations
Mapping for Operations
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:
module VisitorCenter{ interface Greeter { string greet(string name); }}The proxy class generated from the Greeter interface, after removing extra details, is as follows:
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:
GreeterPrx greeter{communicator, "greeter:tcp -h localhost -p 4061"};string greeting = greeter.greet("Alice"); // Get greeting via RPCThis 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:
GreeterPrx greeter{communicator, "greeter:tcp -h localhost -p 4061"};greeter.greet("Alice"); // Useless, but no leakThis 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.
Sync and Async Functions
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 anstd::futureor a callback depending on the async overload you selected. These async functions are described in more detail in Asynchronous Method Invocation (AMI) in C++.
Exception Handling
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:
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:
ChildPrx child = ...; // Get Child proxy...try{ child.askToCleanUp(); // Give it a try...}catch (const Tantrum& t){ cout << "The child says: " << t.reason << endl;}Server-Side Mapping for Operations
Default Mapping for Operations
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:
module VisitorCenter{ interface Greeter { string greet(string name); }}The skeleton class generated from the Greeter interface, after removing extra details, is as follows:
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:
class Chatbot : public VisitorCenter::Greeter{public: std::string greet(std::string name, const Ice::Current&) override { ostringstream os; os << "Hello, " << name << "!"; return os.str(); }};AMD Mapping for Operations
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.
Throwing Exceptions
To throw an exception from an operation implementation, you simply construct this exception and throw it. For example:
voidMFile::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)
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.
Asynchronous Exception Semantics
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
Asyncfunction throwsCommunicatorDestroyedException. This is necessary because, once the communicator is destroyed, its client thread pool is no longer available. - a call to an
Asyncfunction can throwTwowayOnlyException. AnAsyncfunction throws this exception if you call an operation that has a return value or out-parameters on a oneway proxy.
Asynchronous Oneway Invocations
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.
Canceling an Asynchronous Invocation
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:
EmployeesPrx e = ... // get an Employees proxyauto cancel = e.getNameAsync( 99, [](string name) { cout << "Employee name is: " << name << endl; });
cancel(); // no longer interested in this nameCalling 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.
Polling for Completion
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:
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:
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:
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.
Sent Callbacks
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
Asyncfunction, and passestrueas argument. Note that in this case the sent callback executes during the call to theAsyncfunction, 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
falseas 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:
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.
Flow Control
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:
EmployeesPrx e = ...; // get an Employees proxy
e.getNameAsync( 99, [](string name) { ... handle name ... }, [](exception_ptr ex) { ... handle exception ... }, [](bool) { ... increase sent counter ... });Asynchronous Method Dispatch (AMD)
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.
Async Skeleton
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:
// This servant uses AMDclass Chatbot : public VisitorCenter::AsyncGreeter{public: // your implementation here};Enabling AMD Piecemeal
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:
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.
AMD Mapping
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:
interface Example{ string op(short s, out long l);}Operation op is mapped as follows on the async skeleton (AsyncExample):
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"].
AMD Exceptions
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.
Chaining AMI and AMD Invocations
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:
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;};Mapping for Parameters and Return Values
In Parameters
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 Type | Mapped 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> |
Out Parameters in Synchronous Functions
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:
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):
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);};Return Values in Synchronous Functions
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”.
Out Parameters and Return Values in Asynchronous Functions
Future-Returning Proxy Functions
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.
Callback Proxy Functions
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.
AMD Skeleton Functions
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.
Optional Parameters
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:
optional(1) int execute(optional(2) string params, out optional(3) float value);The corresponding C++ proxy function is:
// Synchronous variantstd::optional<std::int32_t> execute(std::optional<std::string_view> params, std::optional<float>& value, ...);and the corresponding C++ skeleton function is:
// Synchronous variantvirtual std::optional<std::int32_t> execute(std::optional<std::string> params, std::optional<float>& value, ...);See Also
Client-Side Mapping for Operations
Mapping for Operations
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:
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:
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:
GreeterPrx greeter = GreeterPrxHelper.createProxy( communicator, "greeter:tcp -h localhost -p 4061");
string greeting = await greeter.GreetAsync("Alice"); // Get greeting via RPCSync and Async Methods
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 aTask. These async methods are described in more detail in Asynchronous Method Invocation (AMI) in C#.
Exception Handling
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:
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:
ChildPrx child = ...; // Get child proxy...
try{ await child.AskToCleanUpAsync();}catch (Tantrum t){ Console.WriteLine($"The child says: {t.Reason}");}Server-Side Mapping for Operations
Default Mapping for Operations
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:
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:
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:
internal class Chatbot : VisitorCenter.GreeterDisp_{ public override string Greet(string name, Ice.Current current) => $"Hello, {name}!";}AMD Mapping for Operations
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.
Throwing Exceptions
To throw an exception from an operation implementation, you simply construct the exception and throw it. For example:
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)
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.
Asynchronous API
Consider the following Slice definition:
module Demo{ interface Employees { ["cs:identifier:GetName"] string getName(int number); }}slice2cs generates the following asynchronous proxy method:
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:
- a per-invocation request context
- a sent callback
- a cancellation token
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:
EmployeesPrx e = ...;string name = await e.GetNameAsync(99);Asynchronous Exception Semantics
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
Asyncmethod throwsCommunicatorDestroyedExceptiondirectly. - a call to an
Asyncmethod can throwTwowayOnlyException. AnAsyncmethod throws this exception if you call an operation that has a return value or out-parameters on a oneway proxy.
Asynchronous Oneway Invocations
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.
Flow Control
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:
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.
Canceling an Asynchronous Invocation
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.
Asynchronous Method Dispatch (AMD)
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.
Async Skeleton
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:
// This servant uses AMDpublic class Chatbot : VisitorCenter.AsyncGreeterDisp_{ // your implementation here};Enabling AMD Piecemeal
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:
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.
AMD Mapping
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:
interface Example{ ["cs:identifier:Op"] string op(short s, out long l);}Operation op is mapped as follows in the async skeleton class:
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"].
AMD Exceptions
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.
Mapping for Parameters and Return Values
In Parameters
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.
Out Parameters in Synchronous Methods
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:
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):
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);}Return Values in Synchronous Methods
A Slice return value is mapped to a C# return value in synchronous proxy and skeleton methods.
Out Parameters and Return Values in Asynchronous 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>, whereInterface_OpResultis a generated record struct that holds the return value and out parameters.
Consider this example:
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:
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, ...);}Optional Parameters
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:
["cs:identifier:Execute"]optional(1) int execute(optional(2) string parameters, out optional(3) float value);The corresponding C# proxy method is:
// Asynchronous variantTask<Example_ExecuteResult> ExecuteAsync( string? parameters, Dictionary<string, string>? context = null, ...);and the corresponding C# skeleton method is:
// Synchronous variantint? Execute(string? parameters, out float? value, Ice.Current current);See Also
Client-Side Mapping for Operations
Mapping for Operations
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:
["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:
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:
GreeterPrx greeter = GreeterPrx.createProxy( communicator, "greeter:tcp -h localhost -p 4061");
String greeting = greeter.greet("Alice"); // Get name via RPCSync and Async Methods
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 aCompletableFutureimmediately. These async methods are described in more detail in Asynchronous Method Invocation (AMI) in Java.
Exception Handling 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:
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:
ChildPrx child = ...; // Get child proxy...
try { child.askToCleanUp();} catch (Tantrum t) { System.out.print("The child says: "); System.out.println(t.reason);}Server-Side Mapping for Operations
Default Mapping for Operations
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:
["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:
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:
class Chatbot implements Greeter { @Override public String greet(String name, Current current) { return "Hello, " + name + "!"; }}AMD Mapping for Operations
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.
Throwing Exceptions
To throw an exception from an operation implementation, you simply construct the exception and throw it. For example:
@Overridepublic 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)
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.
Asynchronous Exception Semantics
If an invocation throws an exception, the exception can be obtained from the future in several ways:
- Call
geton the future;getthrowsCompletionExceptionwith the actual exception available viagetCause() - Call
joinon the future;jointhrowsExecutionExceptionwith the actual exception available viagetCause() - Use chaining methods such as
exceptionally,handleorwhenCompleteto 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
Asyncmethod throwsCommunicatorDestroyedExceptiondirectly. - a call to an
Asyncmethod can throwTwowayOnlyException. AnAsyncmethod throws this exception if you call an operation that has a return value or out-parameters on a oneway proxy.
InvocationFuture Class
InvocationFuture ClassThe 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.
Polling for Completion
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:
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:
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:
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.
Asynchronous Oneway Invocations
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.
Flow Control
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:
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
sentSynchronouslyargument 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
sentSynchronouslyargument 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.
Canceling an Asynchronous Invocation
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.
Concurrency Semantics for AMI
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 callwhenComplete, the action will execute immediately in the calling thread. If the future is not yet complete when you callwhenComplete, the action will eventually execute in an Ice thread pool thread. - Now suppose you configure an action using one of the
whenCompleteAsyncmethods. 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 theice_executorproxy method. With the Ice thread pool executor, the action is always queued to be executed by the Ice thread pool.
Asynchronous Method Dispatch (AMD)
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.
Async Skeleton
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:
// This servant uses AMDclass Chatbot implements AsyncGreeter { // your implementation here}Enabling AMD Piecemeal
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:
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.
AMD Mapping
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:
interface Example{ string op(short s, out long count);}Operation op is mapped as follows in the skeleton interface:
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"].
AMD Exceptions
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.
Chaining AMI and AMD Invocations
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:
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; }}Mapping for Parameters and Return Values
In Parameters
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.
Out Parameters and Return Values
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:
Sliceinterface I{string op1();void op2(out string name);}The mapping generates corresponding methods with identical signatures:
Javainterface 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, whereOprepresents 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 namedreturnValue.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:
interface Example{ double op(int inp1, string inp2, out bool outp1, out long outp2);}The generated code looks like this:
// Server-side skeletonpublic 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 proxypublic 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) { ... }}Null Parameters
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.
Optional Parameters
The mapping uses standard Java types to encapsulate optional parameters:
java.util.OptionalDoubleThe mapped type for an optionaldouble.java.util.OptionalIntThe mapped type for an optionalint.java.util.OptionalLongThe mapped type for an optionallong.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:
optional(1) int execute(optional(2) string parameters);The mapping for this operation is shown below:
// With String in-parameterjava.util.OptionalInt execute(String parameters);java.util.OptionalInt execute( String parameters, java.util.Map<String, String> context);
// With Optional<String> in-parameterjava.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-parameterCompletableFuture<java.util.OptionalInt> executeAsync(String parameters);CompletableFuture<java.util.OptionalInt> executeAsync( String parameters, java.util.Map<String, String> context);
// Async with Optional<String> in-parameterCompletableFuture<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.util.OptionalInt i;
i = proxy.execute("--file log.txt"); // required mappingi = proxy.execute(java.util.Optional.of("--file log.txt")); // optional mappingi = 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.
See Also
Client-Side Mapping for Operations
Mapping for Operations
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:
module VisitorCenter{ interface Greeter { string greet(string name); }}The generated proxy class (simplified) looks like this:
class GreeterPrx extends Ice.ObjectPrx { constructor(communicator, proxyString) { ... }
greet(name, context) { ... }
// ...}And the TypeScript declaration:
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:
const greeter = new VisitorCenter.GreeterPrx( communicator, "greeter:tcp -h localhost -p 4061");
const greeting = await greeter.greet("Alice"); // Get name via RPCIce 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.
Exception Handling
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:
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:
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; }}Server-Side Mapping for Operations
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:
module VisitorCenter{ interface Greeter { string greet(string name); }}The generated JavaScript skeleton class is:
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:
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:
greet(name: string, current: Ice.Current): PromiseLike<string> | string { return `Hello, ${name}`;}Asynchronous version:
async greet(name: string, current: Ice.Current): Promise<string> { // Nested async invocation return await this._target.greet(name);}Mapping for Parameters and Return Values
Passing Parameters in JavaScript
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:
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:
class ClientToServerPrx extends Ice.ObjectPrx { op1(i, f, b, s, context) { ... } op2(ns, ss, st, context) { ... } op3(proxy, context) { ... }}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:
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 proxyNull Parameters in JavaScript
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 in JavaScript
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:
optional(1) int execute(optional(2) string params, out optional(3) float value);The corresponding proxy method is:
execute( params?: string | undefined, context?: Map<string, string>): Ice.AsyncResult<[number | undefined, number | undefined]>;Client-Side Mapping for Operations
Mapping for Operations
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:
["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:
classdef GreeterPrx < Ice.ObjectPrx methods function returnValue = greet(obj, name, context) % ... end
function future = greetAsync(obj, name, context) % ... end endendGiven a proxy to an object of type Greeter, the client can invoke the greet operation as follows:
greeter = visitorcenter.GreeterPrx( ... communicator, 'greeter:tcp -h localhost -p 4061');
greeting = greeter.greet('Alice'); % Get name via RPCSync and Async Methods
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.
Exception Handling
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:
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:
child = ...; % Get child proxy...
try child.askToCleanUp();catch ex if isa(ex, 'Tantrum') fprintf('The child says: %s\n', ex.reason); else rethrow(ex); endendAsynchronous Method Invocation (AMI)
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.
Future Class
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.
Asynchronous Exception Semantics
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
Asyncmethod throwsIce.CommunicatorDestroyedExceptiondirectly. - a call to an
Asyncmethod can throwIce.TwowayOnlyException. AnAsyncmethod throws this exception if you call an operation that has a return value or out-parameters on a oneway proxy.
Asynchronous Oneway Invocations
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.
Flow Control
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.
Canceling an Asynchronous Invocation
You can call cancel on the future returned by an async invocation to cancel this invocation. For example:
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.
Mapping for Parameters and Return Values
In Parameters
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.
Out Parameters and Return Values
The MATLAB mapping uses the conventional language mechanism for returning one or more result values.
Consider the following Slice definitions:
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:
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 endendOptional Parameters
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:
optional(1) int execute(optional(2) string p, out optional(3) float value);A client can invoke this operation as shown below:
[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 valueendA 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.
Client-Side Mapping for Operations
Mapping for Operations
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:
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:
$greeter = VisitorCenter\GreeterPrxHelper::createProxy( $communicator, 'greeter:tcp -h localhost -p 4061');
$greeting = $greeter->greet('Alice'); // Get name via RPCException Handling
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:
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:
$child = ... // Get child proxy...
try { $child->askToCleanUp();} catch(Tantrum $t) { echo "The child says: " . $t->reason . "\n";}Mapping for Parameters and Return Values
In Parameters
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:
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:
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:
$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 proxyOut Parameters
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:
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:
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:
$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.
Parameter Type Mismatches
Ice validates the arguments to a proxy invocation at runtime and reports any type mismatches as a InvalidArgumentException exception.
Null Parameters
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
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:
optional(1) int execute(optional(2) string params, out optional(3) float value);A client can invoke this operation as shown below:
$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.
Client-Side Mapping for Operations
Mapping for Operations
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:
module VisitorCenter{ interface Greeter { string greet(string name); }}The proxy class generated from the Greeter interface, after removing extra details, is as follows:
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:
greeter = VisitorCenter.GreeterPrx(communicator, "greeter:tcp -h localhost -p 4061")greeting = await greeter.greetAsync("Alice") # Get name via RPCSync and Async Methods
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.
Exception Handling
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:
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:
child = ... # Get child proxy...
try: await child.askToCleanUpAsync()except Tantrum as t: print(f"The child says: {t.reason}")Server-Side Mapping for Operations
Default Mapping for Operations
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:
module VisitorCenter{ interface Greeter { string greet(string name); }}The skeleton class generated from the Greeter interface, after removing extra details, is as follows:
class Greeter(Object, ABC): @abstractmethod def greet(self, name: str, current: Current) -> str | Awaitable[str]: passThe 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:
class Chatbot(VisitorCenter.Greeter): def greet(self, name: str, current: Ice.Current) -> str: return f"Hello, {name}!"AMD Mapping for Operations
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.
Throwing Exceptions
To throw an exception from an operation implementation, you simply construct the exception and throw it. For example:
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)
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.
Asynchronous API
Consider the following Slice definition:
module VisitorCenter{ interface Greeter { string greet(string name); }}slice2py generates the following asynchronous proxy method:
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:
# 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())Asynchronous Exception Semantics
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
Asyncmethod throwsCommunicatorDestroyedExceptiondirectly. - a call to an
Asyncmethod can throwTwowayOnlyException. AnAsyncmethod 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.
Awaitable Objects
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.Futureobjects, includingIce.InvocationFuturefor invocations. - With an asyncio event loop Ice returns
asyncio.Futureobjects 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.
asyncio Integration
asyncio IntegrationIce 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.
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.
Event Loop Restrictions
You can only await a future from the event loop that created it:
Ice.FutureandIce.InvocationFutureThese 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):
Pythonwith 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 threadgreeting = await greeter.greetAsync(getpass.getuser())However, you can await an
Ice.InvocationFuturefrom inside an asynchronous dispatch (AMD), since these coroutines run on the Ice thread pool:Example (✅ works inside AMD with Ice futures):
Pythonasync def greet(self, name: str, current: Ice.Current) -> str:# Using await here is fine because the async dispatch# runs in the Ice thread poolreturn await self.target.greetAsync(name)asyncio.FutureThese belong to the asyncio event loop supplied during communicator initialization. Since asynchronous dispatches also run in this loop, it is safe to awaitasyncio.Futureobjects inside an asyncio-based dispatch.Example (✅ works in asyncio client):
Pythonasync 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.Futuregreeting = await greeter.greetAsync(getpass.getuser())Example (✅ works in asyncio-based AMD):
Pythonasync 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)
Asynchronous Oneway Invocations
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)
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:
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:
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.
AMD Mapping
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:
module VisitorCenter{ interface Greeter { string greet(string name); }}The server can choose to implement the operation synchronously or asynchronously.
Synchronous version:
def greet(self, name: str, current: Ice.Current) -> str: print(f"Dispatching greet request {{ name = '{name}' }}") return f"Hello, {name}!"Asynchronous version (coroutine):
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}!"asyncio Integration for Dispatch
asyncio Integration for DispatchIce 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.
Chaining Asynchronous Invocations
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:
def greet(self, name: str, current: Ice.Current) -> str: return self._greeter.greetAsync(name)Or, using async/await:
# 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.
Mapping for Parameters and Return Values
In Parameters
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:
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:
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:
p = ... # Get proxy...
p.op1(42, 3.14, True, "Hello world!") # Pass simple literals
i = 42f = 3.14b = Trues = "Hello world!"p.op1(i, f, b, s) # Pass simple variables
ns = NumberAndString()ns.x = 42ns.str = "The Answer"ss = [ "Hello world!" ]st = {}st[0] = ssp.op2(ns, ss, st) # Pass complex variables
p.op3(p) # Pass proxyOut Parameters
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:
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:
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:
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.
Parameter Type Mismatches
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.
Null Parameters
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 in Python
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:
optional(1) int execute(optional(2) string params, out optional(3) float value);The corresponding Python proxy method is:
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:
@abstractmethoddef execute( self, params: str | None, current: Current) -> tuple[ int | None, float | None] | Awaitable[tuple[int | None, float | None]]: ...See Also
Client-Side Mapping for Operations
Mapping for Operations
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:
module VisitorCenter{ interface Greeter { string greet(string name); }}The proxy class generated from the Greeter interface, after removing extra details, is as follows:
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
# ...endGiven a proxy to an object of type Greeter, the client can invoke the greet operation as follows:
greeter = VisitorCenter::GreeterPrx.new( communicator, "greeter:tcp -h localhost -p 4061")
greeting = greeter.greet("Alice") # Get name via RPCException Handling
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:
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:
child = ... # Get child proxy...
begin child.askToCleanUp()rescue Tantrum => t puts "The child says: #{t.reason}"endMapping for Parameters and Return Values
In Parameters
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:
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:
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) ... endend
class ClientToServerPrx < Ice::ObjectPrx include ClientToServerPrx_mixinendGiven a proxy to a ClientToServer interface, the client code can pass parameters as in the following example:
p = ... # Get proxy...
p.op1(42, 3.14, true, "Hello world!") # Pass simple literals
i = 42f = 3.14b = trues = "Hello world!"p.op1(i, f, b, s) # Pass simple variables
ns = NumberAndString.new()ns.x = 42ns.str = "The Answer"ss = [ "Hello world!" ]st = {}st[0] = ssp.op2(ns, ss, st) # Pass complex variables
p.op3(p) # Pass proxyOut Parameters and Return Values
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-
voidreturn value, if any, is always the first element in the result array, followed by theoutparameters 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:
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:
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:
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.
Parameter Type Mismatches
Ice validates the arguments to a proxy invocation at runtime and reports any type mismatches as a TypeError exception.
Nil Parameters
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
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:
optional(1) int execute(optional(2) string p, out optional(3) float value);A client can invoke this operation as shown below:
i, v = proxy.execute("--file log.txt")i, v = proxy.execute(Ice::Unset)
if v != Ice::Unset puts "value = " + v.to_sendA 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.
Client-Side Mapping for Operations
Mapping for Operations
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:
module VisitorCenter{ interface Greeter { string greet(string name); }}The proxy protocol generated from the Greeter interface, after removing extra details, is as follows:
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:
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 RPCThis 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.
Async Methods
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.
Exception Handling
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:
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:
let child: ChildPrx = ... // Get child proxy...
do { try await child.askToCleanUp()} catch let t as Tantrum { print("The child says: \(t.reason)")}Server-Side Mapping for Operations
Default Mapping for Operations
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:
module VisitorCenter{ interface Greeter { string greet(string name); }}The skeleton protocol generated from the Greeter interface, after removing extra details, is as follows:
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:
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.
AMD Mapping for Operations
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.
Throwing Exceptions
To throw an exception from an operation implementation, you simply construct the exception and throw it. For example:
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.
Mapping for Parameters and Return Values
In Parameters
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.
Parameter Labels
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:
interface Greeter{ string greet(string name);}The generated code is as follows:
// Client-sidepublic protocol GreeterPrx: Ice.ObjectPrx {}
public extension GreeterPrx { func greet( _ iceP_name: String, context: Ice.Context? = nil) async throws -> Swift.String { ... }}
// Server-sidepublic protocol Greeter: Ice.Dispatcher { func greet(name: String, current: Ice.Current) async throws -> Swift.String}Out Parameters and Return Values
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:
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:
// Client-sidepublic 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-sidepublic protocol Example: Ice.Dispatcher { func op(inp1: Int32, inp2: String, current: Ice.Current) async throws -> (returnValue: Double, outp1: Bool, outp2: Int64)}Optional Parameters
An optional parameter is mapped to a Swift parameter with the corresponding Swift optional type.
Consider the following operation:
optional(1) int execute(optional(2) string p);The corresponding proxy function is:
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.