Skip to content
This repository has been archived by the owner on Jan 27, 2025. It is now read-only.

Create a custom javascript object

fholm edited this page Apr 20, 2011 · 2 revisions

First of all, here's the complete source code for this example: https://github.com/downloads/fholm/IronJS/IronJS-NativeObjectDemo.zip

This tutorial will guide you through the process of creating a custom native JavaScript object in IronJS, this is not the same as exposing a C# or F# class to IronJS. The object created here is a true JS object and will act as such inside user code.

We're going to create a simple image object that allows us to clear the image to white, draw an elipse and save it to disk.

I assume you know how to setup an empty C# console project in visual studio and add the IronJS dll you need, the binary packages can be downloaded from https://github.com/fholm/IronJS/downloads. You also need to reference System.Drawing if you're going to follow along step-by-step.

Creating the classes

The first thing we need to do is to just create an IronJS hosting context in the Main function:

using System;

namespace IronJSDemo
{
    class Program
    {
        static void Main(string[] args)
        {
            var ctx = new IronJS.Hosting.CSharp.Context();
        }
    }
}

After that we need to create two new classes, the first one will extend IronJS.CommonObject - call it ImageObject:

using System.Drawing;
using IronJS;
using CO = IronJS.CommonObject;
using Env = IronJS.Environment;
using FO = IronJS.FunctionObject;

namespace IronJSDemo
{
    public class ImageObject : CommonObject
    {
        Image image;
        Graphics graphics;
    }
}

Then create a static class called ImageConstructor:

using System;
using IronJS;
using IronJS.Hosting;
using IronJS.Native;
using CO = IronJS.CommonObject;
using FO = IronJS.FunctionObject;

namespace IronJSDemo
{
    public static class ImageConstructor
    {
    }
}

Setting up ImageObject

The first thing we're going to add is the constructor for our ImageObject:

namespace IronJSDemo
{
    public class ImageObject : CommonObject
    {
        Image image;
        Graphics graphics;

        public ImageObject(double width, double height, Env env, CommonObject prototype)
            : base(env, env.Maps.Base, prototype)
        {
            Put("width", (double)width, DescriptorAttrs.Immutable);
            Put("height", (double)height, DescriptorAttrs.Immutable);
            image = new Bitmap((int)width, (int)height);
            graphics = Graphics.FromImage(image);
            graphics.Clear(Color.Black);
        }
    }
}

Now it might be time to explain the parameters, the first two are pretty obvious - they're the width and height of our image. IronJS currently only deals with doubles so they have to be typed as such. The third parameter is the IronJS.Environment object which holds all the data required by the currently running context, the fourth parameter is the prototype object for our ImageObject.

env and prototype will just be passed to the base constructor, but we also pass env.Maps.Base - this is what keeps the hidden classes (an optimization tricks IronJS has borrowed from self/V8) running. Always pass the Base map in.

Now inside the constructor body, we want to expose the height and width to user code, so we set the two properties with Put which will allow JavaScript code to access .height and .width. We also pass in DescriptorAttrs.Immutable since it doesn't make sense to delete or change the width or height of an image.

After this we just create our bitmap image and it's associated graphics object and store it in our ImageObject instance.

Overloading the ClassName

However, we also want to overload the class name of our object, this isn't necessary - but we're doing it anyway to show how. This is the class name that Object.prototype.toString reads to print a string like [object Object].

namespace IronJSDemo
{
    public class ImageObject : CommonObject
    {
	/// ... snipped for brevity

        public override string ClassName
        {
            get
            {
                return "Image";
            }
        }
    }
}

ClearWhite

Now for something a bit more tricky, we're going to create our clearWhite method that allows us to clear our bitmap to white, here's the code:

namespace IronJSDemo
{
    public class ImageObject : CommonObject
    {

	/// ... snipped for brevity

        internal static void ClearWhite(FO func, CO that)
        {
            var self = that.CastTo<ImageObject>();
            self.graphics.Clear(Color.White);
        }
    }
}

There's two parameters to ClearWhite that needs explaining. func is the javascript function object representing the ClearWhite method itself inside the IronJS runtime (basically ClearWhite gets passed into itself when it's called). that is the object we're calling ClearWhite on and is the same as the C# this parameter.

Since IronJS inside user code doesn't care if the object it's accessing is the base CommonObject or a subclass the parameters will always be typed as CommonObject (or CO for short) we need to cast our CO instance to the type we need. The CommonObject instance supplies a function for this with the signature: T CastTo<T>(void), this method will throw the appropiate javascript TypeError that is catchable by user code if the type isn't correct. This is what we do on the first line of the method body.

Then we just access .graphics and call .Clear(Color.White) on it. Next is our DrawElipse function.

DrawElipse

namespace IronJSDemo
{
    public class ImageObject : CommonObject
    {

	/// ... snipped for brevity

        internal static void DrawElipse(FO func, CO that, double x, double y, double width, double height)
        {
            var self = that.CastTo<ImageObject>();
            var p = new Pen(Color.Black, 5);
            var rect = new Rectangle((int)x, (int)y, (int)width, (int)height);
            self.graphics.DrawEllipse(p, rect);
        }
    }
}

