Om Module system

Published January 09, 2017
Advertisement

Thought I'd try blogging about this before I started implementing anything to try to get it straight in my head. I've decided to change how the Om module system works a little, based on playing around with writing a driver to allow me to execute Om scripts from within Windows explorer.

At the moment, state of play is that you can do this:

import print;print("hello world");The import statement tells the compiler that the symbol print is to be looked up in the modules list that the engine maintains in its shared state. It doesn't have to be populated until the first time that print is used - it just informs the compiler to emit the OpCode::Type::GetMd instruction when it finds the symbol.

Currently, the host application has to call Om::Engine::addModule(name, value) to put the relevant module value into place.

What I want to do differently is for the import statement itself to take care of this. I also want two different types of module to be avaialble.

I was playing around writing DLLs to support extending Om with native code. It turns out to be pretty easy (using the same toolchain at least) to write a DLL that can expose native code as an Om module. For example:

#include #include Om::Value print(Om::Engine &engine, const Om::Value &object, const Om::ValueList &params){ for(auto &p: params) std::cout << p.toString().c_str(); return Om::Value();}extern "C" __declspec(dllexport) void omExtend(Om::Engine *engine, Om::Value *result){ engine->addModule("print", engine->makeFunction(print)); *result = Om::Value();}Having built the Om library as a DLL as well, I just get QtCreator to build the above as a DLL, linking to the stub libOm.a. I can then, in a driver program, call LoadLibrary on the DLL, find the omExtend method and call it, passing in a pointer to the engine and a value and I then have the native print function available as a module in the engine.

Equally it is possible to write an Om script that returns a value, evaluate this with the engine and add that to the modules list to have it available to other scripts.

So what I'm thinking is that I want there to be two forms of the import statement.

import print;import "path/to/print.om";import "path/to/print.om.dll";In all cases, the symbol "print" should be given to the compiler as a module symbol to be checked for with OpCode::Type::GetMd when it is encountered in the source.

Assuming the file these are in is located in C:/Projects/script.txt, and the Om::Engine has a search paths variable set up as something like "C:/Om/extend;C:/MyOmModules", the first statement should look for the following in the following order:

C:/Projects/print.omC:/Projects/print.om.dllC:/Om/extend/print.omC:/Om/extend/print.om.dllC:/MyOmModules/print.omC:/MyOmModules/print.om.dllThe second import statement should look in

C:/Projects/path/to/print.omC:/Om/extend/print.omCL/MyOmModules/print.omand the third in

C:/Projects/path/to/symbol.om.dllC:/Om/extend/symbol.om.dllCL/MyOmModules/symbol.om.dllWhen the first one is found, it is evaluated in the relevant way and added to the module list.

But when should this happen? I think perhaps the first time the module is actually used as this allows us to create some circular relationships that will otherwise be impossible. Obviously this means there could be some unexpectedly long operations occurring in unexpected places, but I don't think it would be a problem.

So in both cases, the compiler is going to have to store the argument to import in the text cache and embed the id in with the OpCode::Type::GetMd instruction, along with the id for just the symbol. So the psuedocode for the GetMd will be something like:

bool Machine::md(uint path, uint symbol, Om::Result &result){ auto m = state.modules.find(symbol); if(m == state.modules.end()) { m = loadModule(state, path); if(m == state.modules.end()) { result = Om::ValueProxy::makeError(state, stringFormat("module not found - ", state.tc.text(id)), mapToLine()); return false; } } vs.push_back(m->value); inc(state, vs.back()); return true;}Where loadModule looks something like:

pod_map::iterator loadModule(State &state, uint pathId){ pod_string path = state.tc.text(pathId); auto s = doSearch(path); Om::Value v; if(s.type == om) { v = state.engine.evaluate(s.path, Om::Engine::EvaluateType::File); if(v.type() == Om::Type::Error) return state.modules.end(); } else if(s.type == dll) { HMODULE h = LoadLibrary(s.path); FARPROC p = GetProcAddress(h, "omExtend"); // check for errors, return state.modules.end() typedef void(*Func)(Om::Engine*,Om::Value*); reinterpret_cast(p)(&state.engine, &v); } inc(state, v); return state.modules.insert(v);}or whatever.

This should then mean that, like with C and C++, you can reference both a standard installation directory (maybe stored in an OM environment variable or set by options to the driver program, as well as using a local directory structure for a current project.

Can't quite decide if this is the right approach or not so need to ponder a little more. Playing a bit of Skyrim at the moment so that is helping me unwind my mind.

Previous Entry Om - postponing the worry
Next Entry Framework Fun
3 likes 2 comments

Comments

dmatter

Importing on first-use is fairly common (both Java and .Net do it that way). It also means that if you import a module and you don't actually use it anywhere then it won't cost you anything, which is nice, it means you can have a bunch of default or common imports for convenience and you either make use them or you don't with no penalty for unused imports.

January 10, 2017 11:46 AM
Aardvajk
Hey dmatter. Thanks, appreciate the comment. Helps convince me I'm on the right track.
January 10, 2017 12:14 PM
You must log in to join the conversation.
Don't have a GameDev.net account? Sign up!
Profile
Author
Advertisement
Advertisement