Skip to main content

ZeroC Ice Overview

ZeroC Ice (Internet Communications Engine) is a modern, object-oriented middleware platform for building distributed applications. It provides:
  • Language-agnostic RPC: Define interfaces once, implement in any supported language
  • Efficient binary protocol: Compact marshalling for network efficiency
  • Transparent networking: Abstract away TCP/UDP complexities
  • Exception propagation: Domain exceptions traverse network boundaries
Ice uses the Slice language (Specification Language for Ice) to define interfaces. The slice2py compiler generates Python code from Slice definitions.

Slice Interface Definition

The contract between client and server is defined in backend/Conversor.ice:
module Conversor
{
    exception UnidadInvalidaException
    {
        string mensaje;
    };

    interface ConversorUnidades
    {
        double convertirTemperatura(double valor, string desde, string hasta)
            throws UnidadInvalidaException;

        double convertirLongitud(double valor, string desde, string hasta)
            throws UnidadInvalidaException;

        double convertirPeso(double valor, string desde, string hasta)
            throws UnidadInvalidaException;

        double convertirVelocidad(double valor, string desde, string hasta)
            throws UnidadInvalidaException;

        string unidadesDisponibles(string categoria)
            throws UnidadInvalidaException;
    };
};

Slice Components

module Conversor creates a namespace to prevent naming conflicts. This becomes a Python package after code generation.

Code Generation

The Slice definition is compiled to Python code using slice2py:
slice2py Conversor.ice
This generates:
  • Conversor.py - Contains exception classes and proxy interfaces
  • Conversor_ice.py - Contains Ice-specific metadata
Generated files should not be edited manually. Always modify the .ice file and regenerate.

Server Implementation

Servant Pattern

The servant is the actual object implementation that handles remote calls. From backend/server.py:14-128:
import Ice
import Conversor  # Generated by slice2py

# Servant inherits from generated interface
class ConversorUnidadesImpl(Conversor.ConversorUnidades):
    
    def convertirTemperatura(self, valor, desde, hasta, current=None):
        """Implement temperature conversion logic"""
        unidades = {"celsius", "fahrenheit", "kelvin"}
        desde, hasta = desde.lower(), hasta.lower()
        self._validar(desde, hasta, unidades, "temperatura")
        
        # Strategy: convert to Celsius first, then to target
        if desde == "fahrenheit":
            en_celsius = (valor - 32) * 5 / 9
        elif desde == "kelvin":
            en_celsius = valor - 273.15
        else:
            en_celsius = valor  # already Celsius
        
        if hasta == "fahrenheit":
            return en_celsius * 9 / 5 + 32
        elif hasta == "kelvin":
            return en_celsius + 273.15
        return en_celsius
    
    def _validar(self, desde, hasta, validas, categoria):
        """Validate units and throw ICE exception if invalid"""
        errores = []
        if desde not in validas:
            errores.append(f"Unidad origen '{desde}' no valida")
        if hasta not in validas:
            errores.append(f"Unidad destino '{hasta}' no valida")
        
        if errores:
            ex = Conversor.UnidadInvalidaException()
            ex.mensaje = " ".join(errores)
            raise ex  # ICE will marshal this exception to client
The current parameter is required by Ice. It provides context about the invocation (client address, operation name, etc.), but can be ignored if not needed.

Object Adapter

The Object Adapter is Ice’s component that receives network requests and dispatches them to servants. From server.py:131-159:
1

Initialize Communicator

with Ice.initialize(sys.argv) as communicator:
The communicator is the main Ice runtime object. It:
  • Manages network connections
  • Handles thread pools
  • Parses command-line Ice properties (e.g., --Ice.Trace.Network=2)
2

Create Object Adapter

adapter = communicator.createObjectAdapterWithEndpoints(
    "ConversorAdapter",  # Adapter name (for configuration)
    "default -p 10000"   # Endpoint: TCP on port 10000
)
Endpoint syntax: protocol -option value
  • default: TCP protocol
  • -p 10000: Listen on port 10000
  • Could add -h hostname to bind specific interface
