This project has moved and is read-only. For the latest updates, please go here.

[Solved] [Patch enclosed] Proper prototype chains for C#-created objects

Apr 16, 2012 at 3:10 PM
Edited Apr 16, 2012 at 3:13 PM

This is going to get long and possibly boring, so please bear with me. It will probably be worth the effort, if you ever got stuck trying to create a usable Javascript pseudo-class hierarchy in C#.

Say we have two C# classes, defined as follows:

using System;
using Jurassic;
using Jurassic.Library;

namespace Test
{
    class PersonConstructor : ClrFunction
    {
        public PersonConstructor(ScriptEngine engine)
            : base(engine.Function.InstancePrototype, "Person", new Person(engine.Object.InstancePrototype, null))
        {
        }

        [JSConstructorFunction]
        public Person Construct(string name)
        {
            return new Person(InstancePrototype, name);
        }
    }

    class Person : ObjectInstance
    {
        private static int _created = 0;
        private string _name;

        public Person(ObjectInstance prototype, string name)
            : base(prototype)
        {
            ++_created;
            PopulateFunctions();
            _name = name;
        }

        [JSProperty(Name = "created")]
        public static int Created
        {
            get
            {
                return _created;
            }
        }

        [JSProperty(Name = "name")]
        public string Name
        {
            get
            {
                return _name;
            }
        }

        [JSFunction(Name = "sayHello")]
        public void SayHello()
        {
            Console.WriteLine("Hello, " + _name);
        }
    }

    class EmployeeConstructor : ClrFunction
    {
        public EmployeeConstructor(PersonConstructor parent)
            : base(parent.Engine.Function.InstancePrototype, "Employee", new Employee(parent.InstancePrototype, null, 0.0))
        {
        }

        [JSConstructorFunction]
        public Employee Construct(string name, double monthlyWage)
        {
            return new Employee(InstancePrototype, name, monthlyWage);
        }
    }

    class Employee : Person
    {
        private double _monthlyWage;

        public Employee(ObjectInstance prototype, string name, double monthlyWage)
            : base(prototype, name)
        {
            _monthlyWage = monthlyWage;
        }

        [JSProperty(Name = "monthlyWage")]
        public double MonthlyWage
        {
            get
            {
                return _monthlyWage;
            }
        }

        [JSFunction(Name = "showIncome")]
        public void ShowIncome()
        {
            Console.WriteLine(Name + " gets " + (12 * _monthlyWage).ToString("#0.00") + "$ per year.");
        }
    }
}

Let's write a small console application to test the code above:

using System;
using Jurassic;
using Jurassic.Library;

namespace Test
{
    class Program
    {
        static void TestCS(ScriptEngine engine)
        {
            var bill = engine.GetGlobalValue<PersonConstructor>("Person").Construct("Bill");
            var joe = engine.GetGlobalValue<EmployeeConstructor>("Employee").Construct("Joe", 50);

            Console.WriteLine("{0} Person instances were created so far.", Person.Created);

            bill.SayHello();
            joe.SayHello();
            joe.ShowIncome();
        }

