The pi (Porting Interfaces
) module contains all interfaces for
everything that must be done in a platform-specific way, which thus
does not belong in core. Displaying text in a web page, for example,
requires usage of some font engine and painting in some sort of
windowing system, and it is up to the platform to decide how to do
this. All interfaces must be implemented when porting Opera to a new
platform; however, some of them may have been implemented in a
cross-platform manner inside core, and a platform may choose to use or
not use them. For example, there is a platform-independent host name
resolver in core (implementation of OpHostResolver) that acts as a DNS
client using OpSocket. There is also a concept called GOGI, which
implements the window management and painting classes (OpPainter,
OpView, OpBitmap, etc.) in a platform-independent way, so that all
that the actual platform needs to implement is a frame buffer
interface and some basic input events. GOGI is ideal for rapid porting
and/or when there is little benefit (performance, etc.) in making
native implementations of those classes.
The porting interfaces have developed during several years, and have been adjusted and appended so that Opera has become more and more portable.
The porting interfaces were introduced in 2001 as a directory in the Opera CVS module (Opera/Porting_Interfaces/). When core was modularized a couple of years later, it was moved to the pi module.
Prior to the introduction of porting interfaces, core had a lot of ifdefs for each platform supported. Among other things, each platform had their own version of VisualDevice, a class currently in the display module, controlled by ifdefs. The same was true for the Comm class in the url module. This sort of worked (although it became really messy and difficult to maintain) when there were less than 5 platforms to support.
The pi module consists of three main categories of classes:
This module contains interfaces that core requires the platform to implement, in order to work.
The porting interfaces cover the following basic platform dependencies that Opera has, such as:
This module also contains a Pike script that can be used to create stub implementations of all the interfaces that are defined in this module. This may save an implementor some amount of boring work when porting Opera to a new platform, or when implementing a new interface. The script is called make_stubs.pike
APIs for user interface and product code are not in this
module. Such APIs belong in the windowcommander module. See the
chapter about The burger model
.
(user interface and product code)
windowcommander module
core proper modules
pi module
(platform code)
The burger model
From a core-perspective[1], if we imagine Opera as a 3-layer hamburger, the porting interfaces form the bottom bun of the burger. This is the interface towards the platforms. The burger's middle part, the meaty burger patty is Opera's core, and the top bun is the interface towards the different user interfaces and general product control. That part is taken care of by the windowcommander module. The bottom layer varies with platform (Linux, Symbian, etc.), the middle layer varies with feature settings (mouse support, ECMA Script support, etc.), the top layer varies with product (desktop, gogi).
Ownership-wise it goes like this: The product/UI owns core through windowcommander (and hardcore for initialization and shutdown), core owns the platform through pi.
Direction of requests or commands (do this!
) in this model is
downwards. The product/UI requests/commands core to do something via
windowcommander (e.g. go to URL
), core requests/commands the
platform to do something via pi (e.g. connect to IP address
).
Direction of notifications (this happened
) in this model is
upwards. The platform notifies core about events via pi (e.g. data
received on socket
), core notifies the product/UI about events via
windowcommander (e.g. 70% of document loaded
). This is implemented
with methods in listener classes.
It follows from this model that core should never command the user
interface; instead it should notify it about events. That is, core
does not tell the user interface: Display a top-aligned bar with red
Tahoma 12pt font on white background with the text '70% lastet'!
. No,
it tells the user interface Just so you know, 70% of the document has
been loaded
. The the user interface code decides how to convey this
information to the user (if at all).
[1] The burger model is useful to describe how it looks from
core's point of view. Outside of core, on the platform or product/UI
side (if there is such a distinction), platform and product/UI code
may meet, making it look more like a hot dog model
, or even French
hot dog model
. This is probably the case for gogi. The burger model
may be somewhat obscured to the gogi client, as gogi takes care of a
lot of the communication with (and implementation of) pi and
windowcommander, so that the platform/product implementor won't have
to bother so much about classes like e.g. OpWindowCommander and
OpPainter. Having said that, it probably makes sense to retain as much
as possible of the burger model idea on the platform / product side
too.
The basic design goal is that no platform dependent code should remain in core. This code should be abstracted and broken out into proper porting interfaces. This way core code doesn't have to be infested with platform #ifdefs.
What is considered proper
is up for discussion in each case. One
needs to determine if an interface is more of a product/user
interface, than a platform interface. If it is, it should most likely
go into the top bun of the burger (see chapter about The burger model
), i.e. the windowcommander
module. The pi module is and shall remain Opera core's only means of
interacting with the underlying (operating) system (and windowing
system). It shall not contain anything else.
All interfaces defined in this module shall be used by core in some way or another; or they don't belong here. That is, interfaces that indeed may be needed on several platforms from e.g. cross-platform user interface code (such as QUICK), but that are not used in core code, do not belong in the pi module. Such interfaces could belong in mantle, or, in the case of the desktop version of Opera, perhaps the desktop_pi module.
Another design goal is to keep the interface as small as possible. We have no intentions of building a full, general-purpose library; we only want whatever is needed to port Opera to any platform. This is especially important because in principle, most of the interfaces have to be implemented on every platform, from the smallest web-capable earring or mobile phone, to the largest desktop or web-capable meaning-of-life computing machine.
It is also important to choose a good level of abstraction; the
interface should be as generic as necessary and reasonable, but not
necessarily as generic as possible. (example: Logitech mouse
,
mouse
, pointing device
, input thingie
, or body extension
?)
Under no circumstances should the platform implementation of porting interfaces have to access global objects in core.[1]
[1] This is currently not possible for a platform implementation to achieve. To signal keyboard input, it needs to access g_input_manager in the inputmanager module. A lot of platform implementations also access g_prefsManager in the prefs module.
We should use as few data types as possible in the pi module, to keep dependencies between core code and platform code at a minimum. The pi module is however not entirely self-contained; some essential data types and macros are provided by the hardcore and util modules.
Types not defined in the pi module allowed in pi interfaces and listeners:
Allowed core macros for the interface itself and the platform implementation:
Regarded as design flaws, the following additional types are currently used in pi:
Since practically everything in the pi module actually is some sort of interface used or implemented by other modules, changing it will normally affect other modules. For core modules, the regular capability system is used to deal with API changes in pi. This way, upgrading or downgrading pi or the module that a change affects, will work smoothly and cause no compilation or runtime errors (if the capability system is used properly). Changes in pi that affect platform code will also be announced with a capability define, but since platform code cannot announce capabilities, this only goes one way, meaning that a change in pi will not wait for the platforms to adapt. Platform code can retain backwards-compatiblity with older pi module versions by checking for a capability announced by pi, but the platform code is not forwards-compatible with future module releases.
Each interface is a C++ class with only pure virtual
members[1]. Non-const pass-by-reference is implemented with
pointers[2], for code readability reasons. The idea is that it is more
obvious that parameter data is to be modified by the method if it is
passed as a pointer instead of as a reference. Note that this does not
apply to objects passed as const; we want to be able to do e.g.
miff->Bar(OpRect(17,17,4711, 4711)) (where
Bar is declared as virtual void Bar(const
OpRect&)).
Each class name should start with the Op
prefix. A main API class
along with its related listener classes (if any) should be put in a
header file with the same file name as the main class, with .h
appended. Capitalization in the class name is retained in the file
name. (Class OpFoo is found in file OpFoo.h)
A class name should consist of Op
followed by a noun or a
compound noun.
A method name in a request class should resemble a command,
typically a clause starting with a verb in its imperative mood,
optionally followed by an object. Example: Eat the burger!
→
EatBurger()
A method name in a listener class should start with On
followed
by a clause in passive voice (only the participle), pretty much like a
newspaper headline. Example: The burger was eaten by a horse
→
OnBurgerEatenByHorse()
Unless instances of a class are created by calling some method on a manager class or owner object (e.g. OpFontManager::CreateFont()), the class has a static Create() method, which creates (and initializes) an instance of the class. It is implemented on the platform side, typically along with the rest of the abstract class re-implementation.
[1] We have some members still remaining as non-pure virtual, because people have historically taken shortcuts. Work is in progress to fix that.
[2] Normally true, and this is the rule to follow when adding new methods or parameters to the interfaces. However, we still have some methods that pass by reference using '&' instead of '*'.
Tabs (4 spaces wide) are used for indentation.
Each class should be prefixed by doxygen documentation, on this format:
/** @short <short description of class>
*
* <Additional information, if necessary. May span over several lines if
* necessary>
*/
class OpFoo
{
public:
<...>
... and each method should be prefixed like this:
/** <short description of what the method does, starting with an verb in its imperative mood>
*
* <More detailed description of what the method does, if that is
* necessary. May span over several lines>
*
* @param a <description of the 'a' parameter, which may span over
* several lines>
* @param hest <description of the 'hest' parameter>
*
* @return <possible return values>
*/
virtual OP_STATUS DoStuff(const uni_char* a, int hest) = 0;
<...>
};
The width of comment lines should generally not exceed 80
characters (where each tab counts as 4 characters), but the short
description
part of class or method documentation may, unless it is
ridiculously long (but then it's not really a short description, is
it?).
The pi module does not need to deal much with OOM handling on its own, since there's almost no code in there. The module initialization sequence PiModule::InitL() may run out of memory, and if it happens, it will propagate that via LEAVE.
OOM is signalled to callers of methods in the porting interface classes. Methods in porting interface listener classes may never signal OOM. In other words, platform code may report OOM to core code, but not vice-versa.
Core calls a porting interface method. If the method signals OOM, it is up to the caller to deal with that.
The module itself doesn't use heap memory at all, except for whatever is required by the classes initialized in PiModule::InitL(), and that shouldn't be much.
The platform code implementing the porting interface may use considerable amounts of heap memory. This is the case especially for OpBitmap. When core creates a new image object, the pixel data is allocated and owned by the implementation of OpBitmap. OpFont may also use a lot of heap memory. OpSocket may have to use some memory for network buffering. OpView may have to use some for double-buffering. There is no way for core to keep track of this kind of memory usage, except for the OpBitmap case, where OpScreenInfo can tell core how much RAM an OpBitmap of a given size would require.
The worst case of stack usage is most likely the callbacks from the platform layer back into the Opera core code. One example is the socket code: if sending all data completes immediately, the platform may notify core code about this immediately, and core code may then legally send more data right away. It is the platform implementation's responsibility to limit the recursion depth in this case, and core's responsibility to avoid deep recursions.
The interfaces use none. Platform implementations may use some.
There are currently no means of requesting platform implementation to reduce memory usage, other than deleting pi objects.
PiModule::Destroy() frees the memory. Platform code is responsible for freeing any static memory allocated on their side.
N/A
No tuning.
No tests.
Load and display a web page with text, images, plug-ins, borders and scrollbars over HTTP. Print it.
Since there is almost no code in this module, one can say that this
module deals more with memory information (you're out of memory
, to
store an image of that size I need to allocate this many bytes
) than
memory itself.
We should consider getting better control over the platform implementation's memory usage. Classes that are likely to use a lot of memory, are OpBitmap, OpFont and OpView.