Don't click here unless you want to be banned.

LSL Wiki : LibrarySynchronousDescReturn

HomePage :: PageIndex :: RecentChanges :: RecentlyCommented :: UserSettings :: You are ia360925.us.archive.org
To receive a return value synchronously, a module declares these two functions:
// ========== For synchronous return values ==========
list parsePacket(string src) {
    return [
        llGetSubString(src, 0, 3), // Hex return ID.
        llGetSubString(src, 4, 5), // Hex packet number.
        llGetSubString(src, 6, 7), // Hex packet count.
        llDeleteSubString(src, 0, 7) // Message data.
    ];
}
string RECEIVED_MSG = "RECEIVED";
list getReturnValue(integer returnId, list defaultVal, float timeout) {
    string builtValue;
    list parsedDesc;
    string desc;
    float startTime = llGetTime();
    // Wait for the returner script to post my return ID
    // to the description.
    do {
        desc = llGetObjectDesc();
        parsedDesc = parsePacket(desc);
    } while ((integer)("0x"+llList2String(parsedDesc, 0)) != returnId && llGetTime() - startTime < timeout && desc != "RESET");
    if (desc == "RESET" || llGetTime() - startTime >= timeout)
        return defaultVal;
    integer count = (integer)("0x"+llList2String(parsedDesc, 2));
    integer lastReceived = -1;
    do {
        string data = llList2String(parsedDesc, 3);
        if ((integer)("0x"+llList2String(parsedDesc, 0)) == returnId && data != RECEIVED_MSG) {
            if (lastReceived + 1 == (integer)("0x"+llList2String(parsedDesc, 1))) {
                // Received next packet.
                builtValue += data;
                ++lastReceived;
            }
            string metadata = llGetSubString((string)parsedDesc, 0, 7);
            llSetObjectDesc(metadata + RECEIVED_MSG);
        }
        desc = llGetObjectDesc();
        parsedDesc = parsePacket(desc);
    } while (lastReceived + 1 != count && desc != "RESET");
    if (desc == "RESET")
        return defaultVal;
    return stringToList(builtValue);
}
// =============================================

The method stub has to change a bit too:
integer m_doSomething() {
    integer retID = (integer)llFrand(187431); // MUST BE UNIQUE
    callMethod(0, "returnListen", [retID]); // MUST BE CALLED BEFORE METHOD CALL.
    callMethod(retID, "doSomething", []);
    return retID;
}

When calling the method, you need to wrap it in a getReturnValue call.

default {
    state_entry() {
        list returnValue = getReturnValue(m_doSomething(), ["argh"], 120);
        if (llList2String(returnValue, 0) == "argh") {
            llOwnerSay("Synchro return failed.");
        } else {
            llOwnerSay("Return value: " + (string)returnValue);
        }
    }
}
getReturnValue blocks until it receives the return value or it times out. If it times out, it returns the default value passed to it. The callee's code doesn't need to change at all, it still acts as though its returning a value asyncronously. You do, however, need the SynchronousDescReturn module in the object's contents.

The SyncronousDescReturn module:
// Allows modules to call the blocking getReturnValue function
// when calling a method that "returns" a value 
// (calls a corresponding <methodName>_ret method).
// This module uses the object's description field for data transfer
// but makes a best-effort attempt to preserve the user-specified
// description. As it cannot 100% guarentee this, 
// it is recommended to not use the description field 
// descriptively. 
// See the Synchronous Return Values notecard for details
// about implementing the required structure in the caller.

// How long (in seconds) to wait for a module
// to acknowledge the return packet was received.
float RETURN_TIMEOUT = 2.5;
// What the receiveing module posts to the description
// when it receives a packet.
string RECEIVED_MSG = "RECEIVED";
// Maximum number of characters in the description.
integer MAX_DESC = 127;


// ========== For method invocation ==========
string randomSeperator(integer len) {
    integer firstChar = (integer)llFrand(60) + 20;  // Range of printable chars = 0x20 to 0x7E
    if (len <= 1)
        return llUnescapeURL("%"+(string)firstChar);
    integer lastChar;
    do { // Last char must not equal first char.
        lastChar = (integer)llFrand(60) + 20; 
    } while (lastChar == firstChar);
    string ret = llUnescapeURL("%"+(string)firstChar);
    for (len -= 2; len > 0; --len)
        ret += llUnescapeURL("%" + (string)((integer)llFrand(60) + 20));
    return ret + llUnescapeURL("%"+(string)lastChar);
}

