TypeId in NS3

NS3作为一个网络仿真库,出于性能的考量选择了C++。在写仿真程序时,不可避免的要对各种实体进行建模,自然C++中的class成了唯一可选的方案。不加任何技巧的class的确可以满足对某些实体的建模,可是在仿真软件的编写中需要有足够的动态性,比如有这样一些需求:

  1. 动态的获知某个实体所具有的各类属性与属性的值
  2. 这个实体的状态变化后引发一系列的动作

这些都不是过分的需求,如果真的写过仿真程序的话肯定会非常渴求使用的软件能够提供实现这些需求的方法。要自己干巴巴的实现这些需求也不是不可以,比如可以提供一些查询接口来实现1;对于2的话,Qt的signal/slot或许可以实现。说到Qt了,其实QObject拥有了超越普通C++ class的能力,也都能满足上面指出的这些需求,但是其解决方案似乎有点重。

幸好,NS3通过TypeId可以很好的解决上面提出的各类需求。

TypeId是什么

class TypdId
{
    uint16_t m_tid;
}

这就是TypdId,就是这么简单。似乎有些不可思议,但TypdId本身只是一个16bit的整型,之前提到的所有的复杂功能都是藏在TypdId背后的IidManager完成的,这个m_tid只是作为一个索引而存在。

TypdId提供了非常多的方法,比如说增加一个属性(AddAttribute),增加一个TraceSource(AddTraceSource),这些方法只是直接了当的将所需信息搜集起来转发给IidManager。可以看个例子:

TypeId 
TypeId::AddAttribute (std::string name,
                      std::string help, 
                      uint32_t flags,
                      const AttributeValue &initialValue,
                      Ptr<const AttributeAccessor> accessor,
                      Ptr<const AttributeChecker> checker,
                      SupportLevel supportLevel,
                      const std::string &supportMsg)
{
  NS_LOG_FUNCTION (this << name << help << flags
                   << &initialValue << accessor << checker
                   << supportLevel << supportMsg);
  IidManager::Get ()->AddAttribute (m_tid, name, help, flags,
                                    initialValue.Copy (), accessor, checker,
                                    supportLevel, supportMsg);
  return *this;
}

基本上所有的TypdId的方法都是这个样子。所以解决问题的核心其实是IidManagerIidManager可认为是一个类型数据库,保存了与TypdId想关联的Attribute与TraceSource。具体的内部实现就太细节了,作为使用方是不需要也不应该去关注的。

从使用角度看TypdId

正如在Qt中一样,想要使自己写的一个类拥有强大的能力,需要自己动手在类的声明中添加Q_OBJECT。在NS3设计的TypeId系统中,这个步骤是要给自己的class添加一个静态方法static TypeId GetTypeId (void),然后在这个函数里返回一个TypeId。在这个过程,可以尽情的使用TypeId提供的各种方法来给本类加属性和TraceSource,唯一的限制就是这个返回的TypeId应该是处于GetTypeId里的静态变量,这是为了保证全局的唯一性。当然了,写C++的限制多了去了,这个规则应该归入这个NS3库的使用方法吧,不太值得吐槽。

TypeId对于我们平时写程序的最大的帮助在于,它可以给自己的类添加Attribute和TraceSource。

Attribute可以代表该实体的一些属性,比如说一台PC的IP地址、一只猫的体重等。你可能会想,这个不就是一个Get函数的事儿么,值得专门搞这么一套复杂的系统么。其实还真值得:你会去写一个127.0.0.1还是2130706433?在NS3里,可以直接写127.0.0.1,这也得归功与这个Attribute系统。

TraceSource可类比Qt的Signal,在需要的时候调用这个Functor(想不到更好的名称了,不过写C++的应该都知道这个东西),连到这个TraceSource的其他函数(所谓的Slot)就会被自动调用。好处自不必多说,要知道Qt能得到广泛的认可,Signal/Slot功不可没,NS3里的TraceSource系统就是Signal/Slot的翻版。

还有一个使用限制就是,需要通过一个宏来告知系统我一个TypeId要注册:NS_OBJECT_ENSURE_REGISTERED。这个宏其实声明了一个静态类并同时声明了一个本文件内的静态实例。在这个静态类的构造函数中调用了我们定义的类的GetTypeId,这就实现了自定义TypeId的注册。

Attribute与TraceSource