        static void Main()
        {
            var engine = new ScriptEngine();
            var global = engine.Global;
            global.DefineProperty("console", new PropertyDescriptor(new FirebugConsole(engine), PropertyAttributes.Sealed), true);

            var personConstructor = new PersonConstructor(engine);
            var employeeConstructor = new EmployeeConstructor(personConstructor);
            global.DefineProperty("Person", new PropertyDescriptor(personConstructor, PropertyAttributes.Sealed), true);
            global.DefineProperty("Employee", new PropertyDescriptor(employeeConstructor, PropertyAttributes.Sealed), true);

            Console.WriteLine(">>> Testing CSharp code");
            TestCS(engine);

            Console.WriteLine();
            Console.WriteLine(">>> Testing Javascript code");
            engine.Execute(@"
// Enclose all code in an anonymous function, to avoid
// global namespace pollution
(function () {

    var bill = new Person(""Bill"");
    var joe = new Employee(""Joe"", 50);

    console.log(Person.created + "" Person instances were created so far."");

    bill.sayHello();
    joe.sayHello();
    joe.showIncome();

})();
            ");

            Console.WriteLine("Press any key to terminate the program...");
            while (Console.KeyAvailable)
                Console.ReadKey(true);
            Console.ReadKey(true);
        }
    }
}

As you can see, we are using our two C# classes (Person and Employee) both from C# and from Javascript. Let's run the code and see what happens:

>>> Testing CSharp code
4 Person instances were created so far.
Hello, Bill
Hello, Joe
Joe gets 600,00$ per year.

>>> Testing Javascript code
 undefined Person instances were created so far.
Hello, Bill
Hello, Joe
Joe earns 600.00$ per year.
Press any key to terminate the program...

The first thing that puzzled me for some seconds here is the 4 Person instances: why, I had only created 2! Aw, well, not exactly: I had to use two instances (one Person and one Employee) as prototypes. Not that this helped much, on second thought: every instance of Person calls PopulateFunctions in the constructor and thus gets a copy of all its prototype's properties. From a Javascript point of view, this is a bad thing, to say the least. We're going back on this later; for now, let's see what else went wrong.

Oops! "undefined" Person instances?!? Of course, silly me: "created" is not a property of Person, it's a property of every Person instance! Again, from the point of view of one who wants to create a decent pseudo-class hierarchy, this is a big no-no.

And there's more. By taking a close look at the code, you'll see that it's quite complicated, and it's going to get even worse for a more complex hierarchy of C# classes / Javascript pseudo-classes. All this for not even having proper prototype chains (remember that all prototype properties get copied into every single object); furthermore, what if creating actual objects (which we need to use as prototypes) had even worse side effects, like opening sockets, creating files, and so on?

To recap, what I'd like is a way to:

