Sign in to follow this  
evolutional

Unity [.net] .NET Scripting - Sharing Types/Objects

Recommended Posts

Let me try and get this into some understandable terms as my research is confusing me - it doesn't help being a relative newcomer in the .NET world either [grin] I've been trying to implement some scripting in a VB.NET application using the inbuilt .NET compiler. I've followed the stuff in this thread and on CodeProject and got a mini script compiler up and running ok. The problem I'm having now is that I want to expose some (not all) functionality from my host application to the scripting environment. Mainly, I have several custom data field objects that I'd like the scripts to be able to read. So my question is, how do I allow my scripting environment access to certain types from the host assembly and expose some objects to the script? It has something to do with Reflection, which is obvious. However, all my searching on the subject is coming up blank. Does anyone have any examples of what I'm trying to achieve? Thanks

Share this post


Link to post
Share on other sites
Off the top of my head, have a separate assembly that contains all of the types you want the script to have access to and compile the script with a reference to that assembly. I haven't done much in the way of runtime compilation with .NET so I'm not sure if there would be any security risks associated with that (assuming that the types in your assembly are all considered 'safe').

Share this post


Link to post
Share on other sites
Ok, I've worked out how to get an assembly to reference another and the associated types, data et cetera. However, the problem is that the script has access to all of the types from the host, something that isn't a huge problem, but I'd rather not have it there. So the solution looks to me now as if I need to do as joanus suggested and have a 'proxy' assembly inbetween the host and the script assembly. I'll look into doing this for now, but would be grateful of any further insights you nice people can offer.

Thanks

EDIT:

A couple of useful resources I've found in searching this:

LiveCode.NET

AppDomains and Dynamic Loading

[Edited by - evolutional on September 21, 2004 5:27:19 AM]

Share this post


Link to post
Share on other sites
Forgive me if this is a waste of your time.

Recently, I've been working on a project to allow dynamically loaded "plugins" (read: assemblies) to be loaded for a business application.

The approach that was taken was to define a set of interfaces that specified the definition of: a) the framework that loaded the assemblied and b) the assemblies themselves.

Being a business environment, and given that the client wants something that works yesterday, what we did was make the framework implement an interface, and passed a reference of the framework to the plugin. The interface defined functions that gave the plugin access to properties of the framework.

Since this is a game development site, this may be totally inappropriate for your problem, but for us it worked a treat.

Let me know if you want further details.

Share this post


Link to post
Share on other sites
I'd love further details if you're willing to supply them, as I'm essentially learning scripting/plugins and how they work in .NET. The context of a particular problem domain doesn't really apply in this instance, so I'd like to see what you came up with.

I've also been reading Writing Plugin-based .NET applications which is useful. As I say, I'm a .NET n00b but looking to explore it a lot more in the future.

Share this post


Link to post
Share on other sites
Okay.

Our particular application is designed to be a framework from which we can load additional modules that are written post-deployment, and don't require us to constantly have to re-compile the framework application.

Interface-wise, the framework is a shell that loads tab pages (our plugins are written as user controls and are tab pages), and each tab page will process different input files.

Additionally, each assembly, while containing logic to load and process data files, also needed to manage a collection of settings.

In order to do this, we have three interfaces defined:


namespace TheApp
{
public interface Plugin
{
System.Windows.Forms.TabPage TabPage
{
get;
}
bool Init(TheApp.Framework f, string tabText, string configDir, string pluginName);
}


public interface Framework
{
System.Data.SqlClient.SqlConnection SqlCnn
{
get;
}

Control.MessageBar MessageBar
{
get;
}
}

public interface OptionInterface
{
bool Init(TheApp.Framework f, string configDir);
}
}


So these interfaces define the functionality of the framework, the plugin and the settings component of each interface.

The idea was to define an interface for the framework so that, if you look at the Init() function of the Plugin interface, we pass the framework in as a parameter, and then the Framework interface defines how and what we get access to (in our case, the SqlConnection and a MessageBar).

In order to load the plugin into the framework, all we do is (from the framework):

using System.Reflection;
using System.Runtime.Remoting;
using System.EnterpriseServices;
/*
....
*/
ObjectHandle h;
h = Activator.CreateInstanceFrom(p.m_filename, p.m_namespace + "." + p.m_class);
TheApp.Plugin i = (TheApp.Plugin)h.Unwrap();

i.Init(this, p.m_title, m_configPath, p.m_class);
tcMain.TabPages.Add(i.TabPage);