终于到重头戏了。其实这两部分的代码都切实的体现了C++罪恶的地方:模板与宏。一个新手要看懂这些代码要补充太多的东西了。说来惭愧,其实我自身也是一个新手,从开始接触这个库到现在能初步搞明白这样一个系统(真的只是大致初步明白),已经过去了3年多。这个系统的实现是模板里套宏、宏里套模板,看的时候需要时刻注意这段代码是宏生成的代码还是用了模板。

Attribute

前面提到了我们可以用127.0.0.1来代表ip的那32个bit,这就是Attribute系统的神奇所在。在这个例子里,127.0.0.1其实是一个字符串,Attribute系统可以把字符串转化为任何一种类型(可以是自定义类型)。

就单纯的以这个地址类型来解释好了。我们的程序中需要使用IP地址,其最合适的存储方式其实是一个int,但IP地址最适合人类的表述方式其实是点分表示,我们自然也想在使用的时候用这种方式。那这个应该怎么做?

首先先不管人怎么读写的问题,先考虑程序中的这个属性的使用方式。作为一个Ipv4的值,肯定有一些相关联的方法,比如说是否为广播地址、是否为多播地址、是否为本地地址之类类的。这些可以以用成员函数的方式实现,既然这样,那就尽情的实现吧!不需要考虑怎么集成到Attribute系统中去。同理,这个类里面有什么字段,想要什么字段就尽情的加。想必你也看出来了,我们在实现一个Attribute的时候,其实根本不需要考虑什么集成的问题。

能够用ns3的方式来给一个对象设置属性的这个能力依赖与3个基本的组件

  1. AttributeValue
  2. AttributeAccessor
  3. AttributeChecker

首先看看什么是ns3的方式为一个对象设置属性,看一下官方manual里的例子

Ptr<ConfigExample> a2_obj = CreateObject<ConfigExample> ();
a2_obj->SetAttribute ("TestInt16", IntegerValue (-3));
IntegerValue iv;
a2_obj->GetAttribute ("TestInt16", iv);

第一行创建了新的对象ConfigExample,并存在指针a2_obj里。第二行就是所谓的ns3的方式设置属性,依赖于一个方法SetAttriute。这个方法属于ObjectBase,所有能用Ptr指向的对象都是objectBase的子类。所以说,在调用SetAttribute时,除去C++的语法糖,这句话完整的形式是这样的:

SetAttribute (a2_obj, "TestInt16", IntegerValue (-3));

好了,我们跳进去看看实现

void
ObjectBase::SetAttribute (std::string name, const AttributeValue &value)
{
  NS_LOG_FUNCTION (this << name << &value);
  struct TypeId::AttributeInformation info;
  TypeId tid = GetInstanceTypeId ();
  if (!tid.LookupAttributeByName (name, &info))
    {
      NS_FATAL_ERROR ("Attribute name="<<name<<" does not exist for this object: tid="<<tid.GetName ());
    }
  if (!(info.flags & TypeId::ATTR_SET) ||
      !info.accessor->HasSetter ())
    {
      NS_FATAL_ERROR ("Attribute name="<<name<<" is not settable for this object: tid="<<tid.GetName ());
    }
  if (!DoSet (info.accessor, info.checker, value))
    {
      NS_FATAL_ERROR ("Attribute name="<<name<<" could not be set for this object: tid="<<tid.GetName ());
    }
}

这个方法是对象自己身上的方法,所以要记住this这时候指向的是谁:这里就是a2_obj。这个方法也很直白

  1. 首先能通过GetInstanceTypdId()拿到真正的、ConfigExampleTypeId
  2. 拿到AttributeInformation,就有了accessorchecker,还有作为参数传进来的值value
  3. DoSet做了实际的设置工作

再看看DoSet

bool
ObjectBase::DoSet (Ptr<const AttributeAccessor> accessor, 
                   Ptr<const AttributeChecker> checker,
                   const AttributeValue &value)
{
  NS_LOG_FUNCTION (this << accessor << checker << &value);
  Ptr<AttributeValue> v = checker->CreateValidValue (value);
  if (v == 0)
    {
      return false;
    }
  bool ok = accessor->Set (this, *v);
  return ok;
}

检查什么的就不说了,最让人关心的是这个方法accessor->Set (this, *v)。这个方法是怎么定义的,是哪里来的?这下欢迎进入模板与宏的世界。

