[Ns-developers] finalizing the ns-3 object model
Tom Henderson
tomh at tomh.org
Sun Jan 13 23:01:10 PST 2008
(lengthy email follows)
This note is intended to migrate to the list some off-list discussions
concerning the finalization of the ns-3 object model. We are trying to
finish the last major portion of the API (topology code) and to address
lingering questions and possible changes to what we have now. There are
some specific proposals below, and perhaps more forthcoming, that we
will need to discuss and decide what, if anything, we want to do about
them.
Some of these notes below are attempts to summarize some proposals
raised by Mathieu and Craig; apologies if I did not accurately
paraphrase everything or frame them in the right way. Some of these are
also my perspectives. I tried to organize the below as follows:
1. Summary of goals
2. Summary of current model limitations
3. Sample code
1) Goals for the ns-3 object model
===============
We've settled on an C++ object system, and are trying to extend it to
deal with some limitations of C++ object systems in general.
1) memory management
2) solve the weak base class problem and fragile base class problems,
and avoid downcasts.
3) topology/script reuse
i) write general topology or scenario code and allow it to be run
populated with different types of objects, such as swapping out
TCP variants
4) simulation configuration
i) expose existing parameter configurations
ii) allow users to create new default variables for new classes
iii) hook into command line arguments
iv) support scope control, both in terms of simulation time
(pre-simulation, object construction, object inheritance) and the
objects that have access to them
5) documentation and clarity
i) what interfaces does an object support?
ii) what default variables affect an object's configuration or operation?
(ideally, like to not have to rely on C++ comments as much as possible)
iii) is the syntax approachable or learnable by non-expert-C++ users?
2. Solutions and Limitations
==========
It is believed that our current model solves many of the above issues,
with some limitations. The question is what and how much to do about
the limitations right now.
We first discuss above problems 2, 3, and partly 5, and then turn to
problem 4. Memory management (1) seems to be a resolved issue, other
than the need to perhaps support a CreatePtr<> non-object creation
call.
2.1 Aggregation, Objects, and Interfaces
======================
This addresses mainly problems 2 and 3, and partly 5.
The weak base class/downcast problem is addressed by the object
aggregation and query interface mechanism we have added.
The ClassId and ComponetManager are designed to address problem 3.
The current system works but has the following limitations:
2a) The syntax of b->QI<Derived> (Derived::iid) could be perhaps
simplified to: b->QI<Derived> () (bug 122)
2b) Move SetInterfaceId, a source of some programming errors, to the
base class Object (bug 123)
2c) We have the partial paradigm of Interface vs. Object (vs.
Component) and it is not clear to the code reader which is which (our
discussion in Seattle's offsite meeting)
2d) there isn't an easily apparent way to determine which objects
have which interfaces automatically aggregated to them (example:
you create CraigsIpv4Impl but there is no way in reading the client
code to know that it aggregates an Ipv4 interface for you)
2e) there is no way to aggregate objects with any sort of access
control (all are part of the public interface)
3a) the implementation of ComponentManager has been described as
scary and will be hard to maintain
Mathieu has suggested patches to solve 2a) and 2b). Gustavo has
complained about some Python bindings issues (perhaps resolved now).
Problem 2c) generated a lot of discussion. Mathieu first suggested
to replace Create<> with CreatePtr<> and CreateObject<>, with
CreatePtr<> being used to create objects that did not derive from
ns3::Object.
Craig took this one step further with the following suggestion, to
also try to address 1d):
- CreateObject<T> that inherits from Object, doesn't add an
InterfaceId and disables QI.
- CreateInterface<T1, T2> that inherits from Object, does add an
InterfaceId and enables QI (see below).
- Create<T> that creates a C++ object and assigns lifetime
management to a Ptr<T>.
Craig suggesting solving 1d) above by making Interface addition
explicit in the code, rather than transparent. That is, for example,
you would call:
Ptr<UdpImpl> udpImpl = CreateInterface<Udp, UdpImpl> (udp);
node->AddInterface (udpImpl);
In this example, it is more explicit that you are creating a private
UDP implementation class that provides the Udp public API, and you are
explicitly aggregating it to the node. The semantics proposed by
Craig are that, by default, you do not aggregate interfaces; you
require that users explicitly aggregate them with templated calls
such as the above.
There has been no patch solution proposed for problem 1e) (providing
a capability to distinguish public vs private aggregation)
---
Mathieu then looked at this problem a bit differently and suggested
that another approach to the problem of trying to distinguish
between Interfaces and Components would be to collapse them.
He defined a new TypeId, replaced the component manager with an
Object::Create() facility, and rename QueryInterface to QueryObject.
This change also paves the way for the Parameter proposal below.
Mathieu also proposed a sort-of unified proposal to deal with
these problems (not presented in entirety here).
Mathieu has suggested that, among other things, this proposal is partly
a way of more clearly framing that ns-3 is an object system; quoting:
"moving away from iid and cid to tid really means moving back to normal
c++ design: you can design normal c++ base classes with pure virtual
methods, normal subclasses which provide implementation. The main thing
we are providing to the user is:
- a coherent memory management scheme (Ref/Unref/Ptr<>)
- a dynamic object aggregation/Query facility
- a generic factory system
- optionally, the 'object parameter' (below).
To get the former, all you have to do is subclass from Object. To get
the latter three, you have to provide an tid (TypeId) for your new type.
Every subclass of the Object base class is expected to define a TypeId.
The TypeId provides access to metadata about the underlying C++ type
such as:
- what is its parent TypeId (from a C++ inheritance perspective)
- does it provide constructors ? If so, how many ? How many
arguments do these constructors take ?
- does it provide 'parameters' ? If so, their name, etc.
- If there are constructors, allow you to invoke them to create a
new instance of a C++ type with a call to a non-static member method
TypeId::CreateObject. "
This also has a benefit of replacing the current ComponentManager
implementation and gets rid of static initialization ordering problems
and overall simplifies the implementation, from Mathieu's perspective.
Craig has observed that this change does not totally solve the
problem of distinguishing between Component and Interface and may
make it even worse. After some discussion, it seemed like there was
no way to solve this issue outside of documentation-- we have purposely
designed a system where objects can be used as interfaces or not,
or as components or not, and we seem to want this flexibility, so we
have to deal with the possible ambiguity that is the flip side of
flexibility (through code comments).
While the merging of ClassId and InterfaceId to TypeId paves the way
for the Parameter proposal (below), Mathieu has stated that independent
of the Parameter decision, the above TypeId proposal would still add
some value.
2.2 Simulation configuration
======================
We have the current default value system, which is a system for
providing globally scoped variables and hooking them into command
line syntax.
How we choose to do (or change how we do) simulation configuration also
will have a lot of bearing on the topology code (API) we are about to
define.
Limitations:
4a) Not easy to document which objects use which parameters
4b) Have to provide setters and getters for all of these parameters
(i.e., these access functions are not automatically generated)
4c) Cannot control the temporal scope of when a parameter can be
used, such as enforcing that it must be used only before
simulation::Run() is called.
4d) Cannot locally override the globals in an easy manner.
We also have to consider not only how a user can hook into the
current default values, but how he/she could define a new one.
---
Mathieu proposed the polymorphic Parameters class to try to address
the above limitations. As mentioned above, the proposed TypeId patch
would enable the Parameters proposal. the
Parameters class addresses the above problems:
4a): The TypeId declaration lists the parameters in use. Doxygen
introspection can be used to generate the appropriate documentation.
4b): Set() and Get() are provided as base methods.
4c): Scoping is possible. Such as a virtual Set() that restricts
setting after Simulation::Now() > 0.
- ability to attach arbitrary flags to a parameter which specify which
operations are allowed/disallowed: CONTRUCT_ONLY, READ_ONLY, WRITE_ONLY,
etc.
- ability to separate the parameter default value and the
setting/getting process. i.e., to have a bool parameter which is a
member variable or which is set through a setter
4d): The idea is that CreateObject<> is overloaded with an optional
Parameter object that can be passed into the constructor and that
overrides the global default values. That is, this is an extension
of what we presently have where the values are global; here, we provide
globals but allow override by local Parameter object.
Here is some sample code, for an object that uses one of these:
class ParameterObjectTest : public Object
{
public:
static InterfaceId iid (void) {
static InterfaceId iid = InterfaceId ("ParameterObjectTest")
.SetParent<Object> ()
.AddParameter ("TestGroup", "TestBoolName", "help text",
MakeBooleanParameter (&ParameterObjectTest::m_boolTest,
false));
return iid;
}
private:
bool m_boolTest;
};
Here, the object that uses this parameter calls an AddParameter
function that takes as arguments a "group" (for documentation/organizational
purposes), a unique name, help text (as before), and syntax similar
to our Callback API.
Then, client code has the following options: use whatever has
been globally defined:
Ptr<ParameterObjectTest> p = CreateObject<ParameterObjectTest> ();
or override the global before creating:
Parameters::GetGlobal ()->Set ("TestGroup::TestBoolName", "true");
Ptr<ParameterObjectTest> p = CreateObject<ParameterObjectTest> ();
or locally override by creating a Parameter object and passing it
into CreateObject<>:
Parameters params;
params.Set ("TestGroup::TestBoolName", "true");
Ptr<ParameterObjectTest> p = CreateObject<ParameterObjectTest> (params);
Maybe this can also be thought of as a way to get non-default constructors
for these objects? This would help the Topology code, perhaps.
Some details/questions have been identified about this proposal:
- how to specify (C++ object value, or string) these values?
- how will these be handled by composite objects. For instance,
if WifiNetDevice is a composition of WifiPhy, WifiRateControl, etc.,
how does the user of WifiNetDevice know about (and try to parameterize)
the WifiPhy underneath? Mathieu suggested that making Parameter
work for these composite objects was still not well defined and may
be hard.
- how do Python bindings handle these?
- how do default values that do not have value semantics get handled
by the system?
More general issues to discuss:
- Is this a good conceptual model to present to the user?
- Is there an alternate implementation that would be easier to deal with?
- Do we want really to plumb "params" throughout our public APIs?
3) Sample user programs
===============
Here is an attempt at showing how the simple-point-to-point.cc might
change:
int
main (int argc, char *argv[])
{
// Set up some default values for the simulation.
Parameters::GetGlobal ()->Set ("Queue::DefaultQueue", "DropTailQueue");
Parameters::GetGlobal ()->Set
("Application::OnOffApplicationPacketSize", "210");
Parameters::GetGlobal ()->Set
("Application::OnOffApplicationDataRate", "448kb/s");
// Allow the user to override any of the defaults and the above
// Set()s at run-time, via command-line arguments
CommandLine::Parse (argc, argv);
// Here, we will explicitly create four nodes. In more sophisticated
// topologies, we could configure a node factory.
NS_LOG_INFO ("Create nodes.");
Ptr<Node> n0 = CreateObject<InternetNode> ();
Ptr<Node> n1 = CreateObject<InternetNode> ();
Ptr<Node> n2 = CreateObject<InternetNode> ();
Ptr<Node> n3 = CreateObject<InternetNode> ();
Parameters params;
params.Set ("PointToPoint::ChannelDataRate", "5000000");
params.Set ("PointToPoint::ChannelDelay", "2");
Ptr<PointToPointChannel> channel0 =
PointToPointTopology::AddPointToPointLink ( n0, n2, params);
Ptr<PointToPointChannel> channel1 =
PointToPointTopology::AddPointToPointLink ( n1, n2, params);
params.Set ("PointToPointTopology::Milliseconds", "10");
Ptr<PointToPointChannel> channel2 =
PointToPointTopology::AddPointToPointLink ( n2, n3, params);
etc.
And then users would declare something like:
class PointToPointChannel : public Channel {
public:
static TypeId GetTid (void) {
static TypeId tid = TypeId ("PointToPointChannel")
.SetParent<Channel> ()
.AddParameter ("PointToPoint", "ChannelDelay", " (help text...)",
MakeIntegerParameter (&PointToPointChannel::m_delay, 0));
.AddParameter ("PointToPoint", "ChannelDataRate", " (help
text...)", MakeIntegerParameter (&PointToPointChannel::m_bps,
0xffffff));
return tid;
}
... (rest of class declaration)
Not shown here is how the TypeId would be used by a factory to allow
for component-manager instantiation, but the idea is that we'd preserve
that usage.
So, if a user wants to know the API of the object he or she has just
instantiated, he or she must look at the inheritance tree of the
object (typical C++) plus the inheritance tree of the TypeId to
see all of the default values (parameters) in use, plus the Doxygen
documentation for aggregated objects.
More information about the Ns-developers
mailing list