Other things that we do that aren't shown here including checking each loaded assembly to ensure that it uses the correct interface, retrieve information about the assembly, etc.

Since I'm coding this as a contractor, I need to be careful with what I post. If you would like to discuss this stuff further, feel free to send me an email.

Cheers,
Brendan.

Share this post


Link to post
Share on other sites
Evo, I haven't done much work using a .NET language as a scripting language, but perhaps it helps if you think about the problem in the reverse way. Perhaps the issue isn't that your scripts need to already be aware of your game entities. Perhaps, your game entities need to make themselves aware TO the scripts. So, once you create the object out of the script, then you call a Register() function on the object from the script that accepts a Game object as a parameter. And then anytime the script needs to call any functionality in the game, it calls a method or member of the Game object it was Register'd with. So, in pseudo-code, it might look like this


// Game myGame = new Game();
// GameObject zombie = new GameObjectFactory.CreateZombie();

Script myScript = ScriptManager.LoadFromFile("myScript.gsc");
myScript.Register(myGame, zombie);

....

myScript.Execute()




And then in the script itself you would have something like:


class MyScript : Script
{
private Game game = null;
private GameObject gameObject = null;

public MyScript()
{
// non-game specific load here
}

public void Register(Game game, GameObject gameObject)
{
this.game = game;
this.gameObject = gameObject;

// various other game-specific load here
}

public void Execute()
{
// use Game object to get data from or execute methods

// stuff like this
// Position myPosition = gameObject.Position;
// int score = this.game.Score;
// gameObject.Move(someX, someY);
}
}




This is just a thought though. It might work when approaching it from this different angle. If not, hopefully it might give you a different idea how to solve your problem. I hope this helps in some way! Good luck!

Share this post


Link to post
Share on other sites
Also, if you are loading up your scripts into an app domain, load them up into a different specialized app domain. First of all, if a script crashes for some reason, it won't bring down your entire app domain (and hence, application) with it. Second of all, for security reasons. Although others may not be writing their own scripts for your game to "mod" it, it is always a good idea to get in the habit of writing safe code. When you create the app domain to load your scripts into, make sure to give the app domain restricted priviledges. The last thing you want is some script kiddy to be able to write his own script, load it into your game, and have full access to the machine it is running on (and hence, access to every namespace in the bcl possibly).

Think of the new app domain as a black box that you can put the script objects in. For security reasons, the scripts themselves can't get out of the box and if they do break anything, it only breaks the box, not your application.

Just a thought though. Any time you are "loading" objects at runtime, it is a very good habit to get into loading them into a seperate app domain. Your application will be happy, and all the users of your application will be happy :).

Share this post


Link to post
Share on other sites
Thanks for your comments guys. I'm torn as to which apprach to adopt now, the script or the plugin method. The plugin method does actually seem a lot cleaner than the scripting, mainly because being a .NET n00b the whole application is a greater challenge than it would normally be as I'm learning the .NET framework as I'm going on.

Thanks for the idea Blowfish, however the problem seems to be registering my application types with the script interface. I've never done anything like this in .NET so it's all new territory. I'll keep playing, perhaps go with the plugin solution and take up the scripting idea later on when I feel more accustomed to .NET.

Oh and Blowfish - kudos for using a Zombie in your example [grin]

Share this post


Link to post
Share on other sites
This is how you can do it in C# so maybe it gives some glue how to do it in VB.

First create class libary (dll) wich containts all the functions and classes/types you want to expose for scripting.

Then

CompilerParameters pars = new CompilerParameters();

pars.ReferencedAssemblies.Add( "ClassLibTest.dll" );



I hope it helps any.

Share this post


Link to post
Share on other sites
Evolution, if you do come back to the scripting engine later, you don't necessarily need to use .NET. If you wish, you can use Lua.net. It is an integration of Lua and CLI. Using this method, you can do "function binding" where you can basically tell Lua that when my script uses this given function, then actually call this delegate (or something like that). It also allows you to import and use the .NET base class libraries in your script. So, you get the benefit of being able to use the CLI, as well as have control over the interface between your engine and script down to the function level.

The one drawback to a plug-in architecture when implementing "scripts", is that they have to be compiled before hand. With scripting, it is really powerful to be able to change the script and re-load it on the fly and see the different behavior right away. This isn't very feasible in the plug-in architecture (unless you compile the script in the engine, but then you are actually using a scripting-based approach).

