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

Exposing a CLR function as a native JavaScript function

axefrog edited this page Jul 17, 2011 · 5 revisions

F#

The two main functions for exposing F# functions as native JavaScript objects to IronJS are found in the Native.Utils module and are named createFunction and createConstructor. They both have the same type signature: Environment -> int option -> #Delegate -> FunctionObject.

The first argument is the IronJS environment the function should be bound to, the second is amount of arguments the function will report from it's length property to JavaScript code, the third argument must be a subtype of System.Delegate and is the delegate to be invoked when the function is called.

The only difference between createFunction and createConstructor is that createFunction creates a function that can't be invoked with the new keyword.

Let's step through the whole process of creating a function and exposing it to the runtime and call it from some code, first open the namespaces we're going to need

open System
open IronJS
open IronJS.Hosting.FSharp

Then create the context and pull the environment out of it

let ctx = createContext()
let env = ctx.Env

Define a simple function that repeats a character X amount of times, since JavaScript doesn't deal with char types we'll pass in a string and take the first character of it.

let repeatChar (s:string) (times:double) = new String(s.[0], int times)

The next thing we need to do is to wrap the function in a delegate, the easiest way is just to use the System.Func delegates introduced in .NET 3.x.

let repeatCharDelegate = new Func<string, double, string>(repeatChar)

To actually create the JavaScript object that wraps our delegate we need to call the utility function Native.Utils.createFunction, passing in the delegate we created, the environment and the value of the length property:

let repeatCharFunction = repeatCharDelegate |> Native.Utils.createFunction env (Some 2)

As you might have noticed the length we send in is typed as int option, this is because you can send in None instead and createFunction will try to guess the the value of the length property instead.

The last thing we need to do is to set the function in the global scope and we're ready to call it from our source code, this is simply done by using the setGlobal function from the Hosting.FSharp module.

ctx |> setGlobal "repeatChar" repeatCharFunction

Now we're ready to call our function from user code.

let result = ctx |> execute @"repeatChar('a', 5)" (* "aaaaa" *)

The complete code looks like this

open System
open IronJS
open IronJS.Hosting.FSharp

let ctx = createContext()
let env = ctx.Env

let repeatChar (s:string) (times:double) = new String(s.[0], int times)
let repeatCharDelegate = new Func<string, double, string>(repeatChar)
let repeatCharFunction = repeatCharDelegate |> Native.Utils.createFunction env (Some 2)

ctx |> setGlobal "repeatChar" repeatCharFunction

let result = ctx |> execute @"repeatChar('a', 5)"

Now this just deals with functions. But how about methods, or rather functions called on an object, that has a this parameter? How do we get access to this inside of our F# function? It's really very simple, if the first two arguments to your function is of the type FunctionObject and CommonObject IronJS will automatically send the function being called into the first (FunctionObject) parameter and the object the function is being called on into the second (CommonObject) parameters.

Let's create a function that checks if an object contains two properties, basically like hasProperty but it takes two properties to check.

let hasBothProperties (func:FO) (this:CO) (a:string) (b:string) = this.Has(a) && this.Has(b)
let hasBothPropertiesDelegate = new Func<FO, CO, string, string, bool>(hasBothProperties)
let hasBothPropertiesFunction = hasBothPropertiesDelegate |> Native.Utils.createFunction env (Some 2)

This gives the hasBothProperties function access to both the object that represents the function itself inside the runtime through the func parameter and the object it's being called as a method on through the this parameter. Just note that the type FO and CO are F# type aliases for FunctionObject and CommonObject.

It's pretty obvious why it's good to have access to the this parameter, but why would you want to access the function object itself? If you ever need to modify the environment (for example create a new object, throw a javascript-catchable exception, etc.) you can access the environment object through func.Env, or if you need to call the function recursively (and still want to maintain the JavaScript stacktrace)

So, we have our method defined, now we want to expose it to IronJS, there's two ways to do it - let's start with the obvious but impractical way. Basically set it as a global parameter and then assign it to a the property of the object we want it on.

ctx |> setGlobal "hasBothProperties" hasBothPropertiesFunction
ctx |> execute @"
  var o = {foo:1, bar:2};
  o.hasBothProperties = hasBothProperties;
  o.hasBothProperties('foo', 'bar');
"

But assuming we want to re-use the method on a bunch of objects, the easiest way is to assign it to a common prototype, for example we could put it into the Object.prototype object, which you can find inside the environments Prototype record, like this:

env.Prototypes.Object.Put("hasBothProperties", hasBothPropertiesFunction);

And we can the call it on any object, without having to mess around inside the javascript itself and set the property manually:

ctx |> execute @"
  var o = {foo:1, bar:2};
  o.hasBothProperties('foo', 'bar');
"

C#

Exposing a global function

Exposing a C# method as a JavaScript function is quite simple. First, define the method in C#:

void Print(BoxedValue value)
{
	Console.WriteLine(value.ToString());
}

The example above takes a parameter of type BoxedValue as JavaScript objects are untyped, so regardless of the preferred parameter type, the script can try to pass in any type of value. BoxedValue provides all of the necessary methods and properties to extract the desired value from the parameter. You can use a native CLR type for the parameter if preferred, but if it is not of the correct type, an exception might be thrown.

Next, expose the method to your JavaScript context like so:

var context = new CSharp.Context();
context.SetGlobal("print", IronJS.Native.Utils.CreateFunction<Action<BoxedValue>>(context.Environment, 1, Print));

The first argument is the name of the function as it should be seen by the JavaScript runtime. The second creates the native function object and takes a type argument for a delegate matching our C# method. The CreateFunction method takes three arguments; (1) the Environment object exposed by our context object, (2) an integer telling JavaScript how many parameters the function should expect and (3) a reference to the method itself.

Exposing a function as a member of a native JavaScript type

An example of how to define and implement the substr function on the String prototype in C# can be seen here:

// 'env' is the current hosting environment
var substr = Utils.CreateFunction<Func<FunctionObject, CommonObject, int, int, CommonObject>>(env, 1, Substr);
env.Prototypes.String.Put("substr", substr);

// ....

private static CommonObject Substr(FunctionObject function, CommonObject stringObject, int startIndex, int length)
{
    string stringValue = TypeConverter.ToString(stringObject);

    if (!String.IsNullOrEmpty(stringValue))
    {
        // CLI's System.String.Substring should be equivalent to JavaScript's String.prototype.substr
        stringValue = stringValue.Substring(
            // For some reason, IronJS passes integers as doubles, so we need to cast them here
            (int)startIndex, (int)length);
    }

    return TypeConverter.ToObject(function.Env, stringValue);
}