newspeoplefor developersdocumentationdownloads

Signal System
[Nebula Kernel]


Detailed Description

The signal system for Nebula. It allows native and script code to get called in a flexible way.

Introduction to Nebula Signals

The implementation of signals is based on the proposal written by Bruce Mitchener. There are some minor adaptations. The goals in the implementation are:

  • High performance signal emission, without penalties from C++ side.
  • Type safety in compile time in C++ code.

Signals can be created in two ways:

  1. Native signals: signals created in C++ code. This are faster to execute and allow to use the type-safety checking at compile time.
  2. Scripting signals: signals created from scripting. Can be created at any time, but are slower.

Signals can be bound to objects in two ways:

  1. Native bindings: bound to any C++ member function of a nObject or derived (not needed to be a scripting command).
  2. Scripting binding: bound to any nCmdProto or derived. In some scripting languages (like Lua and Python) it is possible to create nCmdProto to actually execute scripting code on binding, which gives a lot of flexibility.

All combinations are possible without limitations.

The signal functionality is available for any nObject or derived.

Nebula Signals Proposal

Include an adapted version of the proposal written by Bruce Mitchener here.

    From: Mateu Batle
    To: Bruce Mitchener
    Cc: nebuladevice-discuss@lists.sourceforge.net
    Subject: Re: [Nebula-Discuss] Event / Signal system, draft 3
    Date: Mon, 08 Nov 2004 20:31:43 +0100
    
    Hi !
    
    My 2 cents
    
    nSignal
    -------
    
    I assume nSignal type signature will use the same system than nCmdProto. 
    In fact, nSignal and nCmdProto are similar in some things like fourcc, 
    input and output arguments and type signature.
          
    nSignalBinding
    --------------      
          
    The issue about what to store (fourcc / nCmdProto) in nSignalBinding. I 
    would choose nCmdProto, since it will be much faster. And we could have 
    several ways to handle the issue with redefining nCmdProtos at runtime:
    
    a) Have a list of nSignalBinding objects either in nCmdProto or nClass. 
    If a given nCmdProto is removed or redefined, then remove from list or 
    change the nCmdProto ptr respectively. Maybe this is not needed, since 
    next there is already some link between nSignalBinding and target 
    objects, see next option.
    
    b) Misuse the existing reference to the target object in nSignalBinding 
    somehow. One idea is to add some virtual member to nRef, which is 
    implemented to suit our purposes in this case. Let's call it check(). 
    The check function will have the following parameters:
    
 int action. This is just an identifier which specifies what action 
    happened in the target object that could need some intervention in the 
    nRef handler. We could use NREF_ACTION_NCMDPROTO_CHANGED. This is 
    important since we must check for this in the check handler, since an 
    object could referenced by other things different than a nSignalBinding.
    
 nReferenced *. Pointer to the object requiring the check.
    
    For this to work, nSignalBinding must inherit from nRef, instead of 
    having it as a member variable. The check member function would be 
    override, to do the proper thing. In this case, it would look for the 
    class of the object, and resolve the fourcc to nCmdProto.
    
    The overhead added is that every nRef will have a virtual table ptr, 
    because of the addition of the virtual member function.
    
    This mechanism could be used in the future for other uses. Although I 
    agree it is not very clear and a bit quick & dirty solution. It is so 
    unclear, that I would not bet it works :-)
    
    c) Leave the responsability to the programmer. I think the redefinition 
    of nCmdProto is very uncommon, at least I have never seen any code using 
    it. For a very uncommon case, I don't think we should add so much 
    overhead. The same code that is doing the redefinition of commands, will 
    have to issue the fix of signal bindings through some interface provided 
    in the signal server for example.
    
    nSignalEmitter
    --------------
    
    About BindSignal, if report error or not in the case of binding multiple 
    times the same signal and nCmdProto. It does not come to my mind any 
    real useful example having multiple invocations of the same binding. 
    Anyway, if anybody thinks this is useful, we could have a parameter to 
    indicate if perform the check for this or not. Or maybe a different 
    function allowing repetitions.
    
    nSignalServer
    --------------
    
    I think we need to specify the time when the event will be emitted in 
    the PostSignal. The current spec just says the event happens the next 
    time the signal server is triggered. The signal server must be 
    configured to run every X seconds (predicted frame time for example), 
    then when the signal server is triggered it runs all the events 
    hapenning between currentTime and currentTime + X.
    
    Additionally, signal server must have functionality to clean the 
    asynchronous queues for all signals, signals of a given type, signals to 
    a specific object, ...
    
    
    I look forward for your changes to the event / signal system proposal in 
    relation to the nRoot refactoring, although I like the current state 
    pretty much. I can help with implementation once the discussion about 
    the proposal is finished.
    
    cheers !
      Mateu
    
    _______________________________________________
    Nebuladevice-discuss mailing list
    Nebuladevice-discuss@lists.sourceforge.net
    https://lists.sourceforge.net/lists/listinfo/nebuladevice-discuss