AttributeAccessor

答案是这个这个方法是属于accessor的,而accessor的定义是在注册TypeId的时候生成的。RTFSC:

class ConfigExample : public Object
{
public:
  static TypeId GetTypeId (void) {
    static TypeId tid = TypeId ("ns3::A")
      .SetParent<Object> ()
      .AddAttribute ("TestInt16", "help text",
                     IntegerValue (-2),
                     MakeIntegerAccessor (&A::m_int16),
                     MakeIntegerChecker<int16_t> ())
      ;
      return tid;
    }
  int16_t m_int16;
};

NS_OBJECT_ENSURE_REGISTERED (ConfigExample);

看到那句MakeIntegerAccessor (&A::m_int16)了么?搞懂了这个,其实就能搞懂ns3的套路了,再看其他的机制也就顺风顺水了。我们慢慢来,一步一步来,保证每一步都有始有终,不会出现跳跃的现象。这个过程稍微有点冗长,可以去拿包零食边吃边看了。

MakeIntegerAccessor是可调用的一个“东西”。回想一下C++可调用的东西有哪些?1. 函数,2. Functor,就是实现了自定义operator()的一个class的实例。3.实例化一个类型看起来也像是函数调用。我用的Eclipse,f3跳转到定义,等我过去的时候傻眼了:

ATTRIBUTE_ACCESSOR_DEFINE (Integer);

好家伙,看来要展开这个看看了,ctrl+=让它现形:

template <typename T1>                                                \
Ptr<const AttributeAccessor> MakeIntegerAccessor (T1 a1)              \
{                                                                     \
  return MakeAccessorHelper<IntegerValue> (a1);                       \
}                                                                     \
template <typename T1, typename T2>                                   \
Ptr<const AttributeAccessor> MakeIntegerAccessor (T1 a1, T2 a2)       \
{                                                                     \
  return MakeAccessorHelper<IntegerValue> (a1, a2);                   \
}

这展开了2个函数,到这时可以确定,MakeIntegerAccessor是一个函数,而且我们调用的是只有一个入参的那个函数,这个函数返回了一个AttributeAccessor的智能指针。具体的展开过程就不细讲了,也没有讲的必要,看看ATTRIBUTE_ACCESSOR_DEFINE的定义就明白了。现在需要关心的是我们现在调用的函数里有个T1,要搞明白这个T1的类型是什么。

重新回头看看MakeIntegerAccessor (&A::m_int16),这里的T1就是&A::m_int16的类型。先就此打住,这个结论先记下来。我们继续追下去,这下应该看真正的实现MakeAccessorHelper<IntegerValue> (a1)

// 第一种实现
template <typename V, typename T, typename U>
inline
Ptr<const AttributeAccessor>
DoMakeAccessorHelperOne (U T::*memberVariable)

// 第二种实现
template <typename V, typename T, typename U>
inline
Ptr<const AttributeAccessor>
DoMakeAccessorHelperOne (U (T::*getter)(void) const)

// 第三种实现
template <typename V, typename T, typename U>
inline
Ptr<const AttributeAccessor>
DoMakeAccessorHelperOne (void (T::*setter)(U))

结果就是匹配到了第一种实现。 其实我曾经很多次追到了这里,却没看懂这里的类型到底是什么意思。也不知道什么时候忽然就明白了。A::m_int16对应于U T::*,是个正常人第一眼看上去绝对不会明白这到底是怎么联系在一起的,我也是正常人,所以我现在也不明白这种怪异的语法到底是谁第一次使用的。T对应于A,那么U应该是对应于m_int16。这个类型能代表一个类里的一个成员变量的类型,T表明了它是一个类的成员变量,U表明了这个变量的类型是uint16_t,现在就只能这么死记了,要想真正搞明白我觉得应该去翻一下编译器里前端到底是怎么解析这个鬼畜般的语法的,先就这么囫囵吞枣吧!对于另外的两个反而更好懂一点,那个类型和平时用的函数指针类型声明挺像的,反而不用多说。一个是getter,说明这个attribute只提供了获取的接口;一个是setter,说明这个attribute只能设置不能获取。当然了,这是站在ns3的使用方式上说的,直接强行用c++的方式赋值不在我们的讨论范围之内。

这3个函数都返回了一个指向AttributeAccessor的指针。现在来看看实现吧!