3

Create and Register Servant

servant = ConversorUnidadesImpl()
adapter.add(servant, Ice.stringToIdentity("ConversorUnidades"))
  • servant: Your implementation instance
  • identity: Unique name clients use to locate this object
  • stringToIdentity() converts string to Ice identity structure
4

Activate Adapter

adapter.activate()
Starts accepting incoming connections on the configured endpoint.
5

Wait for Shutdown

communicator.waitForShutdown()
Blocks until Ctrl+C or programmatic shutdown. The adapter continues serving requests on background threads.

Complete Server Code

def main():
    with Ice.initialize(sys.argv) as communicator:
        # Create adapter listening on TCP port 10000
        adapter = communicator.createObjectAdapterWithEndpoints(
            "ConversorAdapter", "default -p 10000"
        )
        
        # Create servant and register with identity "ConversorUnidades"
        servant = ConversorUnidadesImpl()
        adapter.add(servant, Ice.stringToIdentity("ConversorUnidades"))
        
        # Start accepting connections
        adapter.activate()
        
        print("Servidor escuchando en puerto 10000... (Ctrl+C para detener)")
        
        # Block until shutdown signal
        communicator.waitForShutdown()

if __name__ == "__main__":
    main()

Client Implementation (Flask Proxy)

The Flask server acts as an Ice client using a proxy to invoke remote methods. From web_server.py:26-73:

Proxy Creation

1

Initialize Communicator

self.communicator = Ice.initialize(sys.argv)
Client-side communicator for managing connections.
2

Create Base Proxy

base = self.communicator.stringToProxy(
    "ConversorUnidades:default -p 10000"
)
Proxy string syntax: identity:endpoint
  • ConversorUnidades: Object identity on server
  • default -p 10000: Connect via TCP to port 10000 on localhost
3

Checked Cast

self.proxy = Conversor.ConversorUnidadesPrx.checkedCast(base)
if not self.proxy:
    raise RuntimeError("Servidor ICE no encontrado")
checkedCast verifies the server actually implements the expected interface by sending a network request. Returns None if types don’t match.
Use uncheckedCast for better performance if you trust the server type, but you lose type safety.

Invoking Remote Methods

Once the proxy is created, calling remote methods looks like local calls:
def convert_temperatura(self, valor, desde, hasta):
    return self.proxy.convertirTemperatura(valor, desde, hasta)
Behind the scenes, Ice:
  1. Marshals parameters into binary format
  2. Transmits over TCP connection to server
  3. Waits for response (synchronous by default)
  4. Unmarshals return value or exception
  5. Returns result or raises exception

Exception Handling

try:
    resultado = cliente.convert_temperatura(valor, desde, hasta)
except Conversor.UnidadInvalidaException as e:
    # Domain exception from Slice definition
    return jsonify({'error': f'Unidad inválida: {e.mensaje}'}), 400
except Ice.ConnectionRefusedException:
    # Ice infrastructure exception - server not running
    return jsonify({'error': 'Servidor ICE desconectado'}), 503
except Exception as e:
    # Other unexpected errors
    return jsonify({'error': str(e)}), 500

Ice Architecture Patterns

Proxy vs Servant

Proxy (Client Side)

  • Location: Flask server (client)
  • Purpose: Represents remote object
  • Behavior: Marshals calls, sends over network
  • Type: Conversor.ConversorUnidadesPrx

Servant (Server Side)

  • Location: ICE server
  • Purpose: Implements business logic
  • Behavior: Processes requests, returns results
  • Type: ConversorUnidadesImpl(Conversor.ConversorUnidades)

Direct vs Indirect Proxies