Like GCoda pointed out, it can be easy to add "referenced" assemblies when you go to compile your script. If you don't want to have all your interfaces in a seperate assembly (which I like to do anyways (think of the seperation between "engine" and "application")) then I'm pretty sure that you can add a reference to the currently executing assembly, like this:


CompilerParameters pars = new CompilerParameters();
pars.ReferencedAssemblies.Add(Assembly.GetExecutingAssembly().FullName);



Just some thoughts. Good luck! One thing is for sure, you are going to learn some FUN things in the time to come :D.

Share this post


Link to post
Share on other sites
I have been referencing assemblies like the example. I guess I need to do as joanusdmentia originally stated and use a proxy assembly to share just the types I need.

Hmmm, LUA.Net... There's managed wrapper for everything these days.

Thanks for your help people, mucho appreciated.

Share this post


Link to post
Share on other sites
Ok, I haven't read the whole thread, but I did read that you don't know how to expose some functionality to the script/plugin.

Checkout my Plugin Managet @ my site.

You can find the source there.
The thing is very simple.
I have a interface which defines methods which will be exposed to the script/plugin and a host aplication that implements that interface. The plugin manager then passes the refrence to the class that implements that interface to the script/plugin.

Share this post


Link to post
Share on other sites
I've been playing with .NET scripting recently and got a nice system up and running. However I decided to use JScript.NET which is Microsoft's version of ECMAScript (or javascript as its called). It has support for classes, inheritance, exceptions... pretty much everything I was looking for.

Basically you create an instance of a VSAEngine and add bits of code to it. This can either be assemblies or bits of JScript (and in theiry bits of VB as well). I have it so it just loads my engine dlls so it doesn't have access to System.IO and other potentially damaging namespaces.

I've managed to create instances of classes i've created in C# from javascript and vice versa (tho that involved me creating abstract classes to cast the instances to).

I've been pretty impressed with the system so far and can't think of anything I will need that it can't do.

Here's my ScriptEngine class to give you some pointers...


using System;
using System.IO;
using System.Reflection;
using Microsoft.JScript.Vsa;
using Microsoft.Vsa;