Ptr<const AttributeAccessor>
DoMakeAccessorHelperOne (U T::*memberVariable)
{
  /* AttributeAcessor implementation for a class member variable. */
  class MemberVariable : public AccessorHelper<T,V>
  {
public:
    /*
     * Construct from a class data member address.
     * \param [in] memberVariable The class data member address.
     */
    MemberVariable (U T::*memberVariable)
      : AccessorHelper<T,V> (),
        m_memberVariable (memberVariable)
    {}
private:
    virtual bool DoSet (T *object, const V *v) const {
      typename AccessorTrait<U>::Result tmp;
      bool ok = v->GetAccessor (tmp);
      if (!ok)
        {
          return false;
        }
      (object->*m_memberVariable) = tmp;
      return true;
    }
    virtual bool DoGet (const T *object, V *v) const {
      v->Set (object->*m_memberVariable);
      return true;
    }
    virtual bool HasGetter (void) const {
      return true;
    }
    virtual bool HasSetter (void) const {
      return true;
    }

    U T::*m_memberVariable;  // Address of the class data member.
  };
  return Ptr<const AttributeAccessor> (new MemberVariable (memberVariable), false);
}

照样很鬼畜。这是在函数里定义了一个类,并且返回了指向这个类的只能指针。这个类继承自AccessorHelper,而AccessorHelper又继承自AttributeAccessor。所以将其作为AttributeAccessor的子类返回也说得过去。

至于为什么要继承这么多?我现在的理解是这样的

  1. AttributeAccessor只是一个纯虚接口,它只定义了作为Accessor应当具有的接口。在Java里的话,估计这就是个Interface。
  2. AccessorHelper提供了SetGet的默认实现,把一些可变的部分留给了它的子类来实现,这些可变的部分是DoSetDoGet。所以在MemberVariable要实现DoSetDoGet。这应该是某种设计模式,看看那本书就能找到了。

到现在为止,我们知道可以造出来一个AttributeAccessor,并把指向这个AttributeAccessor的指针存在了我们的IidManager的数据库中。以后想要拿出来这个AttributeAccessor,就要手拿TypeId去找IidManager去要,而且要到的也是一个指针,这个指针指向了在return Ptr<const AttributeAccessor> (new MemberVariable (memberVariable), false);这句话里的那个new出来的地址。

总结一下,一个类型的AttributeAccessor只有一个,就是那个new出来的地方。程序其他地方都是拿指针去访问的。在那块地址存的东西只有两样(只考虑我们现在这个membervariable类型的accessor)

  1. U T::*m_memberVariable的值,这个值代表了这个变量在拥有TypeId那个类里的偏移量
  2. 一个虚表指针。因为是有virtual函数,所以这个AttributeAccessor的实例是有虚表指针的,这个虚表里就是真正的、对应类型的函数实现。

回头看看那个DoSet,里面那个accessor到底是什么应该已经清楚了。那个个accessor的Set方法在哪儿定义的?答案是AccessorHelper。我直接把结论公开了,但是你现在应该停下来去看看具体的实现。AttributeAccessor->AccessorHelper->DoMakeAccessorHelperOne()里的MemberVariable这是一条继承链,到了最下一层的时候所有的方法都已经定义,只是在不同的层次提供了不同的实现。

假设你已经搞明白Accessor的继承链条了,也明白这个Accessor到底支持什么操作,我们就进入了真正执行Set的地方:

// DoMakeAccessorHelperOne()里的MemberVariable的方法

virtual bool DoSet (T *object, const V *v) const {
  typename AccessorTrait<U>::Result tmp;
  bool ok = v->GetAccessor (tmp);
  if (!ok)
    {
      return false;
    }
  (object->*m_memberVariable) = tmp;
  return true;
}

这里的V *v就是myObj->SetAttribute ("MyIntAttribute", IntegerValue(3));里的IntegerValue(3)。要是没看懂,就去翻代码。这个结论是必须要搞懂的,不然就没有进行下去的必要了。

其实Accessor的世界已经探索的差不多了,为了真正搞明白这个函数做了什么,我们先转向看看AttributeValue

AttributeValue

NS3的套路是什么?用宏和模板做代码生成。这个套路在AttributeValue里也是一样的。自定义了一个AttributeValue需要写一个宏,这个宏帮助我们做了大部分的事情。拿那个IntegerValue说事儿:

