-
Notifications
You must be signed in to change notification settings - Fork 79
Exposing a CLR function as a native JavaScript function
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');
"
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.
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);
}