[MUD-Dev] Re: DevMUD - thoughts.1

Chris Gray cg at ami-cg.GraySage.Edmonton.AB.CA
Sun Oct 25 10:22:00 New Zealand Daylight Time 1998


[James Wilson:]

 >Though I'm usually a big strong-typing partisan (I've been burned enough 
 >times at work by subtle Perl and Scheme bugs that would have been caught
 >with strong typing) I'd like to see a _truly_ dynamic type system, in
 >which an object's type can change dynamically over time. Then adding a 
 >new capability to an object (such as a new magical power) can be done at
 >runtime. This could be implemented on top of a strongly-typed system
 >in exactly the same way Scheme is implemented in C.

Adding things at run-time (new properties ("class members"), new
actions ("class methods")) is not incompatible with a strongly typed
language. You just have to have the right view of things, and not
be glued to the conventions that C++ and Java use. My MUD language
is strongly typed, but I can do this at run time (in wizard mode):
(Warning: 'private' does not mean what it does in C++ - it just says
who can see the newly defined *symbol*.)

    private newProperty CreateIntProp()$
    Me()@newProperty := 12;
    private appleEater CreateActionProp()$
    private proc eatGreenApple(thing theApple)void:
	Print("Ugh! That apple is horrible!\n");
	reduceHealth(Me(), theApple at sourness);
    corp;
    Me()@appleEater := eatGreenApple$
    /* Here is the key - the one break from strong typing: */
    call(Me()@appleEater, void)(theGreenApple)$
    /* That causes run-time checks that the retrieved function returns
       void, and accepts one parameter matching the type of 'theGreenApple'. */

The system is strongly typed in terms of variables, function parameters,
assignments, operators, etc. However, the database entities are completely
flexible - they can be modified at will.

 >I think this is an important feature which should be retained if at all
 >possible. It would allow a builder to try out some code in the interpreter,
 >do some rapid development, and, once things are working, have the 
 >interpreted stuff compiled to native code. Moreover, if one is doing funky
 >graphics or what have you, the extra performance could be a necessity.

I don't disagree. In my system, the very nature of the parser requires
going from compiled code to interpreted code to compiled code to
intepreted code (and possibly more) to handle nearly any input. What I'm
trying to avoid is requiring extra overhead for native => native calls
across module boundaries. That may not be important, but avoiding it
gives us more flexibility in what can be put into a module. Perhaps when
a module exports something, it can choose to export two variants, one
which uses native linkage, and one which uses interpreted linkage. The
core then uses native => native whenever both modules have that available.

 >I'm trying to understand, so let me ask if this example is something along
 >your lines:
 >
 >Module 'web_support' is set up to work using an external resource 
 >'g_net' which is some subclass of interface 'net_manager'. When 'web_support'
 >is loaded, it needs to find 'g_net' (i.e. resolve its external reference).
 >It asks some broker to give it 'g_net'. (There can be multiple specialized
 >brokers, or one huge one.) The broker determines if 'g_net' is currently 
 >available, and if not tries to find a module which claims to provide it. 
 >Assuming said module is found, it is loaded and the symbol is provided to
 >'web_support'.

That's a sort-of valid example, I think. I hadn't thought about the idea
of somehow automatically loading modules to resolve undefined references. I
was more concentrating on how explictly loaded modules would be tied
together. If we use 'dlopen' for loading, would a load module needed
by another be automatically loaded by dlopen? How would *our* loader know
that a given module exports a 'g_net' before someone has loaded that
module?

I had thought that we wanted the various modules to be compiled completely
independently of one-another. The only header file I had envisioned for
inter-module stuff was one that defined constants for all of the currently
existing interface kinds, and provides prototypes for that kind.

 >the issue of 'what goes into a module' is quite different from 'what should
 >be a thread'. Module 'web_support' could simply provide a set of functions
 >and classes for other modules to use, if they want. There is no need for a
 >thread there (although the underlying network functionality might well be
 >threaded). On the other hand, one might conceivably want a weather module
 >to start up a weather daemon thread when it loads (or multiple threads).
 >Depending on the event model, this might not make sense. (i.e. perhaps
 >all these modules do is provide types of events and event contexts, and
 >the threading strategy is chosen by the engine.)