ATTRIBUTE_VALUE_DEFINE_WITH_NAME (uint64_t, Uinteger);
// 在头文件里写这个宏,能够展开为如下的定义
class UintegerValue : public AttributeValue                             \
  {                                                                     \
  public:                                                               \
    UintegerValue ();                                                   \
    UintegerValue (const uint64_t &value);                              \
    void Set (const uint64_t &value);                                   \
    uint64_t Get (void) const;                                          \
    template <typename T>                                               \
      bool GetAccessor (T &value) const {                               \
      value = T (m_value);                                              \
      return true;                                                      \
    }                                                                   \
    virtual Ptr<AttributeValue> Copy (void) const;                      \
    virtual std::string                                                 \
      SerializeToString (Ptr<const AttributeChecker> checker) const;    \
    virtual bool                                                        \
      DeserializeFromString (std::string value,                         \
                             Ptr<const AttributeChecker> checker);      \
  private:                                                              \
    uint64_t m_value;                                                   \
  }

// 上述定义的实现仰赖于对于cc文件里的实现,也是用宏
ATTRIBUTE_VALUE_IMPLEMENT_WITH_NAME (uint64_t,Uinteger);

// 展开后是这样的
UintegerValue::UintegerValue ()                                         \
    : m_value () {}                                                     \
  UintegerValue::UintegerValue (const uint64_t &value)                  \
    : m_value (value) {}                                                \
  void UintegerValue::Set (const uint64_t &v) {                         \
    m_value = v;                                                        \
  }                                                                     \
  uint64_t  UintegerValue::Get (void) const {                           \
    return m_value;                                                     \
  }                                                                     \
  Ptr<AttributeValue>                                                   \
  UintegerValue::Copy (void) const {                                    \
    return ns3::Create<UintegerValue> (*this);                          \
  }                                                                     \
  std::string UintegerValue::SerializeToString                          \
    (Ptr<const AttributeChecker> checker) const {                       \
      std::ostringstream oss;                                           \
      oss << m_value;                                                   \
      return oss.str ();                                                \
  }                                                                     \
  bool UintegerValue::DeserializeFromString                             \
    (std::string value, Ptr<const AttributeChecker> checker) {          \
      std::istringstream iss;                                           \
      iss.str (value);                                                  \
      iss >> m_value;                                                   \
      do {                                                                 \
    if (!(iss.eof ()))                                                          \
      {                                                                \
        std::cerr << "aborted. cond=\"" << "!(iss.eof ())" << "\", ";           \
        do                                                    \
    {                                                   \
      std::cerr << "msg=\"" << "Attribute value " << "\"" << value << "\""  \
                           << " is not properly formatted" << "\", ";           \
      do                                                      \
    {                                                     \
      std::cerr << "file=" << "D:\\Code\\ns-allinone-3.28\\ns-3.28\\src\\core\\model\\uinteger.cc" << ", line=" <<    \
        35 << std::endl;                            \
      ::ns3::FatalImpl::FlushStreams ();                  \
      if (true) std::terminate ();                       \
    }                                                     \
  while (false);               \
    }                                                   \
  while (false);                                          \
      }                                                                \
  } while (false);            \
      return !iss.bad () && !iss.fail ();                               \
  }

具体的展开过程感兴趣的可以去追一下,要是没有IDE的帮助,要展开一个这么复杂的宏也还是需要一点时间的。结合之前accessor的内容(v->GetAccessor),这个函数就定义在了这里(头文件里作为模板类成员函数实现了)。

