Code Generation
7 min read
9 min read
The Python mapping supports two forms of code generation: dynamic and static.
Dynamic Code Generation in Python
With dynamic code generation, Slice files are compiled at run time and the generated Python code is immediately evaluated by the Python interpreter.
In this mode, no Python source files are created by the Slice compiler. Instead, you load Slice files directly with the Ice.loadSlice function.
For example:
import Ice
Ice.loadSlice(["Greeter.ice"])
...
import VisitorCenter
greeter = VisitorCenter.GreeterPrx( communicator, "greeter:tcp -h localhost -p 4061")Here, the VisitorCenter module becomes available only after the Slice file is loaded and translated into Python. Attempting to import it before calling Ice.loadSlice would fail.
For this example, assume that Greeter.ice contains the following Slice definitions:
module VisitorCenter{ /// Represents a simple greeter. interface Greeter { /// Creates a personalized greeting. /// @param name The name of the person to greet. /// @return The greeting. string greet(string name); }}Ice.loadSlice Options in Python
Ice.loadSlice Options in PythonThe Ice.loadSlice function behaves like the Slice compiler: it accepts command-line arguments for specifying preprocessor options and controlling code generation. The arguments must include at least one Slice file.
The function is defined as:
def Ice.loadSlice(args:[str])The args parameter supports all standard Slice compiler options.
For example:
Ice.loadSlice(["-I.", "Greeter.ice"])The supported arguments are the same as those documented for the Slice for Python compiler under standard compiler options.
Locating Slice Files in Python
If your Slice files depend on Ice’s built-in types, you don’t need to hard-code the path to your Ice installation. Instead, you can call the Ice.getSliceDir function to obtain the directory where the standard Ice Slice files are installed.
For example:
Ice.loadSlice([f"-I{Ice.getSliceDir()}", "Greeter.ice"])This ensure the application remains portable and does not rely on a fixed installation path.
Loading Multiple Slice Files in Python
You can specify multiple Slice files in a single invocation of Ice.loadSlice:
Ice.loadSlice(["Syscall.ice", "Process.ice"])Alternatively, you can call Ice.loadSlice several times:
Ice.loadSlice(["Syscall.ice"])Ice.loadSlice(["Process.ice"])Note that the Slice for Python compiler does not generate code for included files. It only generates code for the Slice files explicitly passed in the args parameter.
Static Code Generation in Python
With static code generation, Slice files are compiled into Python source files using the Slice for Python compiler (slice2py). The generated Python code is stored in .py files, which are then imported and compiled by the Python interpreter along with the rest of your application code.
Compiler Output in Python
The Slice for Python compiler generates a Python module for each Slice definition. Each module is placed within a Python package that corresponds to the Slice module containing the definition.
For Slice classes and interfaces, the compiler also generates a second Python module named <name>_forward, which contains forward declarations for the generated types.
Each generated package includes an __init__.py file that re-exports all definitions from the modules it contains. This allows you to import definitions directly from the package without referencing individual modules.
Using the Slice definitions from the Ice/callback demo as an example:
module EarlyRiser{ enum ButtonPressed { Snooze, Stop }
interface AlarmClock { ButtonPressed ring(string message); }
interface WakeUpService { void wakeMeUp(AlarmClock* alarmClock, long timeStamp); }}(Doc comments omitted for brevity—see the demo for the full Slice definitions.)
The Slice compiler generates the following files:
EarlyRiser/AlarmClock.pyEarlyRiser/AlarmClock_forward.pyEarlyRiser/ButtonPressed.pyEarlyRiser/WakeUpService.pyEarlyRiser/WakeUpService_forward.pyEarlyRiser/__init__.pyAlarmClock.py– contains the definitions forAlarmClockPrxproxy type, and theAlarmClockservant skeleton.AlarmClock_forward.py– contains the forward declaration for theAlarmClockPrxproxy type. There is never a need to manually import_forwardfiles.ButtonPressed.py– contains theButtonPressedenum.WakeUpService.py– contains the definitions forWakeUpServicePrxproxy type, and theWakeUpServiceservant skeleton.WakeUpService_forward.py– contains the forward declaration for theWakeUpServicePrxproxy type. There is never a need to manually import_forwardfiles.__init__.py– is the package index and re-exports all definitions from the other modules.
The init.py file for each generated package re-exports all definitions from the modules within the package. For this reason, you must compile all Slice files that contribute to a given package in the same invocation of the Slice compiler.
If you compile only a subset of the Slice files, the generated __init__.py will be incomplete, and some definitions may be missing from the package.
A related situation arises when a Python package contains a mix of Slice-generated code and manually written code. In this case, you should avoid generating an init.py file with the Slice compiler, since it will not account for your manually written code.
The Slice compiler --build option allows you to control what kind of files are generated:
--build=modulesGenerates only the Python module files for the Slice definitions.--build=indexGenerates only the Python package index files (init.py).--build=all. Generates both module and index files (this is the default if --build is omitted).
Customizing Compiler Output Using Metadata in Python
By default, the Slice for Python compiler generates Python modules and packages using the layout described in the previous section.
Sometimes, however, you may need to map a Slice definition to a different Python package than the one produced by the default mapping. This is typically necessary when the default mapping would:
- collide with another module already in use, or
- conflict with a Python built-in module.
In such cases, you can use the python:identifier metadata to remap the Slice identifier. The generated Python code will then consistently use the remapped identifier instead of the original Slice identifier.
See Also
The Ruby mapping supports two forms of code generation: dynamic and static.
Dynamic Code Generation in Ruby
Using dynamic code generation, Slice files are "loaded" at runtime and dynamically translated into Ruby code, which is immediately compiled and available for use by the application. This is accomplished using the Ice::loadSlice method, as shown in the following example:
Ice::loadSlice(["Color.ice"])puts "My favorite color is #{M::Color::Blue.to_s}"For this example, we assume that Color.ice contains the following definitions:
module M{ enum Color { red, green, blue }}Ice::loadSlice Options in Ruby
Ice::loadSlice Options in RubyThe Ice::loadSlice method behaves like a Slice compiler in that it accepts command-line arguments for specifying preprocessor options and controlling code generation. The arguments must include at least one Slice file.
The function has the following Ruby definition:
def loadSlice(args)The args array holds the command-line arguments, one string per argument. The function always returns nil.
For example:
Ice::loadSlice(["-I/opt/IceRuby/slice", "Color.ice"])In addition to the standard compiler options, Ice::loadSlice also supports the following command-line option:
--allGenerate code for all Slice definitions, including those from included files.
Locating Slice Files in Ruby
If your Slice files depend on Ice types, you can avoid hard-coding the path name of your Ice installation directory by calling the Ice::getSliceDir function:
Ice::loadSlice(["-I#{Ice::getSliceDir()}", "Color.ice"])This function attempts to locate the slice subdirectory of your Ice installation using an algorithm that succeeds for the following scenarios:
- Installation of a binary Ice archive
- Installation of an Ice source distribution using
make install - Installation via a Windows installer
- RPM installation on Linux
- Execution inside a compiled Ice source distribution
If the slice subdirectory can be found, getSliceDir returns its absolute path name, otherwise the function returns nil.
Loading Multiple Slice Files in Ruby
You can specify as many Slice files as necessary in a single invocation of Ice::loadSlice, as shown below:
Ice::loadSlice(["Syscall.ice", "Process.ice"])Alternatively, you can call Ice::loadSlice several times:
Ice::loadSlice(["Syscall.ice"])Ice::loadSlice(["Process.ice"])If a Slice file includes another file, the default behavior of Ice::loadSlice generates Ruby code only for the named file. For example, suppose Syscall.ice includes Process.ice as follows:
// Syscall.ice#include <Process.ice>...If you call Ice::loadSlice(["-I.", "Syscall.ice"]), Ruby code is not generated for the Slice definitions in Process.ice or for any definitions that may be included by Process.ice. If you also need code to be generated for included files, one solution is to load them individually in subsequent calls to Ice::loadSlice. However, it is much simpler, not to mention more efficient, to use the --all option instead:
Ice::loadSlice(["--all", "-I.", "Syscall.ice"])When you specify --all, Ice::loadSlice generates Ruby code for all Slice definitions included directly or indirectly from the named Slice files.
There is no harm in loading a Slice file multiple times, aside from the additional overhead associated with code generation. For example, this situation could arise when you need to load multiple top-level Slice files that happen to include a common subset of nested files. Suppose that we need to load both Syscall.ice and Kernel.ice, both of which include Process.ice. The simplest way to load both files is with a single call to Ice::loadSlice:
Ice::loadSlice(["--all", "-I.", "Syscall.ice", "Kernel.ice"])Although this invocation causes the Ice extension to generate code twice for Process.ice, the generated code is structured so that the interpreter ignores duplicate definitions. We could have avoided generating unnecessary code with the following sequence of steps:
Ice::loadSlice(["--all", "-I.", "Syscall.ice"])Ice::loadSlice(["-I.", "Kernel.ice"])In more complex cases, however, it can be difficult or impossible to completely avoid this situation, and the overhead of code generation is usually not significant enough to justify such an effort.
Limitations of Dynamic Code Generation in Ruby
The Ice::loadSlice method must be called outside of any module scope. For example, the following code is incorrect:
# WRONGmodule M Ice::loadSlice(["--all", "-I.", "Syscall.ice", "Kernel.ice"]) ...endStatic Code Generation in Ruby
You should be familiar with static code generation if you have used other Slice language mappings, such as C++ or Java. Using static code generation, the Slice compiler slice2rb generates Ruby code from your Slice definitions.
Compiler Output in Ruby
For each Slice file X.ice, slice2rb generates Ruby code into a file named X.rb in the output directory. The default output directory is the current working directory, but a different directory can be specified using the --output-dir option.
Include Files in Ruby
It is important to understand how slice2rb handles include files. In the absence of the --all option, the compiler does not generate Ruby code for Slice definitions in included files. Rather, the compiler translates Slice #include statements into Ruby require statements in the following manner:
- Determine the full pathname of the included file.
- Create the shortest possible relative pathname for the included file by iterating over each of the include directories (specified using the
-Ioption) and removing the leading directory from the included file if possible. For example, if the full pathname of an included file is/opt/App/slice/OS/Process.ice, and we specified the options-I/opt/Appand-I/opt/App/slice, then the shortest relative pathname isOS/Process.iceafter removing/opt/App/slice. - Replace the
.iceextension with.rb. Continuing our example from the previous step, the translatedrequirestatement becomes
require "OS/Process.rb"As a result, you can use -I options to tailor the require statements generated by the compiler in order to avoid absolute pathnames and match the organizational structure of your application's source files.
Static Versus Dynamic Code Generation in Ruby
There are several issues to consider when evaluating your requirements for code generation.
Application Considerations for Code Generation in Ruby
The requirements of your application generally dictate whether you should use dynamic or static code generation. Dynamic code generation is convenient for a number of reasons:
- It avoids the intermediate compilation step required by static code generation.
- It makes the application more compact because the application requires only the Slice files, not the additional files produced by static code generation.
- It reduces complexity, which is especially helpful during testing, or when writing short or transient programs.
Static code generation, on the other hand, is appropriate in many situations:
- when an application uses a large number of Slice definitions and the startup delay must be minimized
- when it is not feasible to deploy Slice files with the application
- when a number of applications share the same Slice files
- when Ruby code is required in order to utilize third-party Ruby tools.
Mixing Static and Dynamic Generation in Ruby
You can safely use a combination of static and dynamic translation in an application. For it to work properly, you must correctly manage the include paths for Slice translation and the Ruby interpreter so that the statically-generated code can be imported properly by require.
For example, suppose you want to dynamically load the following Slice definitions:
#include <Glacier2/Session.ice>
module MyApp{ interface MySession extends Glacier2::Session { // ... }}Whether the included file Glacier2/Session.ice is loaded dynamically or statically is determined by the presence of the --all option:
sliceDir = "-I#{ENV['ICE_HOME']}/slice"
# Load Glacier2/Session.ice dynamically:Ice::loadSlice([sliceDir, "--all", "MySession.ice"])
# Load Glacier2/Session.ice statically:Ice::loadSlice([sliceDir, "MySession.ice"])In this example, the first invocation of loadSlice uses the --all option so that code is generated dynamically for all included files. The second invocation omits --all, therefore the Ruby interpreter executes the equivalent of the following statement:
require "Glacier2/Session.rb"As a result, before we can call loadSlice we must first ensure that the interpreter can locate the statically-generated file Glacier2/Session.rb. We can do this in a number of ways, including:
adding the parent directory (e.g.,
/opt/IceRuby/ruby) to theRUBYLIBenvironment variablespecifying the
-Ioption when starting the interpretermodifying the search path at runtime, as shown below:
Ruby$:.unshift("/opt/IceRuby/ruby")