2008/11/26 Eliot Miranda eliot.miranda@gmail.com:
Hi Igor, one question Andreas raised that I hadn't thought of was how with the new scheme you deal with multiple interpreters? With the old interpreterProxy scheme I guess the interpreterProxy could redirect a function to the relevant interpreter (although this would require a mutex to make it thread safe). How do you do it in Hydra with the new scheme? Do you load the plugin multiple times, once per VM, or do you do some redrecting behind the scenes?
To maintain backward compatibility in Hydra VM we need to support two kinds of plugins: old and new ones.
We have a limitation , that old plugins could work only with single interpreter - main one. Any attempt to use primitives of old plugin in non-main interpreter will lead to primitive failure. Since we can't predict what plugin does, or what internal state its using for work - its really dangerous to use its primitives in a concurrent world. So, we saying big NO to use old plugins for non-main interpreter. In this way, we guarantee that Hydra VM works with old plugins identically as Squeak VM, but only for the main interpreter.
Of course, things are different for new plugins: they are aware that their primitives could run concurrently in multiple threads.
In concurrent world we have to make difference between global state and thread-local(per-interpreter) state. To access global state, there is nothing new - we should use synchronization objects (mutex/semaphore). But most plugins which i converted to Hydra don't using a mutable global state, or initialize it once (at plugin loading stage) and using it in read-only manner. So, the major thing is how to deal with per-interpreter state of plugins. My solution is simple: i added helper functions into VM which allows users to 'attach' a state buffer to all interpreter instances.
By calling #attachStateBuffer: numBytes initializeFn: fn1 finalizeFn: fn2 , plugin telling VM, that for each interpreter instance it needs a numBytes long memory buffer to hold its thread-local state. In result , it receives a unique state id, which it should use later to access an attached state buffer of particular interpreter instance.
So each time it needs to access own thread-local state, it simply calls #getAttachedStateBuffer: aStateHandle of: interpreter
And , of course plugin can provide two pointers of functions which initializing the state buffer or finalize it. These functions is called each time you creating new instance of interpreter or destroying it.
In Hydra-s VMMaker, i made modifications which seamlessly (for plugin writer) deals with per-interpreter state. As well as in old generation scheme - all you need is to declare some ivars in plugin's class. But code generator , instead of placing them at module scope level , wrapping all such variables with struct.
I think it not requires too much explanation, for you Eliot, but for other readers:
Let me show you the difference between old and new code generation on an example of B2DPlugin.
The old generator simply puts all variables into C file:
/*** Variables ***/ static int* aetBuffer; static char bbPluginName[256] = "BitBltPlugin"; static void * copyBitsFn; static sqInt dispatchReturnValue; static sqInt dispatchedValue; static int doProfileStats = 0; static sqInt engine; static sqInt engineStopped; static sqInt formArray; static sqInt geProfileTime; static int* getBuffer;
#ifdef SQUEAK_BUILTIN_PLUGIN extern #endif struct VirtualMachine* interpreterProxy; static void * loadBBFn; static const char *moduleName = #ifdef SQUEAK_BUILTIN_PLUGIN "B2DPlugin 11 November 2008 (i)" #else "B2DPlugin 11 November 2008 (e)" #endif ; static int* objBuffer; static sqInt objUsed; static unsigned int* spanBuffer; static int* workBuffer;
The new one is a bit more clever: all global variables still going to C file, but thread-local going to special struct, declared in <plugin-name>_imports.h :
/* Per-image plugin state */ typedef struct PluginState { int* workBuffer; int* objBuffer; int* getBuffer; int* aetBuffer; unsigned int* spanBuffer; sqInt engine; sqInt formArray; sqInt engineStopped; sqInt geProfileTime; sqInt dispatchedValue; sqInt dispatchReturnValue; sqInt objUsed; #ifdef PLUGIN_STATE_EXTRAS PLUGIN_STATE_EXTRAS #endif } PluginState;
And it defines two helper macros:
#define DECLARE_PLUGIN_STATE() struct PluginState * pstate = vmFunction(getAttachedStateBufferof)(pluginStateId,currentInterpreter()) #define plugin_state(name) pstate->name
Now lets take a look at method code, generated by VMMaker:
/* old */ static sqInt addEdgeToGET(sqInt edge) { if (!(allocateGETEntry(1))) { return 0; } getBuffer[workBuffer[GWGETUsed]] = edge; workBuffer[GWGETUsed] = ((workBuffer[GWGETUsed]) + 1); }
/* new */ static sqInt addEdgeToGET _iargs(sqInt edge) { DECLARE_PLUGIN_STATE(); if (!(allocateGETEntry _iparams(1))) { return 0; } plugin_state(getBuffer)[plugin_state(workBuffer)[GWGETUsed]] = edge; plugin_state(workBuffer)[GWGETUsed] = ((plugin_state(workBuffer)[GWGETUsed]) + 1); }
here an _iargs() macro expands to #define _iargs(args...) (PInterpreter intr, args) and currentInterpreter() macro expands to 'intr'
So, in expanded form code could look like:
static sqInt addEdgeToGET (PInterpreter intr, sqInt edge) { PluginState * pstate = getAttachedStateBufferof(pluginStateId, intr);
pstate->foo = blabla }
Few words about vmFunction(name)
#ifdef SQUEAK_BUILTIN_PLUGIN
#include "interp_prototypes.h" #define vmFunction(name) name
#else
#define vmFunction(name) vmFunctions.name
so, as you see , this macro expands to 'name' in internal plugins and YES, it calls vm functions directly w/o using interpreterProxy.
As for external plugin its expands to an a vmFunctions.name , which is a struct in a form:
typedef struct VMFunctions { char * sel1; sqInt (*attachStateBufferinitializeFnfinalizeFn) (sqInt numberOfBytes, AttachedStateFn stateInitializeFn, AttachedStateFn stateFinalizeFn); char * sel2; sqInt (*booleanValueOf) _iargs(sqInt obj); char * sel3; sqInt (*byteSizeOf) (sqInt oop); char * sel4; sqInt (*classBitmap) _iarg(); char * sel5; sqInt (*classPoint) _iarg(); char * sel6; sqInt (*failed) _iarg(); ... void * _dummy; void * _dummyfn; }
In C file we got a variable vmFunctions
struct VMFunctions vmFunctions = { "attachStateBuffer:initializeFn:finalizeFn:", NULL, "booleanValueOf:", NULL, "byteSizeOf:", NULL, "classBitmap", NULL, "classPoint", NULL, "failed", NULL, "fetchClassOf:", NULL, "fetchInteger:ofObject:", NULL, "fetchPointer:ofObject:", NULL, "firstIndexableField:", NULL, ..... NULL, NULL };
And in, setInterpreter() - a first function which called by VM, this structure is filled with appropriate pointers:
EXPORT(sqInt) setInterpreter (InterpreterProxy * anInterpreter) { sqInt i; char * ptr; char** table; sqInt ok;
ok = anInterpreter->majorVersion() == OBJVM_PROXY_MAJOR; if (ok == 0) { return 0; } ok = anInterpreter->minorVersion() >= OBJVM_PROXY_MINOR; if (ok == 0) { return 0; } i = 0; table = (char**) &vmFunctions; while ((table[i]) != null) { ptr = (char*)anInterpreter->getVMFunctionPointerBySelector(table[i]); if (ptr == null) { dprintf(("Plugin unable to find vm function: %s\n", table[i])); return 0; } table[i + 1] = ptr; i += 2; } ; ok = INIT_PLUGIN_STATE();
return ok; }
as you can see, plugin fails to initialize (returns 0) if any of functions its using is null.
There is more things under the hood concerning where i got function prototypes , and how VM generates a list of public functions.. but i think its enough for this post :)