[MUD-Dev] NWN gets more MUD-like (again)

Smith Smith
Wed Feb 5 13:31:56 New Zealand Daylight Time 2003


Right,

I know that I really ought not do this to ya'll, as you're good
people, but I found out that I am seemingly wholly wrong in my
suppositions from yesterday, so I'll try to keep going. Eventually,
I'll probably hit on the method Avlis uses through sheer volume of
guesses.

> On Friday, January 31, 2003, Smith, David (Lynchburg) wrote:

> 3. The wrapper process must now "discover" the memory segment used
> by the server process for the "large blocks of memory" allocated
> by the script. I can see two ways to do this, all with attendant
> advantages and disadvantages.

Since I've given up (after some initial testing) on it "being that
easy", I'll start from the beginning, and try to work up to a more
reasonable (hopefully) theory regarding the "Avlis Persistence
System / Neverwinter Nights Extender" design. Mind you, just like
last time, I have no idea if my suppositions are correct, since the
Avlis team maintains a binary-only distribution of the "Neverwinter
Nights Extender" portion of the product (congratulations to them on
the intent of licensing the "Avlis Persistence System" under the
GPL; however, I have reservations about the propriety of this given
Bioware's license provisions).

>From the beginning:

  - The Neverwinter Nights scripting language is crippled with
  regards to IO.

  - Use of an external persistence mechanism requires IO.

Q: What would be an ideal method of linking the Neverwinter Nights
server process to another process?

  A1: SOCKETS!

  A2: Shared Memory

While sockets would provide some advantages over shared memory, it
seems likely that they "just won't happen". In addition, sockets
only buy you a few things over shared memory (importantly, they
provide event notification, but more on that later). Shared memory
would require that the Neverwinter Nights IO target be a local
process; however, that's not too much of an issue since most PW's
(and make no mistake, this really is only an interesting question if
you're considering Persistent Worlds) already run external
monitoring and control applications.

Additionally, in a shared memory solution, the power of the
Neverwinter Nights scripting language could conceivably be taken
advantage of directly, if the IO consructs written to and read from
the shared memory space could be written and read directly by the
Neverwinter Nights scripting language, then the "scripting" side of
the solution would be much simplified. Finally, shared memory is
fast. Especially, if the data were "put where the 'scripting' side
of the solution could use it directly".

So far, so good, but there's a major hangup; the Neverwinter Nights
script interpretter doesn't go out of its way to expose the
interpreter's memory as a shared memory resource. Instead, quite
reasonably, some part of the Neverwinter Nights server process
memory is just used by the interpreter.

So much for the normal methods of shared memory.

Well, is there any way to use the Neverwinter Nights server process
memory as a kind of shared memory? We've already implicitly accepted
that there will be two portions to the solution, a scripting portion
and an external portion. A part of that external portion is going to
have to exist locally (if we use a shared memory solution), and it
would be agreeable if the portion that exists locally perform the
kind of "server monitoring" functionality that is generally desired
of server wrappers.

Excellent, a server wrapper.

> 1. The wrapper process starts the server, probably with the flags
> CREATE_SUSPENDED and DEBUG_PROCESS

A bit more expansion is in order here. A Win32 api function
"CreateProcess" has the following description from the MSDN "The
CreateProcess function creates a new process and its primary thread.
The new process runs the specified executable file in the security
context of the calling process." The function allows the caller to
specify process creation flags, among those flags are the
"CREATE_SUSPENDED" and "DEBUG_PROCESS" flags. Additionally, the
CreateProcess function will fill in a PROCESS_INFORMATION structure
passed to it with information about the created process including a
handle to the process, and a handle to the process's main thread.
These handles have PROCESS_ALL_ACCESS and THREAD_ALL_ACCESS rights.

Among the rights granted in PROCESS_ALL_ACCESS are PROCESS_VM_WRITE
and PROCESS_VM_OPERATION. These rights are required for use of yet
another Win32 api function "WriteProcessMemory". Additionally, the
rights granted in PROCESS_ALL_ACCESS include PROCESS_VM_READ. This
right is required for use of yet another Win32 api function
"ReadProcessMemory". With these rights, and the handles discovered
from "CreateProcess", the server wrapper (the parent process) can
read/write memory 'within' the server (child process).

The server's scripting language can already read/write some of the
memory 'within' the server's process. Now, an external application
(the server wrapper) can read/write all of the memory 'within' the
server's process.

The intersection of what memory they can both read/write is "shared"
memory.

Excellent, and so far, just a further explanation down part of the
road from the previous post. If that post was largely in error,
where does the error lie?

To use shared memory effectively, there must be some synchronization
mechanism shared between the users of the shared memory. In any
message passed, the consumer must know where in the shared memory
space to read from, and must know when the producer has finished
writing to that shared memory space. Without a synchronization
mechanism, it would be difficult for the consumer to determine that
the producer has finished writing to the shared memory space, and
would probably require some variety of polling--reading the whole of
the shared memory space and looking for changes (and polling is bad,
m'kay?).

Above, much is made of calling "CreateProcess" with the
"DEBUG_PROCESS" flag. This makes the operating system consider the
parent process to be the debugger of the child process, and
_crucially_ allows the thread of the parent process which called
"CreateProcess" to successfully call "WaitForDebugEvent".
"WaitForDebugEvent" is a Win32 api function which waits for a
debugging event to occur in a process being debugged. One of the
most useful properties of a synchronization mechanism is the ability
to perform a "wait".

So, a "debug event" can work as a one-way synchronization mechanism
_from_ the child process _to_ the parent process. So, if the child
process 1) is the message producer, and 2) can cause an appropriate
"debug event", then half of the shared memory synchronization
mechanism is present. The previous post noted that:

> When the system notifies the debugger of a debugging event, it
> also suspends all threads in the affected process.

So, it's possible to work this behavior into a one-way
synchronization mechanism _from_ the parent process _to_ the child
process in a limited fashion. If the parent process is the message
producer (as it will be when servicing data requests), and the child
process is the message consumer (as it wants to be immediately after
issuing a data request), and since the child process can "wait"
immediately after having produced a "debug event" (in fact, it does
wait, all its threads are suspended by the system), and finally
since the parent process can deliver a "resume event" via the Win32
api call "ContinueDebugEvent", it is possible to work
debugger/debuggee behavior into a simple synchronization mechanism.

This synchronization mechanism can be fired to the parent from the
child (as is the case after a data request is written to the shared
memory segment), and can be fired to the child from the parent (as
is the case after a data reply is written to the shared memory
segment). This behavior is sufficent to provide the synchronization
needed to utilize a shared memory space.

Most of the above reasoning was implied in the previous post; that
post; however, concetrated on the "hard issue" of causing a debug
event from within the child process. One method was proposed,
integer division by zero. Unfortunately, testing indicated that the
Neverwinter Nights script interpretter does not allow integer
division by zero (yes, I know, you're wondering "that's
unfortunate?", but think context folks, think context).

Actually, context will, I believe, provide an answer to the
question: "How do we make the Neverwinter Nights script interpretter
raise a debug event". **NB: This section is filled with _fuzzy_
thinking, and half-substantiated guessing by the author; he's not
presently, and not likely to become, a talented assembly programmer
for the IA32 architecture. That notwithstanding, the IA32
architecture provides what are known as "watchpoints". These are
special kinds of breakpoints that will raise a debug event when data
is accessed. They may be set to cause a break when the watched
location is written, read/written, or executed. The ability to break
when a memory location is written is exactly what the child process
needs to utilize debug events as a synchronization mechanism in our
scheme above.

Watchpoints are specified by manipulating the register contents of a
particular thread utilizing the "GetThreadContext" and
"SetThreadContext" functions from the Win32 api. This manipulation
may be done only when the thread is suspended, and the manipulation
may be done from the debugging parent process. Additionally, the
debugging parent process may discover the threads of the child
process as they are created by analyzing a type of debug event
trapped by "WaitForDebugEvent", the CREATE_THREAD_DEBUG_EVENT.

Altogether, this method allows the following sequence.

  1.  parent process starts child process as a debugee, maintaining
  the process and main-thread handle.

  2.  parent process analyzes debug events during application
  initiation, maintaining the working thread handles.

  3.  child process initializes shared memory area with some special
  pattern

  4.  parent process suspends child process

  5.  parent process scans child process memory space for the
  pattern, maintaining the address of the shared memory area

  6.  parent process constructs appropriate thread contexts for all
  threads of the child process, specifying a write-breaking
  watchpoint for (a portion) of the shared memory area.

  7.  parent process resumes child process

  8.  child process writes to the watched byte of the shared memory
  area, debug event is fired, child process is suspended

  9.  parent process traps debug event, reads shared memory area

  10. parent process fills data request

  11. parent process writes data reply to shared memory area
  
  12. parent process resumes child process

  13. child process reads data reply from the shared memory area

  14. loop at 8.

The above steps are incomplete in that they ignore--at least--the
following issues:

  1. The shared memory area may move, making the watchpoint invalid,
  if the internal variable is reallocated by the scripting system.

  2. If the watched byte of the shared memory area is at the start
  of the shared memory space, then when the debug event fires, the
  entire message will not have been written. If the watched byte of
  the shared memory area is at the end of the shared memory space,
  not every message may overwrite that byte.

The first point may be taken care of by periodically suspending the
child process, scanning the memory space (with an initial sanity
check) to determine, via the special memory pattern, if the child
process has moved the shared memory space.

In a bit of what is either self-delusion, or sleuthing, an
investigation of #2 points to the following snippet from the "Avlis
Persistence System"
 
  void SQLExecDirect(string sSQL)
  {
    ...
    if (GetLocalInt(oModule, "NWNX!READY"))
    {
        SetLocalString(oModule, "NWNX1!", "NWNX!REQUEST!" + sSQL);
        SetLocalString(oModule, "NWNX1!", "NWNX!REQUEST!");
    }
    ...
  }

Notice, that when a query is performed, first the entire query is
written to the local string, then immediately thereafter the local
string is overwritten with the special memory pattern. This implies
that the query had better be read by the parent process between
those two writes. This leads me to believe that in the Avlis system,
the watched byte is one of the bytes written with the special memory
pattern, probably the first byte. If so, when the first call to
"SetLocalString(oModule, "NWNX1!", "NWNX!REQUEST!" + sSQL);" occurs,
a breakpoint would occur as soon as the first byte is written. When
the second call to "SetLocalString(oModule, "NWNX1!",
"NWNX!REQUEST!");" occurs, it would indicate to the parent process
that the producer has finished filling the shared memory space with
the request. The request could then be read and serviced.

In conclusion, no, I still don't know that this method presented is
the method used in the Avlis persistence system, but I'm more sure
that a variation of this method can be made to work with Neverwinter
Nights.

-Dave

_______________________________________________
MUD-Dev mailing list
MUD-Dev at kanga.nu
https://www.kanga.nu/lists/listinfo/mud-dev



More information about the MUD-Dev mailing list