digitalmars.D - RPC and Dynamic function call
- Denis Koroskin (483/483) Nov 20 2009 I am working on a prototype of PRC library and it involves a wide range ...
- grauzone (55/64) Nov 20 2009 Yes, it is possible. You'll have to pass the method as alias template
- Denis Koroskin (63/127) Nov 21 2009 I thought about that, too. And then I thought:
- grauzone (18/40) Nov 21 2009 And you have to special case this in very awkward ways. What if a struct...
- Rory McGuire (18/21) Nov 21 2009 cut
- Denis Koroskin (4/24) Nov 21 2009 You missed the point. I only used asm to invoke function on remote side....
- Rory McGuire (126/166) Nov 22 2009 :) I thought I had missed the point thats why I asked :D.
I am working on a prototype of PRC library and it involves a wide range of techniques that I also implement. I'd like to share my code, implementation details and issues I come across. I will split my report into a few posts. This one is mostly an introduction. RPC is a concept that allows to execute some code in a remote application. To make it work, you must know function name, a set of arguments it accepts, type it returns etc. This is usually is not a problem if local and remote applications are same or based on shared code. The two applications may be ran in different environments (different Operation Systems, different memory modes, x86/x64 etc). In order to make a call a few components are required: Function (one that we want to call), context (remote object reference; not needed for static member functions and global/free functions), input/ouput arguments. A RPC requested is created locally. It contains everything in a list above. Input arguments might be reference type, in which case they might have references to other objects which in turn have references to other objects etc. In general, everything accessible through a set of input arguments might be accessed on remote side. This is why the whole object hierarchy accessible through input arguments need to be saved, sent over network and fully restored on remote side. This is called serialization. The fastest and most compact form of serialization is binary serialization. I'll talk about serialization in one of the next posts. Next thing we need to do is to somehow encode the function we are gonna call in a way that remote application will understand and get a proper function pointer out of this information. This has something to do with reflection (although pretty simple mechanism is required). Okay, now we received a RPC request that contains function pointer and a set of input arguments. We also have some knowledge on ouput arguments. All we need to do is to call that function and return back result. At this point, it's worth to note that all we have at remote side is some void[] array. No types. Once we step out of type system, there is no way back. It means that you can't really call a function in a type-safe manner anymore (well, one could create a set of trampolines for each of set of types involved in a call, but I don't think it's reasonable or even possible; I'll look into it, too, though). That's why you need to make a dynamic call: iterate over some kind of input arguments' type information, push values to stack, call function via funcptr, store the result etc. I was the last piece to make RPC work, that's what I was implementing today and that's what I will share in this post. The resulting code is quite small and simple. Note that all the asm it uses is just 3 constructs: naked, ret and call! Everything else is implemented with a help of compiler (i.e. pushing arguments, grabbing result, stack alignment etc). My code is most probably not very portable (I didn't test it on anything other that Windows), it doesn't account x64 specifics etc, but it shouldn't be hard to fix. When could it be useful? A few applications include RPC, reflection (possibly), binding to other languages (including scripting ones), code injection/hijacking, run-time code generation etc. Any suggestions, critics, comments are highly appreciated. Full listing below (tested with DMD2.036, Windows XP / Windows 7): module DynamicCall; import std.traits; import std.stdio; enum ParamType { // No return value Void, // ST0 Float, Double, Real, // EAX Byte, Word, DWord, Pointer, // AArray, // isn't tested (merge with Pointer?) //EDX,EAX QWord, DArray, // isn't tested // Hidden pointer Hidden_Pointer, // for returning large (>8 bytes) structs, not tested yet } struct Param { ParamType type; // type of value void* ptr; // pointer to value } // This struct describes everything needed to make a call struct Call { Param[] input; // set of input arguments Param output; // result info void* funcptr; // function pointer } // makes a call, stores result in call.ouput void makeDynamicCall(Call* call) { switch (call.output.type) { case ParamType.Void: _makeCall!(void)(call); break; case ParamType.Pointer: makeCall!(void*)(call); break; case ParamType.Byte: makeCall!(byte)(call); break; case ParamType.Word: makeCall!(short)(call); break; case ParamType.DWord: makeCall!(int)(call); break; case ParamType.QWord: makeCall!(long)(call); break; case ParamType.Float: makeCall!(float)(call); break; case ParamType.Double: makeCall!(double)(call); break; case ParamType.Real: makeCall!(real)(call); break; } } // helper function to save some typing void makeCall(T)(Call* call) { *cast(T*)call.output.ptr = _makeCall!(T)(call); } T _makeCall(T)(Call* call) { void* funcptr = call.funcptr; void* argptr; int numArgs = call.input.length; if (numArgs != 0) { // this check is needed because last parameter is passed in EAX (if possible) Param* param = call.input.ptr; // iterate over first numArgs-1 arguments for ( ; --numArgs; ++param) { /* // the following doesn't work for some reason (compiles but lead to wrong result in run-time) // would be so much more elegant! push!(arg)(param); /*/ argptr = param.ptr; switch (param.type) { case ParamType.Byte: // push byte arg(*cast(byte*)argptr); break; case ParamType.Word: // push word arg(*cast(short*)argptr); break; case ParamType.Pointer: case ParamType.DWord: // push dword arg(*cast(int*)argptr); break; case ParamType.QWord: // push qword arg(*cast(long*)argptr); break; case ParamType.Float: // push float arg(*cast(float*)argptr); break; case ParamType.Double: // push double arg(*cast(double*)argptr); break; case ParamType.Real: // push real arg(*cast(real*)argptr); break; } //*/ } // same as above but passes in EAX if possible /* push!(lastArg)(param); /*/ argptr = param.ptr; switch (param.type) { case ParamType.Byte: lastArg(*cast(byte*)argptr); break; case ParamType.Word: lastArg(*cast(short*)argptr); break; case ParamType.Pointer: case ParamType.DWord: lastArg(*cast(int*)argptr); break; case ParamType.QWord: lastArg(*cast(long*)argptr); break; case ParamType.Float: lastArg(*cast(float*)argptr); break; case ParamType.Double: lastArg(*cast(double*)argptr); case ParamType.Real: lastArg(*cast(real*)argptr); } //*/ } asm { // call it! call funcptr; } } // A helper function that pushes an argument to stack in a type-safe manner // extern (System) is used so that argument isn't passed via EAX // does it work the same way in Linux? Or Linux uses __cdecl? // There must be other way to pass all the arguments on stack, but this one works well so far // Beautiful, isn't it? extern (System) void arg(T)(T arg) { asm { naked; ret; } } // A helper function that pushes an argument to stack in a type-safe manner // Allowed to pass argumet via EAX (that's why it's extern (D)) void lastArg(T)(T arg) { asm { naked; ret; } } // Compare it to my older implementation: /+ T _makeCall(T)(Call* call) { void* funcptr = call.funcptr; void* argptr; int i = call.input.length; int eax = -1; foreach (ref param; call.input) { --i; argptr = param.ptr; switch (param.type) { case ParamType.Byte: // passing word asm { mov EDX, argptr; mov AL, byte ptr[EDX]; } if (i != 0) { asm { push EAX; } } else { asm { mov eax, EAX; } } break; case ParamType.Word: // passing word asm { mov EDX, argptr; mov AX, word ptr[EDX]; } if (i != 0) { asm { push EAX; } } else { asm { mov eax, EAX; } } break; case ParamType.Pointer: case ParamType.DWord: // passing word asm { mov EDX, argptr; mov EAX, dword ptr[EDX]; } if (i != 0) { asm { push EAX; } } else { asm { mov eax, EAX; } } break; case ParamType.QWord: // pushing word asm { mov EDX, argptr; mov EAX, dword ptr[EDX+4]; push EAX; mov EAX, dword ptr[EDX]; push EAX; } break; case ParamType.Float: // pushing float asm { sub ESP, 4; mov EAX, dword ptr[argptr]; fld dword ptr[EAX]; fstp dword ptr[ESP]; } break; case ParamType.Double: // pushing double asm { sub ESP, 8; mov EAX, qword ptr[argptr]; fld qword ptr[EAX]; fstp qword ptr[ESP]; } break; } } asm { mov EAX, eax; call funcptr; } } +/ // I was trying to move out common code to a separate function, but failed. It doesn't work for reasons unknown to me /+ void push(alias fun)(Param* param) { switch (param.type) { case ParamType.Byte: fun(*cast(byte*)param.ptr); break; case ParamType.Word: fun(*cast(short*)param.ptr); break; case ParamType.Pointer: case ParamType.DWord: fun(*cast(int*)param.ptr); break; case ParamType.QWord: fun(*cast(long*)param.ptr); break; case ParamType.Float: fun(*cast(float*)param.ptr); break; case ParamType.Double: fun(*cast(double*)param.ptr); break; case ParamType.Real: fun(*cast(real*)param.ptr); break; } } +/ // Convenient templates to map from type T to corresponding ParamType enum element template isStructSize(T, int size) { enum isStructSize = is (T == struct) && T.sizeof == size; } template ParamTypeFromT(T) if (is (T == byte) || is (T == ubyte) || is (T == char) || isStructSize!(T, 1)) { alias ParamType.Byte ParamTypeFromT; } template ParamTypeFromT(T) if (is (T == short) || is (T == ushort) || is (T == wchar) || isStructSize!(T, 2)) { alias ParamType.Word ParamTypeFromT; } template ParamTypeFromT(T) if (is (T == int) || is (T == uint) || is (T == dchar) || isStructSize!(T, 4)) { alias ParamType.DWord ParamTypeFromT; } template ParamTypeFromT(T) if (is (T == long) || is (T == ulong) || isStructSize!(T, 8) || is (T == delegate)) { alias ParamType.QWord ParamTypeFromT; } template ParamTypeFromT(T) if (is (T == float)) { alias ParamType.Float ParamTypeFromT; } template ParamTypeFromT(T) if (is (T == double)) { alias ParamType.Double ParamTypeFromT; } template ParamTypeFromT(T) if (is (T == real)) { alias ParamType.Real ParamTypeFromT; } template ParamTypeFromT(T) if (is (T == void)) { alias ParamType.Void ParamTypeFromT; } template ParamTypeFromT(T) if (isPointer!(T)) { alias ParamType.Pointer ParamTypeFromT; } Test case: import DynamicCall; import std.stdio; Param createParam(T)(T value) { //T* ptr = new T; // BUG! Doesn't work for delegate type T* ptr = cast(T*)(new void[T.sizeof]).ptr; *ptr = value; Param param; param.type = ParamTypeFromT!(T); param.ptr = cast(void*)ptr; return param; } struct b1 { char c; } struct b2 { wchar w; } struct b4 { dchar d; } real foo(byte b, short s, int i, long l, float f, double d, real r, b1 c, b2 w, b4 d2, int function() func, int delegate() dg, int* ptr) { writeln(b + s + i + l + f + d + r + c.c + w.w + d2.d + func() + dg() + *ptr); return 13; } int func() { return 42; } void main() { int dg() { return 14; } Call call; call.funcptr = &foo; int* i = new int; *i = 128; call.input ~= createParam!(byte)(cast(byte)1); // BUG! cast is mandatory to compile call.input ~= createParam!(short)(200); call.input ~= createParam!(int)(4); call.input ~= createParam!(long)(cast(long)8); call.input ~= createParam!(float)(16.0f); call.input ~= createParam!(double)(32.0); call.input ~= createParam!(real)(64.0); call.input ~= createParam!(b1)(b1(1)); call.input ~= createParam!(b2)(b2(2)); call.input ~= createParam!(b4)(b4(4)); call.input ~= createParam!(int function())(&func); call.input ~= createParam!(int delegate())(&dg); call.input ~= createParam!(int*)(i); call.output.type = ParamType.Real; call.output.ptr = new real; makeDynamicCall(&call); writeln(*cast(real*)call.output.ptr); }
Nov 20 2009
Denis Koroskin wrote:type-safe manner anymore (well, one could create a set of trampolines for each of set of types involved in a call, but I don't think it's reasonable or even possible; I'll look into it, too, though). That's whyYes, it is possible. You'll have to pass the method as alias template parameter. Then you get a tuple of the parameter types. You can foreach over that tuple and serialize/deserialize the actual values and read/write them from the tuple. You also can declare a nested function that does the actual call to the server's function. That delegate can have a type independent from the method, and thus can be stored in the non-template world. Basically like this (pseudo code): //client function to invoke a specific RPC //the parameters can be passed normally thanks to tuples void makeDynamicCall(Params...)(Stream stream, char[] method, Params p) { stream.write(method); //serialize the parameters foreach (int index, _; p) { auto val = p[index]; stream.write!(typeof(val))(val); } } alias void delegate(Stream) Stub; //registry of server functions Stub[char[]] stubs; //the server network code calls this on incomming RPC requests void receiveDynamicCall(Stream stream) { auto method = stream.read!(char[])(); stubs[method].call(stream); } //the server calls this on program initialization //he passes an alias to the server function, and its name void registerStub(alias Function)(char[] name) { //generate code to deserialize a RPC and to call the server void stub(Stream stream) { //you can get the param tuple of a function //Phobos2 and Tango should have something similar alias ParamTupleOfFunction!(Function) Params; //deserialize the arguments Params p; foreach (int index, _; p) { alias typeof(p[index]) PT; p[index] = stream.read!(PT)(); } //actually call the function Function(p); } stubs[name] = &stub; } It all can be typesafe and non-compiler specific. The question is; how much template bloat will this emit into the application binary? You have to consider the instantiations of Stream.read and Stream.write, too.Note that all the asm it uses is just 3 constructs: naked, ret and call! Everything else is implemented with a help of compiler (i.e. pushing arguments, grabbing result, stack alignment etc). My code is most probably not very portable (I didn't test it on anything other that Windows), it doesn't account x64 specifics etc, but it shouldn't be hard to fix.Wow, I'm surprised that this works. Actually, I'd *like* to do it this way, but the code is completely compiler and platform specific. Maybe it also depends from the compiler code generator's mood if it works. And if you want to deal with user-define types (structs), you're completely out of luck.
Nov 20 2009
On Sat, 21 Nov 2009 06:54:11 +0300, grauzone <none example.net> wrote:Denis Koroskin wrote:I thought about that, too. And then I thought: 1) you can move deserialization out of this method (my serializer doesn't need type hints to deserialize data). 2) you can reuse the trampoline for those functions that have same type of input argument (less bloat) 3) you can create a trampoline for each type, not for each set of types (N trampolines instead of N! in worst case) One more step and you don't even need for those trampolines (just carry some type information and translate call to trampoline into a switch). That's how I ended up with my implementation.type-safe manner anymore (well, one could create a set of trampolines for each of set of types involved in a call, but I don't think it's reasonable or even possible; I'll look into it, too, though). That's whyYes, it is possible. You'll have to pass the method as alias template parameter. Then you get a tuple of the parameter types. You can foreach over that tuple and serialize/deserialize the actual values and read/write them from the tuple. You also can declare a nested function that does the actual call to the server's function. That delegate can have a type independent from the method, and thus can be stored in the non-template world. Basically like this (pseudo code): //client function to invoke a specific RPC //the parameters can be passed normally thanks to tuples void makeDynamicCall(Params...)(Stream stream, char[] method, Params p) { stream.write(method); //serialize the parameters foreach (int index, _; p) { auto val = p[index]; stream.write!(typeof(val))(val); } } alias void delegate(Stream) Stub; //registry of server functions Stub[char[]] stubs; //the server network code calls this on incomming RPC requests void receiveDynamicCall(Stream stream) { auto method = stream.read!(char[])(); stubs[method].call(stream); } //the server calls this on program initialization //he passes an alias to the server function, and its name void registerStub(alias Function)(char[] name) { //generate code to deserialize a RPC and to call the server void stub(Stream stream) { //you can get the param tuple of a function //Phobos2 and Tango should have something similar alias ParamTupleOfFunction!(Function) Params; //deserialize the arguments Params p; foreach (int index, _; p) { alias typeof(p[index]) PT; p[index] = stream.read!(PT)(); } //actually call the function Function(p); } stubs[name] = &stub; } It all can be typesafe and non-compiler specific. The question is; how much template bloat will this emit into the application binary? You have to consider the instantiations of Stream.read and Stream.write, too.Why? It works well with custom structs, too. There is a test case attached, try to run in, it should work. One of the reasons I posted the code is because I'm not sure it is correct and that it will always work (on a given platform). I'm not an ASM guru, so comments would be invaluable. For example, I use the following trick to push arguments to stack: // A helper function that pushes an argument to stack in a type-safe manner // extern (System) is used so that argument isn't passed via EAX // does it work the same way in Linux? Or Linux uses __cdecl? // There must be other way to pass all the arguments on stack, but this one works well so far // Beautiful, isn't it? extern (System) void arg(T)(T arg) { asm { naked; ret; } } // A helper function that pushes an argument to stack in a type-safe manner // Allowed to pass argumet via EAX (that's why it's extern (D)) void lastArg(T)(T arg) { asm { naked; ret; } } Explanation: when one of these functions are called, arguments to them are pushed to stack and/or target registers properly (compiler does it for me automatically). By convention, extern (D) and extern (Windows) aka __stdcall function pop their arguments from the stack on their own. Naked function (asm naked) lack prolog and epilog (i.e. they don't pop arguments and restore stack). As a result, these function push their arguments to stack the way compiler wants them while still not being compiler specific. lastArg uses extern (D) calling convention that allows passing last argument in EAX (this is specified in http://www.digitalmars.com/d/2.0/abi.html) How safe it is? How portable it is? As far as I know all major compilers support naked functions (Microsoft C++ compiler: _declspec( naked ), GCC: __attribute__(naked)) This is 2 out of 3 uses of asm in my code. The third one is: void* functptr = ...; asm { call funcptr; } I think this is also implementable on other platforms. I updated code to work with Tango, could anybody please try to run it on Linux x86/x86_64 (ldc)? An output should be: 516.00 13.00Note that all the asm it uses is just 3 constructs: naked, ret and call! Everything else is implemented with a help of compiler (i.e. pushing arguments, grabbing result, stack alignment etc). My code is most probably not very portable (I didn't test it on anything other that Windows), it doesn't account x64 specifics etc, but it shouldn't be hard to fix.Wow, I'm surprised that this works. Actually, I'd *like* to do it this way, but the code is completely compiler and platform specific. Maybe it also depends from the compiler code generator's mood if it works. And if you want to deal with user-define types (structs), you're completely out of luck.
Nov 21 2009
Denis Koroskin wrote:I thought about that, too. And then I thought: 1) you can move deserialization out of this method (my serializer doesn't need type hints to deserialize data). 2) you can reuse the trampoline for those functions that have same type of input argument (less bloat) 3) you can create a trampoline for each type, not for each set of types (N trampolines instead of N! in worst case)That would be interesting.And you have to special case this in very awkward ways. What if a struct is larger than 8 bytes? This whole approach is so shaky, I don't trust it one bit. It would be another thing if all this would be part of the runtime core (and the compiler implementer makes sure it works).Wow, I'm surprised that this works. Actually, I'd *like* to do it this way, but the code is completely compiler and platform specific. Maybe it also depends from the compiler code generator's mood if it works. And if you want to deal with user-define types (structs), you're completely out of luck.Why? It works well with custom structs, too. There is a test case attached, try to run in, it should work.One of the reasons I posted the code is because I'm not sure it is correct and that it will always work (on a given platform). I'm not an ASM guru, so comments would be invaluable.Neither am I, but I guess it will be very hard to emulate calling conventions on non-x86 32 bit platforms. E.g. on amd64, several arguments are passed in registers (AFAIK). In any case, you trust the compiler not to insert additional code and not to touch the stack. Maybe it works with dmd, but what about ldc or gdc? Well, let's see if anyone can test it... Now, this doesn't really matter. You can always rewrite it as asm code. And in asm, you definitely can do such dynamic calls. One issue is that you need the static signature of the RPC target for type checking. What are you going to do about this?I updated code to work with Tango, could anybody please try to run it on Linux x86/x86_64 (ldc)? An output should be: 516.00 13.00Segfaulted on x86 32 bit / Tango / Linux.
Nov 21 2009
"Denis Koroskin" <2korden gmail.com> wrote:"Denis Koroskin" <2korden gmail.com> wrote: ...cut...Why did you use asm etc...? I made a de/serializer that just uses templates and static ifs. it doesn't do serialization of delegates etc... but I don't know that moving executable code across would be safe (I think java can do it, but then it has a vm). mine does: int, long, double, string, object, struct. I can post the code if you want. So far I have only used it to present the methods of an object as a kind of server, with a client being able to connect to it. Was inspired by hessiand on dsource.org. Can post code if you would like to see. I posted the automatic object wrapping code a couple of days ago as a simple example of template usage Thread: "Metaprogramming in D: Some real world examples". -Rory
Nov 21 2009
On Sat, 21 Nov 2009 12:42:35 +0300, Rory McGuire <rjmcguire gmail.com> wrote:"Denis Koroskin" <2korden gmail.com> wrote:You missed the point. I only used asm to invoke function on remote side. Serialization is done purely with templates."Denis Koroskin" <2korden gmail.com> wrote: ...cut...Why did you use asm etc...? I made a de/serializer that just uses templates and static ifs. it doesn't do serialization of delegates etc... but I don't know that moving executable code across would be safe (I think java can do it, but then it has a vm). mine does: int, long, double, string, object, struct. I can post the code if you want. So far I have only used it to present the methods of an object as a kind of server, with a client being able to connect to it. Was inspired by hessiand on dsource.org. Can post code if you would like to see. I posted the automatic object wrapping code a couple of days ago as a simple example of template usage Thread: "Metaprogramming in D: Some real world examples". -Rory
Nov 21 2009
"Denis Koroskin" <2korden gmail.com> wrote:On Sat, 21 Nov 2009 12:42:35 +0300, Rory McGuire <rjmcguire gmail.com> wrote::) I thought I had missed the point thats why I asked :D. On the Server side I used a similar method that the guy who wrote hessiand used for calling the objects methods, my code: module servers.ObjectSocketServer; //import serialize.ISerializer; import transports.ITransport; import transports.SocketTransport; import tango.core.Exception; version (Tango) { alias char[] string; import tango.net.Socket; import tango.io.Stdout; import tango.core.Traits; // alias ParameterTupleOf ParameterTypeTuple; } class ObjectSocketServer(T,C) { Socket request_conn; T serializer; ITransport transport; C instance; IMethod[string] methods; this(Socket request_conn) { instance = new C(); transport = new SocketTransport(request_conn); serializer = new T(transport); this.request_conn = request_conn; } ~this() { request_conn = null; serializer = null; transport = null; foreach (key, method; methods) { methods.remove(key); } instance = null; } void close() { try { if (request_conn !is null) { request_conn.shutdown(SocketShutdown.BOTH); request_conn.detach(); } } catch (Exception e) {} } /+ void expose(char[] method)() { pragma(msg, mixin(MethodType_mixin!(method)())); } +/ void expose(alias M)() { const string name = NameOfFunc!(M); //pragma(msg, "expose: "~ name); pragma(msg, "expose:\n"~ GenMethod!(C, name)); methods[name] = mixin(GenMethod!(C, name)); } void run() { scope(exit) close(); try { Stdout("run").newline; int i; while (true) { //Stdout("again").newline; auto method = serializer.read!(string)(); //Stdout("method = "~ method).newline; try { if (!(method in methods)) throw new Exception("Not Found"); methods[method](serializer, instance); } catch (Exception e) { e.writeOut((char[] str) { Stdout(str).newline;}); Stdout("Caught exception must serialize: ")(e.line)(":") (e.file)(" ")(e.toString).newline; } Stdout(i)("\r")(); i++; } Stdout().newline; } catch (Exception e) { Stdout("Caught exception: ")(e.toString).newline; } } interface IMethod { string getName(); void opCall(T, C); } } // this would be inside the class but cant be string GenMethod(alias C, string name)() { string ret = "new class IMethod {\n"; ret ~= "\tstring getName() { return \""~ name ~"\"; }\n"; ret ~= "\tvoid opCall(T serializer, C instance) {\n"; ret ~= "\t\tserializer.write(instance."~name~"(serializer.read! ("~ParameterTupleOf!(mixin(MethodType_mixin!(name)()))[0].stringof~")()));\n"; foreach (i,p; ParameterTupleOf!(mixin(MethodType_mixin!(name)()))[1..$]) { ret ~= "\t\tserializer.write(instance."~name~"(serializer.read! ("~p.stringof~")()));\n"; } ret ~= "\t\tserializer.getTransport().flush();\n"; ret ~= "\t}\n"; ret ~= "}\n"; return ret; } public template NameOfFunc(alias f) { version (LDC) { const char[] NameOfFunc = (&f).stringof[1 .. $]; } else { const char[] NameOfFunc = (&f).stringof[2 .. $]; } } string MethodType_mixin(string method)() { return "typeof(&C.init."~ method ~")"; }"Denis Koroskin" <2korden gmail.com> wrote:You missed the point. I only used asm to invoke function on remote side. Serialization is done purely with templates."Denis Koroskin" <2korden gmail.com> wrote: ...cut...Why did you use asm etc...? I made a de/serializer that just uses templates and static ifs. it doesn't do serialization of delegates etc... but I don't know that moving executable code across would be safe (I think java can do it, but then it has a vm). mine does: int, long, double, string, object, struct. I can post the code if you want. So far I have only used it to present the methods of an object as a kind of server, with a client being able to connect to it. Was inspired by hessiand on dsource.org. Can post code if you would like to see. I posted the automatic object wrapping code a couple of days ago as a simple example of template usage Thread: "Metaprogramming in D: Some real world examples". -Rory
Nov 22 2009