namespace MrsKensington.Engine.Scripting
{
public class ScriptEngine
{
private static ScriptEngine instance = null;

private IVsaEngine engine;

private const string moniker = "MrsKensington.Engine.Scripting://script";
private const string rootNamespace = "MrsKensington.Engine.Scripting";

/// <summary>
/// constructor made private to avoid external instantiation
/// </summary>
private ScriptEngine()
{
Initialize();
}

/// <summary>
/// sole access point for the script engine
/// </summary>
/// <returns>the only instance of the script engine</returns>
public static ScriptEngine Get()
{
if (instance == null)
instance = new ScriptEngine();

return instance;
}

/// <summary>
/// adds a block of code to the engine, compiles it and restarts the script engine
/// </summary>
/// <param name="name">the name of the block to add</param>
/// <param name="block">the source block itself</param>
public void AddCodeBlock(string name, string block)
{
//stop it if its running
if (engine.IsRunning)
engine.Reset();

//get the code item from the engine
IVsaCodeItem item = (IVsaCodeItem)engine.Items.CreateItem(name, VsaItemType.Code, VsaItemFlag.Module);
item.SourceText = block;

engine.Compile();
VsaSite site = (VsaSite)engine.Site;
if(site.error != null)
throw new InvalidSytaxException(site.error);
engine.Run();
}

/// <summary>
/// Loads a script file into the engine with the name as the
/// filename without extension or path
/// </summary>
/// <param name="filename">the path to the file to load</param>
public void LoadScriptFile(FileInfo file)
{
String contents = "";

//load the file
using (StreamReader sr = new StreamReader(file.OpenRead()))
{
char[] buf = new char[1024];

while (sr.ReadBlock(buf, 0, buf.Length) > 0)
{
contents += new String(buf);
}
}

//remove the .js from the end
String name = file.Name.Substring(0, file.Name.LastIndexOf("."));

AddCodeBlock(name, contents);
}

/// <summary>
/// Loads all of the scripts from a directory
/// </summary>
/// <param name="directory">the directory to load the scripts from</param>
public void LoadScriptsFromDirectory(String directory, String extension)
{
DirectoryInfo dir = new DirectoryInfo(directory);

FileInfo[] files = dir.GetFiles("*." + extension);

foreach(FileInfo file in files)
{
LoadScriptFile(file);
}
}

/// <summary>
/// removes a code block from the engine
/// </summary>
/// <param name="name">the name of the code block to remove</param>
public void RemoveCodeBlock(string name)
{
//stop it if its running
if (engine.IsRunning)
engine.Reset();

engine.Items.Remove(name);

engine.Compile();
engine.Run();
}


/// <summary>
/// creates an instance of a class type in the script engine
/// </summary>
/// <param name="className">the name of the class to instantiate</param>
/// <returns>an object of the requested class</returns>
public object CreateInstance(string className)
{
if (!engine.IsCompiled)
{
engine.Compile();
engine.Run();
}
return engine.Assembly.CreateInstance(rootNamespace + "." + className);
}

/// <summary>
/// initializes the engine
/// </summary>
private void Initialize()
{
//standard init stuff
engine = new VsaEngine(true);
engine.RootMoniker = moniker;
engine.Site = new VsaSite(this);
engine.InitNew();
engine.RootNamespace = rootNamespace;
engine.Name = rootNamespace;

//set some options
engine.SetOption("alwaysGenerateIL", true);
engine.SetOption("autoRef", true);
engine.SetOption("print", true);

//give the script engine access to the correct assemblies
//String assemblyName = System.Reflection.Assembly.GetExecutingAssembly().GetModules()[0].FullyQualifiedName;
//engine.Items.CreateItem(assemblyName, VsaItemType.Reference, VsaItemFlag.None);

//give the script engine access to the same assemblies as this
Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies();
foreach( Assembly asm in assemblies )
{
if(asm.Location.IndexOf("MrsKensington.Engine") != -1)
engine.Items.CreateItem( asm.Location, VsaItemType.Reference, VsaItemFlag.None );
}
}

/// <summary>
/// class to represent the vsa site used
/// </summary>
private class VsaSite : IVsaSite
{
private ScriptEngine engine;
public IVsaError error;

public VsaSite(ScriptEngine engine)
{
this.error = null;
this.engine = engine;
}

public object GetEventSourceInstance(string itemName, string eventSourceName)
{
return null;
}

public object GetGlobalInstance(string name)
{
return null;
}

public void Notify(string notify, object info)
{

}

public bool OnCompilerError(IVsaError error)
{
this.error = error;
return true;
}

public void GetCompiledState(out byte[] pe, out byte[] debugInfo)
{
pe = null;
debugInfo = null;
}
}
}
}







this is how you use it...

ScriptEngine scriptingEngine = ScriptEngine.Get();
scriptingEngine.LoadScriptsFromDirectory("scripts", "js");
object testObject = scriptingEngine.CreateInstance("MyClass");


Here's some useful references I used, they are more geared towards using JScript.NET with ASP.NET but they are still useful...
JScript.NET Langauge Reference
JScript.NET Reference

Share this post


Link to post
Share on other sites
Hmmmm, nice. VSA looks more like what I was looking for. Would I be able to selectively allow access to classes into the VSA environment? For example, I don't want the script to be able to access my core classes, I want to be able to create a proxy class and throw that into the script.

EDIT: This is a nice link which has a good starting point I think.

Cookies++

Share this post


Link to post
Share on other sites
the bit where i do the...

//give the script engine access to the same assemblies as this
Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies();
foreach( Assembly asm in assemblies )
{
if(asm.Location.IndexOf("MrsKensington.Engine") != -1)
engine.Items.CreateItem( asm.Location, VsaItemType.Reference, VsaItemFlag.None );
}


specifies assemblies that the scripts are allowed to access. I only allow access to assemblies that are part of my engine. You could either just create a scripting assembly.

Either that or according to this MSDN page You can pass in VsaItemFlag.Class to CreateItem to specify that the code item is the name of a class...

Hope that helps!

Share this post


Link to post
Share on other sites
Man, I'm coming up with so many useful links on this subject now. Here's another I feel I should share Using .NET to make your application Scriptable. I'm going the plugin route for version 1 of this internal application. It's good, but far from ideal. I'm likely to add scripting to each plugin, I think.

Mrs Kensington, the push towards VSA looks really promising. Thanks for punting me in that direction. The more I use .NET the more I like it [grin]

Share this post


Link to post
Share on other sites
The current issue of Dr. Dobbs has two articles on the subject, one on using AppDomains and the other on reflection and AppDomains for writing pluggable interfaces.

Share this post


Link to post
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

Sign in to follow this