Usage of the Nebula Signals system

Below, we can see an explanation of the most common use cases. This section is focused in the usage from native C++ code, with some use examples from scripting. To hide the complexities of the signal subsystem, some preprocessor macros have been defined. Check the examples to see how to use them.

Signal declaration

This is done in a header file (.h) inside the public part of the class definition. It declares the following parameters:

    NSIGNAL_DECLARE(fourcc, RetType, SignalName, NumInArg, InArg, NumOutArg, OutArg)

  • fourcc: nFourCC identifier of the signal (if 0 is provided one is calculated).
  • RetType: return type for the signal.
  • SignalName: name of the signal (signal names are case-sensitive).
  • NumInArg: number of input arguments (from 0 to 6)
  • InArg: list of argument types between parenthesis and separated by commas: (bool, int, const char *). In the case of 0 arguments use ().
  • NumOutArg: number of output arguments (from 0 to 6)
  • OutArg: list of argument types between parenthesis and separated by commas. In the case of 0 arguments just use ().

There are many arguments, but they follow the standard ordering in for a C function (return type, signalname, argumennts), so it should not be so difficult to remember.

Signal name spaces have a class namespace, so they can be repeated in two different classes provided one is not subclass of the other. The same is true for the fourcc identifier.

Examples:

    class MyClass
    {
    public:

        NSIGNAL_DECLARE('COLL', bool, Collide, 2, (vector3 &, vector3 &), 0, ());
        NSIGNAL_DECLARE('OMMV', bool, OnMouseMoved, 1, (vector2 &), 0, ());
        NSIGNAL_DECLARE('OBDW', bool, OnButtonDown, 1, (vector2 &), 0, ());
        NSIGNAL_DECLARE('OCHR', void, OnChar, 1, (int));
        NSIGNAL_DECLARE('OKEY', void, OnKey, 1, (int));
        NSIGNAL_DECLARE('TRIG', void, Trigger, 0, ());

    };

Signal definition

Done in a code file (.cc), outside the scope of a function. It defines the signal object. The header where the signal was declared must be included before the definition. This is done because the signals are static objects, and so follow the rules of declaration & definition of static objects.

    NSIGNAL_DEFINE(ClassName,SignalName)

Examples:

    NSIGNAL_DEFINE(MyClass,Collide);
    NSIGNAL_DEFINE(MyClass,OnMouseMoved);
    NSIGNAL_DEFINE(MyClass,OnButtonDown);
    NSIGNAL_DEFINE(MyClass,OnChar);
    NSIGNAL_DEFINE(MyClass,OnKey);
    NSIGNAL_DEFINE(MyClass,Trigger);

Registration of signals

This is the same as is done for scripting commands. It is done in the n_initcmds function. Just use add one cl->AddSignal() method call for every signal you have defined, and place all these between cl->BeginSignals(NumberOfSignals) and cl->EndSignals().

Example:

    void
    n_initcmds(nClass* cl)
    {
        cl->BeginCmds();
        cl->EndCmds();
        cl->BeginSignals(8);
        cl->AddSignal(MyClass::SignalCollide);
        cl->AddSignal(MyClass::SignalOnMouseMoved);
        cl->AddSignal(MyClass::SignalOnButtonDown);
        cl->AddSignal(MyClass::SignalOnChar);
        cl->AddSignal(MyClass::SignalOnKey);
        cl->AddSignal(MyClass::SignalTrigger);
        cl->EndSignals();
    }

Here we add the static signal objects directly to the class. As you can see all signal objects are created with the 'Signal' prefix to avoid name clashing, but this is not necessary.