值得一看的倒是那两个SerializeToStringDeserializerFromString。这两个函数完成了字符串到实际定义类型的转换,里面用到了<<的重载,所以这也是为什么在自定义属性的时候要去实现全局operator<<的原因了。通过这两个函数,我们就可以用一个字面意义上的127.0.0.1去设置一个IP,而非2130706433。其实这个系统在解析string的时候出了一点小bug被我发现了,也算是对开源的一点点小贡献吧!(https://www.nsnam.org/bugzilla/show_bug.cgi?id=2447) 这个bug在ns3.25里提出来,之后应该是修好了。

还剩一个AttributeChecker,但这个好像不影响对系统的理解,就不去看啦!想必搞明白套路之后,要看懂也不是什么难事儿啦!

TraceSource

Callback杂谈

说起TraceSource,那么Callback就是绕不过去的坎。可以作为一个TraceSource的属性关联一个TracedCallback类型,用于通知自身值的改变。TracedCallback只是一个Callback的容器,里面有一个std::list<Callback>用于存放连接到该TraceSource的函数。一个TraceSource可以连接多次,每次它被Connect一次,就会往这个list里填一个元素。当然,这个元素就是一个Callback

Callback类本身只是提供了创建的接口于调用接口。调用接口就是对各个operator()的重载,最多有9个参数,也因此有9个operator()。这种情况在c++11之后应该会有更好的写法,只是我并不知道怎么写罢了。Callback继承自CallbackBase,这里存放了真正的指向实现(CallbackImplBase)的指针。

怎么CallbaciImpl还有继承?从Callback开始已经跳转两次了还没见到真正的实现,其实这也不远了,CallbackImplBase说到底就是一个Interface一样的东西,对CallbackImpl做了一些限定,这样继承了CallbackImpl的子类就能以比较一致的方法去操作。其实CallbackImplBase->CallbackImpl->各种具体的CallbackImpl弄这么复杂也是无奈之举。抽出来中间的CallbackImpl是为了实现多输入参数类型的operator()的重载,考虑到这个库的编写时间,那时候的c++模板编程好像没有c++11之后的那么完善,没有可变长类型参数,这样做也是无可厚非。我想如果用最新的c++11之后的标准来写,这个Callback可能就不会这么难以理解了,似乎可以直接采用std::function或者只是做一些小的改动就可以了。不管怎样,到了CallbackImpl这个层级时,单单CallbackImpl这个名字就已经可以支撑多达9个入参的operator()了,再下面层级的类就可以方便的享用这种便利,这也是为什么MemPtrCallbackImplFunctorCallbackImpl等子类可以在一个class里就重载多次operator()同时还能做类型检查的原因了。

TracedValue追踪

先来看看在TypeId里怎么使用TraceSource吧!我随便从代码里摘了一条出来

.AddTraceSource ("Tx", "A new packet is created and is sent",
                MakeTraceSourceAccessor (&BulkSendApplication::m_txTrace),
                "ns3::Packet::TracedCallback")

又看到了熟悉的Accessor,这个Accessor为的就是能拿到类里的一个成员变量。所幸对于TraceSource来说,只存在访问Get而不存在设置Set,这个Accessor相比起AttributeAccessor来说要简单一些。追到代码里看到的还是熟悉的的套路:

template <typename T>
Ptr<const TraceSourceAccessor> MakeTraceSourceAccessor (T a)
{
  return DoMakeTraceSourceAccessor (a);
}
// DoMakeTraceSourceAccessor的实现
// 在函数内部定义新的类,这个类实现了TraceSourceAccessor的接口
// 因为`TraceSource`只有一种类型,这种类型就是
// “类内部的成员变量”
// 所以可以看到函数的签名就只有一种
// SOURCE T::a
// sigh...又是这个鬼畜的标记
template <typename T, typename SOURCE>
Ptr<const TraceSourceAccessor> 
DoMakeTraceSourceAccessor (SOURCE T::*a)
{
  struct Accessor : public TraceSourceAccessor
  {
    virtual bool ConnectWithoutContext (ObjectBase *obj, const CallbackBase &cb) const {
      T *p = dynamic_cast<T*> (obj);
      if (p == 0)
        {
          return false;
        }
      (p->*m_source).ConnectWithoutContext (cb);
      return true;
    }
    virtual bool Connect (ObjectBase *obj, std::string context, const CallbackBase &cb) const {
      T *p = dynamic_cast<T*> (obj);
      if (p == 0)
        {
          return false;
        }
      (p->*m_source).Connect (cb, context);
      return true;
    }
    virtual bool DisconnectWithoutContext (ObjectBase *obj, const CallbackBase &cb) const {
      T *p = dynamic_cast<T*> (obj);
      if (p == 0)
        {
          return false;
        }
      (p->*m_source).DisconnectWithoutContext (cb);
      return true;
    }
    virtual bool Disconnect (ObjectBase *obj, std::string context, const CallbackBase &cb) const {
      T *p = dynamic_cast<T*> (obj);
      if (p == 0)
        {
          return false;
        }
      (p->*m_source).Disconnect (cb, context);
      return true;
    }
    SOURCE T::*m_source;
  } *accessor = new Accessor ();
  accessor->m_source = a;
  return Ptr<const TraceSourceAccessor> (accessor, false);
}

TraceSource存在的理由就是要触发其他的逻辑的,因此要提供挂接其他逻辑的方法,即Connect。这里的Accessor只是简单的把Connect的请求转发给了TraceSource。好几个类都有Connect,一不小心就晕头转向了,现在可以总结一下不同类的Connect到底做了什么,以及它们究竟时何时被调用的。

假设现在已经有一个ns3的类MyObject(fifth.cc),也继承了Object,意味着它实现了GetTypeId,拥有了AttributeTraceSource的能力。它有一个可以被trace的值m_myInt。这个值不是简单的类型,而是一个TracedValue<int32_t> m_myInt;。这样的话,只要对m_myInt进行赋值,Trace系统就可以工作了。给这个m_myInt赋值的话会调用什么?当然是operator=了。跳到TracedValue的对应实现看看:

TracedValue &operator = (const TracedValue &o) {
  TRACED_VALUE_DEBUG ("x=");
  Set (o.m_v);
  return *this;
}

// 关键就在`Set`里了,里面肯定有触发`Callback`的代码
void Set (const T &v) {
  if (m_v != v)
    {
      m_cb (m_v, v);
      m_v = v;
    }
}

果然,有个m_cb,这就是我们之前提到的TracedCallback。每次给这个m_myInt赋值,就会调用m_cb通知这个值已经变化。可以看到,TracedValue本身提供了一个Connect的方法,这意味着我们可以直接用m_myInt->Connect来把自己的处理函数连接上去。但是实际中往往是通过myObj->ConnectWithoutContext("myInt", MakeCallback(&mycallback))这样的方式。

bool 
ObjectBase::TraceConnectWithoutContext (std::string name, const CallbackBase &cb)
{
  NS_LOG_FUNCTION (this << name << &cb);
  TypeId tid = GetInstanceTypeId ();
  Ptr<const TraceSourceAccessor> accessor = tid.LookupTraceSourceByName (name);
  if (accessor == 0)
    {
      return false;
    }
  bool ok = accessor->ConnectWithoutContext (this, cb);
  return ok;
}

Accessor我们之前已经讲过了,是一个在GetTypeId里被调用并生成的一个类型,具体的accessor->ConnectWithoutContext在上面的DoMakeTraceSourceAccessor里有定义,还是通过了SOURCE T::*a这个类型得到了TracedValue在类中的位置,调用了这个类型的ConnectWithoutContext

至于TracedCallback,理解起来就没什么难度了,在这个类型的operator()里,注册进来的Callback以此执行一遍即可。

所以在运行是整个trace的过程是这样的:

  1. m_myInt=1;会跑到TracedValueoperator=
  2. TracedValue::operator=调用了与m_myInt相关联的TracedCallback::operator()
  3. TracedCallback::operator()以此调用事先注册好的Callback

总结

  1. IidManager是一个数据库,TypeId是数据库的key,Attribute和TraceSource是数据库的两张表。每在GetTypeIdAddAttribute或者AddTraceSouce一次就相当于给表里加一行记录,所有与AttributeTraceSource相关的操作都会去表里找自己需要的信息。
  2. 大量运用了模板和宏来生成一些框架代码,比如Accessor。这也是代码难以理解之处的根本所在。熟悉一些模板的套路,比如CRTP(在object模型里用到)、PIMPL(callback里用到);熟悉一些c++里的编程套路,比如在函数内部定义class(Accessor里用到),静态变量初始化(保证自定义Object可被注册时用到);以及字面意义上的代码生成(宏,#与##),类型这个层次的代码生成(template,多个类型参数,traits),都是需要去细心体会的。

后记

C++真难。

这套代码看下来,也是让人惊叹C++真的是没有做不到,只有想不到。那些模板和宏生成代码的套路,基本上把能在编译期算的都在编译器搞定,能在初始化搞的全在初始化时完成,运行时跑得都是很难再简化的必要的逻辑。其实网络仿真程序也基本算一个"well-defined"的东西,有着明确的需求,又是开源项目,可以花心思把系统设计的如此巧妙。

希望自己以后能有机会能从头参与类似项目的开发,而不是在反复无常的业务逻辑上消磨时光。