string listToString(list src) {
    string chars = (string) src; // Squashes all elements together.
    string seperator;
    do { // Find a seperator that's not in the list's string form
        seperator = randomSeperator(3); // so we dont kill data.
    } while (llSubStringIndex(chars, seperator) != -1);
    return seperator + llDumpList2String(src, seperator);
}

list stringToList(string src) { // First 3 chars is seperator.
    return llParseStringKeepNulls(llDeleteSubString(src, 0, 2),
        [llGetSubString(src, 0, 2)], []);
}

callMethod(integer identifyer, string methodName, list parameters) {
    llMessageLinked(LINK_THIS, identifyer, // ID only necessary for return value.
        listToString(parameters), methodName);
}

returnValue(integer identifyer, string methodName, list value) {
    callMethod(identifyer, methodName + "_ret", value);
}
// =============================================
m_moduleReset(string moduleName) {
    callMethod(0, "moduleReset", [moduleName]);
}

string intToHex(integer x, integer len) {
    string hexc="0123456789ABCDEF";
    integer x0 = x & 0xF;
    string res = llGetSubString(hexc,x0,x0);
    x = (x >> 4) & 0x0FFFFFFF; //otherwise we get infinite loop on negatives.
    while (x != 0) {
        x0 = x & 0xF;
        res = llGetSubString(hexc,x0,x0) + res;
        x = x >> 4;
    }
    for (len -= llStringLength(res); len > 0; --len)
        res = "0" + res;
    return res;
}

list parsePacket(string src) {
    return [
        llGetSubString(src, 0, 3), // Hex return ID.
        llGetSubString(src, 4, 5), // Hex packet number.
        llGetSubString(src, 6, 7), // Hex packet count.
        llDeleteSubString(src, 0, 7) // Message data.
    ];
}

sendPacket(integer returnIndex, integer packetNum) {
    integer dataLength = MAX_DESC - 8; // 4-char ID + 2-char num + 2-char count.
    integer startIndex = packetNum * dataLength;
    string returnValue = llList2String(returnValues, returnIndex);
    string packetData = llGetSubString(returnValue, startIndex, startIndex + dataLength - 1);
    integer packetCount = llCeil(llStringLength(returnValue) / (float)dataLength);
    packet = llList2String(returnIds, returnIndex)
        + intToHex(packetNum, 2)
        + intToHex(packetCount, 2)
        + packetData;
    llSetObjectDesc(packet);
    //llOwnerSay("Sent packet " + (string)packetNum+": "+ packet);
    lastPacketSent = llListReplaceList(lastPacketSent, [packetNum], returnIndex, returnIndex);
    llResetTime();
}

deleteReturnData(integer index) {
    lastPacketSent = llDeleteSubList(lastPacketSent, index, index);
    returnValues   = llDeleteSubList(returnValues,   index, index);
    returnIds        = llDeleteSubList(returnIds,    index, index);
    if (llGetListLength(returnIds) == 0) {
        llSetTimerEvent(0);
        llSetObjectDesc(originalDesc);
    }
}

addReturnData(integer id, string data) {
    string hexId = intToHex(id, 4);
    if (llListFindList(returnIds, [hexId]) != -1)
        return; // TODO: Make this smarter.
    returnIds      += hexId;
    returnValues   += data;
    lastPacketSent += -1;
    llSetTimerEvent(0.1);
}

list waitingReturns;
list returnValues;
list returnIds;
list lastPacketSent;
string originalDesc;
string packet;