It is also possible to use the macro NSIGNAL_OBJECT and to not use the prefix 'Signal'. This macro is useful in all the calls that required the object signal.

        cl->AddSignal( NSIGNAL_OBJECT( MyClass, Trigger ) );

Signal binding

An emitter is the object emitting signals. Other objects (receivers) must bind to the emitter to be able to catch the signals when emitted. Any object derived from nObject can be a receiver. When the receiver is bound, it is bound to an specific member function. The binding action takes place inside a C++ code block. There are many ways to add bindings to a signal, depending on the type of signal and the type of binding.

For bindings of native signals to native bindings use:

    emitter->BindSignal(SignalObject, Receiver, MemberFunctionPointer, priority);

    obj->BindSignal(MyClass::SignalTrigger, recv, ReceiverClass::Trigger, 10);

Check out the other BindSignal methods in nSignalEmitter.

    bool BindSignal(nFourCC signal4cc, nSignalBinding * binding);
    bool BindSignal(nFourCC signal4cc, nObject * object, nCmdProto * cmdProto, int priority);
    bool BindSignal(nFourCC signal4cc, nObject * object, nFourCC cmdFourCC, int priority, bool rebind = false);
    bool BindSignal(nFourCC signal4cc, nObject * object, const char * cmdName, int priority, bool rebind = false);
    bool BindSignal(const char * signalName, nObject * object, const char * cmdName, int priority);

The priority parameter allows to specify the order in which bound objects will be called on invocation of the signal. Priorities with lower number are called first.

Signal emission

When the signal is emitted by the emitter object, all the bound objects are called in priority order.

This is an example of signal emission from C++ code, with type checking at compile time but only for native signals:

    // emitter->SignalName(emitter, arg1, arg2, arg3);

    emitter->SignalOnKey(emitter, 1024);

There are other ways of emitting signals, which are valid for any type of signal, but don't check at compile time like the previous way. Examples:

    emitter->EmitSignal( "onkey", 1024 );
    emitter->EmitSignal( &MyClass::SignalOnKey, 1024 );
    emitter->EmitSignal( &NSIGNAL_OBJECT( MyClass, OnKey ), 1024 );

The string name of the signal must be lower case.

Asynchronous signals

It is possible to emit a signal to be executed at the time specified. For this the signal server must be active. The time is provided in a relative value from the current time. The following command emits the signal in 10 seconds. If time 0 is specified the signals will be executed in the next trigger invocation of the signal server.

At the moment, there is no way to post signals with compile-time type checking (although this could be added if needed).

    emitter->PostSignal( 10.0f, "onkey", 1024 );
    emitter->PostSignal( 10.0f, &MyClass::SignalOnKey, 1024 );
    emitter->PostSignal( 10.0f, &NSIGNAL_OBJECT( MyClass, OnKey ), 1024 );

The method Trigger of the nSignalServer should be called to allow the asynchronous signals be called. The signal server executes all the signals with posted time < currentAppTime.

    signalServer.Trigger( currentAppTime );

Scripting from TCL

You can define signals from scripting code, these signals can be bound and emitted from script and from C++ code. The addsignal command takes two parameters, the first one is the signal definition and the second one is the FOURCC.

    /emitter.addsignal "v_newsignal_i" "SFS1"

The BindSignal method is visible to script. A posible call will be:

    /emitter.bindsignal "onkey" /receiver "onkeyreceiver" 0 

The bind from scripting requires that the method be a cmd method.

From TCL scripting the signals are emitted this way:

    # emit object.signalname arg1 arg2 arg3

    emit /emitter.onkey 1024
    emit .onkey 1024

Emit and Post take the object plus signal name, but if the object isn't present and the signal name is preceded with a period, they take the actual pwd.

From scripting it is also possible to post signals and regular commands:

    post 10.0 /emitter.onkey 1024

Scripting from Python

In Python the N.O.H. objects are mapped to Python objects, so the call to signals commands are different from TCL. In the next code we obtain the emitter and receiver objects.

    emitter = pynebula.lookup( '/emitter' )
    receiver = pynebula.lookup( '/receiver' )

This is how to add a signal in Python.

    emitter.addsignal( 'v_newsignal_i', 'SFS1' )

This code shows how to bind a signal.

    emitter.bindsignal( 'onkey', receiver, 'onkeyreceiver', 0 )