This does pretty much the same thing as ClearWhite except it takes four parameters (the coordinats in which to draw the elipse) - these must be typed as double if we need them to be numbers since IronJS, in the way the ECMA3 specification dictates, only deal with doubles.

Other then that we cast the CO instance to ImageObject once more and then use standard System.Drawing calls to draw an elipse. Time for SaveFile.

SaveFile

namespace IronJSDemo
{
    public class ImageObject : CommonObject
    {

	/// ... snipped for brevity

        internal static bool SaveFile(FO func, CO that, string name)
        {
            if (name == null)
                return func.Env.RaiseTypeError<bool>("Name was null");

            var self = that.CastTo<ImageObject>();
            self.image.Save(name);
            return true;
        }
    }
}

The only new thing going on here that we havn't seen before is the return func.Env.RaiseTypeError<bool>("Name was null"); call, this is a shorthand into the IronJS environment for raising user catchable exceptions.

Setting up ImageConstructor

Now, setting up the ImageConstructor is a bit trickier, this could be done without it's own class but I like to encapsulate the logic in a static class plus two methods so it's easy to re-use. First lets setup the method that will act as our JavaScript constructor and is what allows user code to do var img = new Image(200, 200);, this is not much different from the functions we defined in ImageObject:

namespace IronJSDemo
{
    public static class ImageConstructor
    {
	/// Snipped for brevity

        static CO Construct(FO ctor, CO _, double width, double height)
        {
            var prototype = ctor.GetT<CO>("prototype");
            return new ImageObject(width, height, ctor.Env, prototype);
        }
    }
}

The only thing going on here that is different from the previous functions is that we call T GetT<T>(string) to get the .prototype of our constructor function. We havn't created this yet, but will do in a bit. After that we just call our ImageObject constructor with the parameters passed, the environment we get of the ctor object and our prototype. Notice that the return type of Construct is CommonObject (or CO for short), this is very important.

Time to create the function that does the setup and attaches the constructor to our running context, allowing our user code to access it.

namespace IronJSDemo
{
    public static class ImageConstructor
    {

	/// Snipped for brevity


        public static void AttachToContext(CSharp.Context context)
        {
            var prototype = context.Environment.NewObject();
            var constructor = Utils.CreateConstructor<Func<FO, CO, double, double, CO>>(context.Environment, 2,Construct);
            var clearWhite = Utils.CreateFunction<Action<FO, CO>>(context.Environment, 0, ImageObject.ClearWhite);
            var saveFile = Utils.CreateFunction<Func<FO, CO, string, bool>>(context.Environment, 1, ImageObject.SaveFile);
            var drawElipse = Utils.CreateFunction<Action<FO, CO, double, double, double, double>>(context.Environment, 4, ImageObject.DrawElipse);

            prototype.Prototype = context.Environment.Prototypes.Object;
            prototype.Put("clearWhite", clearWhite, DescriptorAttrs.Immutable);
            prototype.Put("saveFile", saveFile, DescriptorAttrs.Immutable);
            prototype.Put("drawElipse", drawElipse, DescriptorAttrs.Immutable);
            constructor.Put("prototype", prototype, DescriptorAttrs.Immutable);
            context.SetGlobal("Image", constructor);
        }
    }
}

the type parameter has to be a delegate and needs to match the signature of the last parameter we pass in. The first line of the AttachToContext method creates a new ordinary javascript object (an instance of CommonObject from the .NETs point of view). This will be our .prototype property on our constructor and also the object set as the prototype of our ImageObjects.

Next we call the utility functions HostFunctionObject CreateConstructor<T> and HostFunctionObject CreateFunction<T>, The first parameter is the context environment, the second one is the .length propertys value and the last one is the .NET method that should be called when this function is invoked from JavaScript code.

We then need to setup our prototype object. First we attach the Object.prototype object to be the prototype of the Image.prototype object - this is done by assigning the .Prototype property. All default native object prototypes can be accessed through context.Environment.Prototypes.

Now it's time to add the functions to our Image.prototype so our image objects can access them, we call the Put function and also pass in DescriptorAttrs.Immutable so they can't be removed or changed.

Only two steps left, assign the prototype object to the constructor object so it can be accessed through Image.prototype and we can find it later inside our Construct function.

And, finally, expose the constructor as the global Image to our context by calling SetGlobal. We're now ready for attaching the object to our context and start using it from JavaScript.

Attaching the object to our context and using it

Update our Main function so it looks like this:

using System;

namespace IronJSDemo
{
    class Program
    {
        static void Main(string[] args)
        {
            var ctx = new IronJS.Hosting.CSharp.Context();
            ImageConstructor.AttachToContext(ctx);

            ctx.CreatePrintFunction();
            ctx.Execute(@"
                var img = new Image(100, 100); 
                print(img); // [object Image]
                img.clearWhite();
                img.drawElipse(5, 5, 20, 20);
                img.drawElipse(50, 50, 70, 70);
                img.saveFile('test.bmp');
            ");

            var img = ctx.GetGlobalAs<ImageObject>("img");
        }
    }
}

What the above code does should be clear after this tutorial :) Enjoy!