  • have proper prototype chains, with properties attached to prototypes instead of being attached to instances;
  • have the pseudo-class hierarchy automatically reflect the actual class hierarchy (that is, without the need to reference a PersonConstructor in EmployeeConstructor's constructor);
  • not need two separate C# classes (one for instances and one for constructors) for each Javascript pseudo-class;
  • create objects from C# in a more natural way, like this:
    var bill = new Person(engine); // can't get rid of the ScriptEngine reference, obviously

Whew! I know, that's a lot of a pretense. The good news is that I made it. The bad news is that it required a small patch to Jurassic, because of a couple of features I needed to add to the ObjectInstance class.

By following this link you can download a complete Visual Studio 2010 solution with a patched Jurassic (for testing purposes ONLY: it has the above-mentioned patch, plus Rob Paveza's dynamic support), the code you can add to your solution (in the JurassicExtensions folder), instructions on how to patch ObjectInstance.cs, the revised version of the Person and Employee classes and the testing program.

Let's see the new classes, so you may get a taste of what's cooking:

using System;
using Jurassic;
using Jurassic.Library;
using JurassicExtensions;

namespace JavascriptObjectTest
{
    class Person : JavascriptObject
    {
        private static int _created = 0;
        private string _name;

        public Person(ScriptEngine engine, string name)
            : base(engine)
        {
            ++_created;
            _name = name;
        }

        [JSProperty(Name = "created")]
        public static int Created
        {
            get
            {
                return _created;
            }
        }

        [JSProperty(Name = "name")]
        public string Name
        {
            get
            {
                return _name;
            }
        }

        [JSFunction(Name = "sayHello")]
        public void SayHello()
        {
            Console.WriteLine("Hello, " + _name);
        }
    }

    class Employee : Person
    {
        private double _monthlyWage;

        public Employee(ScriptEngine engine, string name, double monthlyWage)
            : base(engine, name)
        {
            _monthlyWage = monthlyWage;
        }

        [JSProperty(Name = "monthlyWage")]
        public double MonthlyWage
        {
            get
            {
                return _monthlyWage;
            }
        }

        [JSFunction(Name = "showIncome")]
        public void ShowIncome()
        {
            Console.WriteLine(Name + " gets " + (12 * _monthlyWage).ToString("#0.00") + "$ per year.");
        }
    }
}

See? Boilerplate code has practically disappeared. The test application code is simpler, too:

using System;
using Jurassic;
using Jurassic.Library;
using JurassicExtensions;

namespace JavascriptObjectTest
{
    class Program
    {
        static void TestCS(ScriptEngine engine)
        {
            var bill = new Person(engine, "Bill");
            var joe = new Employee(engine, "Joe", 50);

            Console.WriteLine("{0} Person instances were created so far.", Person.Created);

            bill.SayHello();
            joe.SayHello();
            joe.ShowIncome();
        }

        static void Main()
        {
            var engine = new ScriptEngine();
            var global = engine.Global;
            global.DefineProperty("console", new PropertyDescriptor(new FirebugConsole(engine), PropertyAttributes.Sealed), true);

            Console.WriteLine(">>> Testing C# code");
            TestCS(engine);

            Console.WriteLine();
            Console.WriteLine(">>> Testing Javascript code");

            var Person = JavascriptObject.GetConstructor<Person>(engine);
            var Employee = JavascriptObject.GetConstructor<Employee>(engine);
            global.DefineProperty("Person", new PropertyDescriptor(Person, PropertyAttributes.Sealed), true);
            global.DefineProperty("Employee", new PropertyDescriptor(Employee, PropertyAttributes.Sealed), true);

            engine.Execute(@"
// Enclose all code in an anonymous function, to avoid
// global namespace pollution
(function () {

    var bill = new Person(""Bill"");
    var joe = new Employee(""Joe"", 50);

    console.log(Person.created + "" Person instances were created so far."");
    bill.sayHello();
    joe.sayHello();
    joe.showIncome();

})();
            ");

            Console.WriteLine("Press any key to terminate the program...");
            while (Console.KeyAvailable)
                Console.ReadKey(true);
            Console.ReadKey(true);
        }
    }
}

I hope this will be useful to someone else; it sure is to me. Better still would be if it got incorporated into Jurassic.

Feedback is very welcome: download the code and tell me what you think!

Apr 19, 2012 at 1:19 PM

Okay, wow.  Wrapping your head around the dual dichotomy of OO inheritance and prototypical inheritance is tough - yet you've made it seem simple.  Congrats.

Now, I seriously considered implementing your changes to ObjectInstance wholesale.  I decided not to, however, for one reason: I really don't like the design of PopulateFunctions() and I wish I could take it all back.  Your patch kinda just makes it more complicated and even harder to support :-)  I really want to enable you (and others) to build this kind of thing (without needing to patch Jurassic) though, so when I get a chance I will make the following changes:

  • I will make make the ObjectInstance.Prototype property settable.  There is no real reason you can't modify the prototype after an object is constructed, other than paranoia and the fact that I didn't need it to build the framework.  This should eliminate the need for the extra constructor you added.
  • The PopulateFunctions method is mostly just basic reflection and nothing you can't do yourself, except for one bit: creating the ClrFunction instance (the constructor is marked internal).  I will provide a way to call this publically (maybe in a simplified way - it's pretty complicated right now).

Do you think this would meet your needs?  I am open to including your extension classes as well, but I want to get the core stuff right first.

Apr 19, 2012 at 2:29 PM

JurassicExtensions is something I'm developing on my own spare time, even if its first use will be for a commercial application I'm developing at work, so that "wow" of yours repaid me abundantly of quite a few sleepless hours! :-)

I'm all for having JurassicExtensions use an unpatched Jurassic, obviously. I'll still build my own .NET 4.0-only, dynamic-supporting version, but always with an eye to JurassicExtensions' compatibility with the original.

A settable Prototype property would be a bliss!

I don't think all that bad of PopulateFunctions. If you strip it away from Jurassic, it will probably find its way in JurassicExtensions, as will JsFunctionAttribute and JsPropertyAttribute. The only reason why I didn't include PopulateFields and JsFieldAttribute in last sentence is that so far I haven't taken a good look at them, but they'd probably follow the same path. Of course there will be the need for an appropriate public ClrFunction constructor, as you said.

In short, yes, that would definitely meet my needs, and hopefully someone else's too. Thank you very much and keep up the good work!

Apr 19, 2012 at 2:38 PM

As a side note, you may have noticed that the JsClass atribute allows for declaring an own constructor class, as long as it's derived from the appropriate JavascriptConstructor. This should allow you to merge JavascriptObject into ObjectInstance, because e.g. ArrayConstructor can derive from JavascriptConstructor<ArrayInstance>, and so on. If you decide to do it, though, please do it after the other changes you mentioned, so I can adapt JurassicExtensions and publish it with the enhancements and fixes I added in the meantime.