"ConversorUnidades:default -p 10000"
Hardcodes endpoint in proxy string. Simple but requires knowing server location at compile/deploy time.Pros: Simple, no additional infrastructure
Cons: Not flexible, requires reconfiguration if server moves

Network Protocol Details

Ice Protocol

Ice uses a compact binary protocol for efficiency:
  1. Request Header: Operation name, request ID, context
  2. Parameters: Marshalled in order (valor, desde, hasta)
  3. Response: Return value or exception

Marshalling Example

For convertirTemperatura(100.0, "celsius", "fahrenheit"):
[Header: 8 bytes]
[Operation: "convertirTemperatura"]
[Param 1: double 100.0 = 8 bytes]
[Param 2: string "celsius" = 4 + 7 = 11 bytes]
[Param 3: string "fahrenheit" = 4 + 10 = 14 bytes]
Total: ~41 bytes
Response:
[Header: 8 bytes]
[Return: double 212.0 = 8 bytes]
Total: 16 bytes
Ice protocol is significantly more efficient than JSON over HTTP. A comparable REST call would be ~150+ bytes.

Connection Management

Client Connection Lifecycle

class ConversorClient:
    def connect(self):
        self.communicator = Ice.initialize(sys.argv)
        base = self.communicator.stringToProxy(
            "ConversorUnidades:default -p 10000"
        )
        self.proxy = Conversor.ConversorUnidadesPrx.checkedCast(base)
        # Connection established on first method call (lazy)
    
    def disconnect(self):
        if self.communicator:
            self.communicator.destroy()  # Close all connections
Always call communicator.destroy() to clean up resources. Using context managers (with Ice.initialize()) handles this automatically.

Advanced Ice Features

Asynchronous Invocation

Ice supports async calls for non-blocking operations:
# Synchronous (blocks)
result = proxy.convertirTemperatura(100, "celsius", "fahrenheit")

# Asynchronous (returns future)
future = proxy.convertirTemperaturaAsync(100, "celsius", "fahrenheit")
result = future.result()  # Block only when result needed

Oneway Calls

For fire-and-forget operations (not used in this app):
interface Logger {
    void logMessage(string msg);  // No return value
};
proxy.ice_oneway().logMessage("Event occurred")  # Returns immediately

Timeout Configuration

Set per-proxy or globally:
# Per-proxy timeout (5 seconds)
proxy = proxy.ice_timeout(5000)

# Global timeout via Ice properties
# --Ice.Default.InvocationTimeout=5000

Debugging Ice Applications

Server-side Tracing

python3 server.py --Ice.Trace.Network=2 --Ice.Trace.Protocol=1
  • Network: Logs connection events
  • Protocol: Logs message headers

Client-side Tracing

Add to Flask startup:
self.communicator = Ice.initialize([
    '--Ice.Trace.Network=2',
    '--Ice.Trace.Protocol=1'
])

Performance Considerations

Concurrency

Ice handles concurrent requests via thread pool. Default size is typically sufficient for this workload.

Connection Pooling

Ice automatically reuses connections. Multiple proxies to the same endpoint share a connection.

Compression

Enable for large payloads:
proxy = proxy.ice_compress(True)

Batching

Batch multiple oneway calls:
batch = proxy.ice_batchOneway()
batch.operation1()
batch.operation2()
batch.ice_flushBatchRequests()

Security Features

SSL/TLS Support

Ice supports encrypted connections:
# Server
adapter = communicator.createObjectAdapterWithEndpoints(
    "ConversorAdapter",
    "ssl -p 10000"  # SSL instead of TCP
)

# Client
proxy = communicator.stringToProxy(
    "ConversorUnidades:ssl -p 10000"
)
Requires Ice SSL configuration (certificates, keys).
Current implementation uses unencrypted TCP. Use SSL for production deployments over untrusted networks.

Next Steps

Flask Server

Learn how Flask integrates the Ice client

Slice Reference

Official ZeroC Ice Slice documentation

Build docs developers (and LLMs) love