I agree. That's what I was trying to say.

 >>	void inputHandler(void *source, uint kind, void *data, ulong len)
 >
 >right here is where the C++ goons come and break your kneecaps.

Ooh. Ouch! :-)

 >Using a
 >straight function call doesn't allow someone to write a handler which 
 >has its own individual state, e.g.
 >
 >static string s_magic;
 >
 >void inputHandler (/* rep */)
 >{
 >	// manipulate s_magic in a non-reentrant way
 >}
 >
 >is BAD in a multi-threaded environment (and inflexible in any case). The
 >alternative is quite appealing:
 >
 >struct input_handler
 >{
 >	virtual void handle (/* rep */) = 0;
 >};
 >
 >can be specialized to be thread-safe and have any state it likes:
 >
 >struct my_input_handler
 >{
 >	virtual void handle (/* rep */) 
 >	{ /* magic */ }
 >private:
 >	string _magic;
 >	mutex _lock;
 >};

I understand the C++ syntax you have written (except for "/* rep */" - is
that just meant to copy the prototype?), but I'm completely failing to
see what you are getting at. Sorry.

Why would a handler want state? It seems to me that in a persistent
virtual world, all state should either be attached to the entity
(e.g. player character) doing the action, or be global state attached to
some other persistent entity in the virtual world. In either case, the
preserving and retrieval of that state have to go through the system's
database. There should be very little static state in any modules, and
that all has to be protected by mutex's or spinlocks. Or, the module
could use queuing to serialize accesses. An example of static state would
be the set of client sockets that a telnet input module would maintain.

 >IMO, interfaces should be up to the module programmers. Think of a module
 >as a shared library; to use that shared library, you write a program that
 >#includes the proper headers, and link it against libfoo.so. It's up to
 >you to use libfoo's api according to the headers, and up to the compiler
 >to check your errors. If you want to write a new module with some funky
 >interface, and Bob wants to use said funky interface, that should be as
 >easy as #including your headers and linking against your library (or 
 >whatever analogue applies).

If some modules wish to use shared libraries, that is up to them. 'dlopen'
can take care of that. It is outside our concern. What I think we need is
ways whereby, e.g. modules that provide user input/output to the system
can be linked up to modules that provide parsing, scenario code, and
database operations. That linking up isn't done by name, it is done
based on the semantic nature of what is needed. Hmm. I suppose you could
use names to indicate that semantic nature. E.g. any module that exported
a parser would export a public symbol called 'parse', and the dlsym
facilities could be used to find that at run-time, and connect to it
from an input provider. However, that is just replacing the set of
constants in a header file with a set of strings, which would likely
also be in the header file beside the prototypes needed.

In this scheme of the header file containing the prototypes for the
interface kinds, it is quite easy to add new interface kinds. By
definition, no module is using or exporting an interface kind before
that interface kind is defined. OK, so someone needs a new one. They
add it to the big header file, along with the prototype they intend for
it. New modules can now be written that communicate using that new
interface kind. Old modules, which by definition do not use the new
kind, are not affected. Similar if someone wants old modules to use
the new kind - they must be modified to use it, and all is still well.
WOLOG (one of my math profs used that: WithOut Loss Of Generality).

There is no particular reason that the "one big header file" couldn't
be split up into smaller pieces, if there are specialized interfaces
that only a couple of modules need. However, there does need to be
some mechanism (Perl scripts?) to check that no interface kind ID
(whether a number or a name) is used more than once. We don't want
a parser calling a database's 'fixup' interface when it is supposed
to be calling an output filter's 'fixup' interface.

James, it seems to me you are thinking of building modules that are
aware of each other at compile time. I've been trying to avoid that,
so that only at run-time are they actually attached to one-another.
But then, I could be missing your point altogether. How about a
more detailed example?


Please, someone jump on me if I'm coming on too strong here. I'm not
trying to squelch anyone else's ideas and proposals. I'm just espousing
some thoughts I've had.

--
Chris Gray     cg at ami-cg.GraySage.Edmonton.AB.CA




More information about the MUD-Dev mailing list