string this;
default {
    state_entry() {
        this = llGetScriptName();
        m_moduleReset(this);
        originalDesc = llGetObjectDesc();
        llSetObjectDesc("RESET");
        llSleep(5);
        if (originalDesc != "RESET") {
            llSetObjectDesc(originalDesc);
        } else {
            llSetObjectDesc("");
        }
    }
    link_message(integer sender, integer call, string parameters, key methodName) {
        if (methodName == "returnListen") {
            list paramList = stringToList(parameters);
            // Method sig: 
            // returnListen(integer id) 
            // Called before a method returning a value is called - indicates
            // that the calling module wants to receive the data syncronously.
            integer id = llList2Integer(paramList, 0);
            if (llListFindList(waitingReturns, [id]) == -1)
                waitingReturns += id;
        } else if (llDeleteSubString(methodName, 0, -5) == "_ret") {
            integer index = llListFindList(waitingReturns, [call]);
            if (index != -1) {
                //llOwnerSay("Heard registered return value.");
                originalDesc = llGetObjectDesc();
                waitingReturns = llDeleteSubList(waitingReturns, index, index);
                addReturnData(call, parameters);
            }
        } else if (methodName == "moduleReady") {
            list paramList = stringToList(parameters);
            string module = llList2String(paramList, 0);
            if (module == this) 
                returnValue(call, methodName, [TRUE]);
        }
    }
    timer() {
        string desc = llGetObjectDesc();
        list parsedDesc = parsePacket(desc);
        if (llList2Integer(lastPacketSent, 0) == -1 || desc == originalDesc) {
            // We haven't sent anything yet.
            sendPacket(0, 0);
        } else if (llList2String(parsedDesc, 3) == RECEIVED_MSG) {
            // See the parseDesc and sendPacket functions
            // for the format of the description. 
            string returnId = llList2String(parsedDesc, 0);
            integer returnIndex = llListFindList(returnIds, [returnId]);
            integer sentPacket = llList2Integer(lastPacketSent, returnIndex);
            if ((integer)("0x"+llList2String(parsedDesc, 1)) == sentPacket) {
                llResetTime();
                // I might want to compute the number of packets the message
                // has myself, just so I can trust the value. 
                // Right now Ill just use the val passed back to me.
                if (sentPacket + 1 == (integer)("0x"+llList2String(parsedDesc, 2))) {
                    // They received the last packet.
                    deleteReturnData(returnIndex);
                } else {
                    // Send the next packet.
                    sendPacket(returnIndex, sentPacket + 1);
                    //llOwnerSay("Sent packet " + (string)(sentPacket+1) + " of return " + returnId);
                }
            } else {
                //llOwnerSay("Resending packet " + (string)sentPacket + " of return " + returnId);
                // Stale return message, resend the packet.
                sendPacket(returnIndex, sentPacket);
            }
        } else if (desc != packet) {
            // Someone has set the description to something else
            // while the return operation was going on
            llSetObjectDesc(packet);
            llResetTime();
        } else {
            // We're waiting for the receiver to take
            // the data and post a RECEIVED_MSG.
            if (llGetTime() > RETURN_TIMEOUT) {
                deleteReturnData(0);
            }
        }
    }
}

How it Works

The concept is simple really. SynchronousDescReturn accepts call IDs via its returnListen method. It then listens to all return value method calls in a kind of "promiscuous" fashon. When one of these return calls matches a registered call ID, then it stores the value returned. It then broadcasts this value and the value's ID in the object's description. While all this is happening, every module that's expecting a synchronous return value is blocking in getReturnValue, continuously reading the object's description, waiting for the SynchronousDescReturn to post a return value with the ID it called the method with. Return values are sent in packets, due to the short 128 character limit put on the object's description. The packet format is similar to that used by the ExchangePacketChat protocol. Every time a packet is received by the calling module, the calling module posts a RECEIVED packet to the description.

Why this is evil

1. Its extremely easy to create a deadlock scenerio using synchro returns.
Take this example:
Module A calls Module B's method getFoo, and goes into getReturnValue, waiting for getFoo to return.
In Module B's getFoo, Module B calls Module A's getBar, and goes into getReturnValue, waiting for getBar to return.
Without timeouts, this would result in deadlock - neither module would respond to the other's request. This is why timeouts are good.
This is something inherant in the way the synchro returns work - each module is a seperate thread, so single-threaded stack push and pop emulation is pretty much impossible.
To avoid this you have to be careful where you use synchro returns.

2. Polling is hard on the sim.
Each module blocking in getReturnValue is requesting the object's description as fast as it can. This can potentially be a lot of calls to llGetObjectDesc. Simulator performance may take a significant hit when many modules are waiting for a return value.

3. Any failure in the system could result in an infinite loop.
If the SynchronousDescReturn" does not register the call ID before the callee returns the value, then the caller could potentially be left waiting for the Synchro module to post something that it never will. This is why timeouts are good.

4. It is slower then an async return.
Async returns will always be faster then synchro primarily because async only involves the caller and callee. In my testing, a simple synchro return call took about 3 seconds to complete, whereas that same call using async took 1.
Depending upon the number of times you need to call the method, an async return might be better even if it appears to increase the code's complexity.


LibraryContentBrowser
There is no comment on this page. [Display comments/form]