And this code shows how to emit and post a signal.

    emitter.emit( 'onkey', 1024 )

    emitter.post( 10.0, 'onkey', 1024 )

Scripting from Lua

From Lua scripting you can bind signals to objects, emit signals and post signals in the same way as Python scripting but with Lua syntax. Lua objects are obtained by calling the lookup function too.

    obj = lookup( '/receiver' )
    emitter = lookup( '/emitter' )
    sel( '/emitter' )

This is how to add a signal in Lua.

    call( 'addsignal', 'v_newsignal_i', 'SFS1' )

This code shows how to bind a signal.

    call( 'bindsignal', 'onkey', obj, 'onkeyreceiver', 0 )

In Lua emit and post are also object methods. Emit and post aren't cmds and can not be called with call function.

    emitter:emit( 'onkey', 1024 )

    emitter:post( 10.0, 'onkey', 1024 )

Lua allows you to add additional commands to a class's script interface at run-time. You can then implement the new commands in script per object. These added commands can be bound and signaled.

First there is a block of begincmds/endcmds to declare the script-side cmds which must be added to the nClass.

Second we obtain the thunk of the object, it is also neccesary to pin the thunk, so the Lua server can find it later and implement the commands. And then, we implement the code for the script in that object.

When the object is pinned, then we can implement the cmds.

Inside the new script functions of Lua, we can access to the call object using self implicit parameter.

    begincmds( 'receiverclass', 2 )
       addcmd( 'receiverclass', 'v_ScriptCmds1_v' )
       addcmd( 'receiverclass', 'v_ScriptCmds2_i' )
    endcmds( 'receiverclass' )
    
    node = lookup( '/signals/receiver' )
    pin(node)

    function node:ScriptCmds1()
       puts( 'function ScriptCmds1 called \n' )
    end \n

    function node:ScriptCmds2(num)
       puts( 'function ScriptCmds2 called with '.. num ..'\\n' )
    end

The block addcmds/endcmds from Lua don't effect the C++ block AddCmds/EndCmds of the same class, only add new cmds on the scripting side.

The script command names are case-sensitive and not lower-case like the cmds from C++ code.

The emit/post signal only effects the bound objects that have a script implementation of the script cmds.

    emitter->BindSignal( "trigger", Receiver, "ScriptCmds1", 0 );
    emitter->BindSignal( "onkey", Receiver, "ScriptCmds2", 0 );

    emitter->EmitSignal( "trigger" );
    emitter->EmitSignal( "onkey", 1024 );

Implementation notes

There are some changes between the original proposal and what has been implemented. Changes have been motivated to improve performance and type-safety.

  • nSignal. It is not just a single class. It is derived from nCmdProto for convenience. nSignal directly is used for signals defined from scripting or from dynamic C++ code. But normal C++ code will use nSignalNative template classes.

  • nSignalRegistry. At the moment has all the nClass extensions, this is mixed-in in nClass. It is better to be done separately since allows easier refactoring in the future.

  • In the registration of signals the call cl->BeginSignals(NumberOfSignals) specifies the number of signals to add. This number is a hint to create a hash table to put on the signals. It's possible to add more signals or less signals that the parameter without crashing the application.

  • In the registration of script-side cmds (in Lua for example) the call to endcmds actually does not do anything, and the begincmds(NumberOfSignals) is also a hint to create a hash table. So it's posible to addcmds, after the begincmds, for the same class 'ad eternum'. It is important to remember to call only one single time to begincmds for that class in all scripting code of all script servers.


Classes

class  nSignal
 nSignal, while largely internal, is a key concept in the signal system. More...
class  nSignalBinding
 Internal: nSignalBinding represents the callback to the signal receiver. More...
class  nSignalBindingCmdProto
class  nSignalBindingNative< TClass, TR, TListIn, TListOut >
 Fast binding in native world. More...
class  nSignalBindingSet
 nSignalBindingSet is a container of bindings. More...
class  nSignalEmitter
 This is the main class for the public consumption and how the programmer will interact with signals. More...
class  nSignalNative< TR, TListIn, TListOut, signal4cc >
class  nSignalRegistry
 nSignalRegistry is the interface for adding signals to an nClass. More...
class  nSignalServer
 The nSignalServer provides a means for asynchronously emitting a signal at some time in the future. More...

Copyright © 1999-2005 by the contributing authors. Ideas, requests, problems: Send feedback.