How to use CLR enum in script?

Aug 1, 2013 at 2:48 PM
Enum just worked with other engines I tried. But in ClearScript, I cannot get it to work.

If I call AddHostType() to add an Enum type and print members, they all show as [object HostObject]. If I compare a member with itself, it evaluates as false!

If I pass an Enum property to script and print its value, it shows as the corresponding CLR enum member name, and its type is the CLR enum type. Such values seem to work well. But how can I expose such Enum constants to script so that I can switch on an input enum value?
Coordinator
Aug 1, 2013 at 3:35 PM
Hello qrli! Thanks for your question.

If I call AddHostType() to add an Enum type and print members, they all show as [object HostObject].

This is correct. Enum members are host objects.

If I compare a member with itself, it evaluates as false!

This is a bug. When returned from the host, an enum member is always re-wrapped with a new object on the JavaScript side. This breaks comparison via the equality operator. We'll fix that right away. Thanks for reporting it!

But how can I expose such Enum constants to script so that I can switch on an input enum value?

Instead of the equality operator, use Object.Equals(). This ClearScriptConsole session demonstrates both the bug and the workaround:
-> day = System.DayOfWeek.Monday
[HostObject:DayOfWeek]
-> day == System.DayOfWeek.Monday
False
-> day.Equals(System.DayOfWeek.Monday)
True
Cheers!
Aug 2, 2013 at 6:51 AM
That's a good explanation.

But you didn't answer the other part: reading enum properties from objects passed to script.
In such case, the enum value I got is not [object HostObject] but just enum value. When I do print(keyEventArgs.KeyCode) for space key down, the output is "Space" instead of [object HostObject].

Another concern is the fact that enum values are treated as object rather than int values. In Jint or Javascript.NET, I can compare enum values with integers freely, as well add/subtract,etc. Especially bitwise logic for flags. An enum value is just a constant number. If it is an object, I cannot do anything except simply passing. So I'd hope ClearScript not to treat enum values as objects. Or at least it can work like/with an integer.
Coordinator
Aug 2, 2013 at 5:02 PM
Edited Aug 2, 2013 at 5:44 PM
But you didn't answer the other part: reading enum properties from objects passed to script.
In such case, the enum value I got is not [object HostObject] but just enum value. When I do print(keyEventArgs.KeyCode) for space key down, the output is "Space" instead of [object HostObject].

There might be some confusion here, and we apologize for that.

ClearScript always exposes enums the same way. Script code gets access to a regular (boxed) .NET enum object. If the script engine is V8, the object is wrapped within a special native JavaScript object of type HostObject. It is this wrapper that supports the standard JavaScript toString() method that returns "[object HostObject]".

Another concern is the fact that enum values are treated as object rather than int values. In Jint or Javascript.NET, I can compare enum values with integers freely, as well add/subtract,etc.

In the interests of type safety, .NET languages usually don't convert implicitly between enums and integers, and neither does ClearScript. Explicit conversion is available, to which ClearScript provides full access should you need it (see below).

Especially bitwise logic for flags.

Yes, combining flags is an important scenario. ClearScript supports it via HostFunctions.flags(). Here's a demonstration in ClearScriptConsole:
-> UriComponents = System.UriComponents
[HostType:UriComponents]
-> f = host.flags(UriComponents.Scheme, UriComponents.Host, UriComponents.Path)
[HostObject:UriComponents]
-> f.ToString()
Scheme, Host, Path
You could also do this with explicit conversion and bitwise arithmetic. HostFunctions.flags() is just a convenience function.

An enum value is just a constant number.

A .NET enum value is not just a number. It is a value whose type is the enum type. This type is important because it allows the value to be converted to its symbolic name via Object.ToString(), it can be used to correctly select a method overload based on the argument types at a call site, etc. Yes, an enum's in-memory representation is numeric and it can be converted to a number, but ClearScript's general approach is to provide seamless access rather than conversion. You're free to convert it yourself if you wish:
-> day = System.DateTime.Now.DayOfWeek
[HostObject:DayOfWeek]
-> day.ToString()
Friday
-> day.toString()
[object HostObject]
-> host.cast(System.Int32, day)
5
-> System.Convert.ToInt32(day)
5

If it is an object, I cannot do anything except simply passing.

You can compare it against another value of the same type - currently via Object.Equals(), but we'll soon post an update that enables enum comparison via the equality operator. You can also convert it to an integer and back. In general, you can do everything you can do with an enum in C#, although perhaps with less convenient syntax. We believe that the benefits of strong typing are worth it.

We'd love to hear your further thoughts on this topic!
Aug 3, 2013 at 6:21 AM
Thanks for the detailed explanation.

IMO, Javascript is a weak/dynamic typing language and that's one of its attracting points as a script language. Casting is natural in C# but strange in Javascript. Enforcing strong typing is not what I'd expect in Javascript. If I've chosen strong typing, I'd take C# as script language and compile dynamically.

And I'm having another trouble which I think is also related to strong typing: I cannot call a CLR function with single precesion float parameters, because Javascript numbers are double. (Not 100% sure because I don't fully understand those dynamic binder code...)

For the enum value from keyEventArgs.KeyCode issue, I think there are still some confusion. My debugger shows the object type is System.Windows.Forms.Keys rather than HostObject, while print(Keys.Space) in script gives HostObject rather than "Space". So two differences from what you explained: 1) Enum values are not wrapped in HostObject in all scenarios; 2) Enum member's toString() does not give the enum member name but just [object HostObject].


I'm a bit frustrated for now. I don't know if these issues are related to my usage style. I'm calling Application.Run() for message loop in script, and scripts are called back though events for UI interaction. This is normal in C# but maybe not so well for script. I know some script engines have issues in such re-entrance usage scenario. I changed from Jint to ClearScript because Jint is really shitty at script error reporting, and Javascript.NET (which wrapps v8 too) does not support callbacks at all. Sorry for the rant...
Coordinator
Aug 3, 2013 at 4:14 PM
Edited Aug 4, 2013 at 1:25 PM
IMO, Javascript is a weak/dynamic typing language and that's one of its attracting points as a script language. Casting is natural in C# but strange in Javascript.

We agree 100%. However, ClearScript's job is to bridge JavaScript with a system that relies on strong typing for basic things like method binding. With .NET's heavy use of classes, overloading, generics, and extensions, you really couldn't get very far without strong type information. That's why ClearScript usually avoids conversion and gives script code direct access to .NET objects and types.

Having said that, some conversion is necessary. It would be a huge pain, for example, if ClearScript didn't convert things like strings and integers into their script form. That's a no-brainer because strings and integers can be converted back and forth without data loss. What to do with enums, however, was less obvious. Enums exist for convenience and code readability, but their status as true .NET types involves them in overload resolution, among other things. That makes the one-way enum-to-integer conversion a serious problem. We were on the fence about enums but decided not to expose them as integers mostly for that reason.

I cannot call a CLR function with single precesion float parameters, because Javascript numbers are double.

Right you are, and thanks for finding yet another bug! It is currently not possible for script code to invoke a .NET method with a float parameter in ClearScript. We'll fix that ASAP. Note that the only safe implicit double-to-float conversion is one that is reversible without precision loss, so in most cases you'll have to use an explicit conversion such as Convert.ToSingle() to invoke such a method. Currently doing so doesn't help, and that's what we need to fix.

My debugger shows the object type is System.Windows.Forms.Keys rather than HostObject, while print(Keys.Space) in script gives HostObject rather than "Space". So two differences from what you explained: 1) Enum values are not wrapped in HostObject in all scenarios;

The HostObject wrapper is a native V8/JavaScript object that is not visible on the .NET side. These wrappers are required by V8's embedding model and reside in V8's internal heap. When script code passes any .NET object to a .NET method, the wrapper is stripped away; the method wouldn't work otherwise. There might be some confusion because ClearScript has an unrelated internal .NET class called HostObject.

2) Enum member's toString() does not give the enum member name but just [object HostObject].

That's correct. ClearScript does not intercept toString(), so you're seeing its default JavaScript behavior as applied to the HostObject wrapper. If you want the enum member name, use the similarly named .NET method ToString().

I'm a bit frustrated for now. I don't know if these issues are related to my usage style. I'm calling Application.Run() for message loop in script, and scripts are called back though events for UI interaction. This is normal in C# but maybe not so well for script. I know some script engines have issues in such re-entrance usage scenario.

We haven't used ClearScript with Windows Forms, but we're always looking to improve ClearScript, so we'd love to know the specifics of any problems you encounter in that environment. In any case, we'd love for you to keep using ClearScript; you have a knack for finding bugs! :)
Aug 4, 2013 at 2:19 AM
I see that we have a design philosophy difference here. You want to keep the .NETish part as much as possible. But when we use scripts, we want to minimize that as much possible. In ideal case, script writers don't need to know .NET at all. The .NET interop layer is only a convenience so that we don't need to go back to C++ to provide objects to script. It is just like a web developer who works with the web page DOM, where the script writer does not need to know C++/COM nor follow C++/COM typing rules. I agree that it is not easy/trouble-free to achieve 100% of that. But I will expect a script writer can feel at home most of the time. If he/she encounters .NET quirks frequently and has to use varies workarounds, it is clearly wrong design (either the language choice or the interop choice).

Having tried LuaInterface and IronPython, I'd say they do the job very well. You really feel at home when working with them. However, some irrational part of me really want the web language instead. Pity that the road to .NET-Javascript integration is not so easy.

Anyway, here's more findings:
1) I finally find out why I was confused on enum values:
print() is a host method with a string parameter.
print("Key.Space = " + Key.Space); // result is "Key.Space = [object HostObject]"
print(Key.Space); // result is "Space"
You see the different results?

2) ScriptEngineException.ErrorDetails is null when an error happened inside an event callback function.
This only happens with JScriptEngine. V8ScriptEngine works fine.

And 2 feature related questions:
1) Does ClearScript support or intend to support .NET's dynamic/expendo object? I mean, in natural script syntax.
2) It is possible to enable V8's harmony features?
Coordinator
Aug 5, 2013 at 3:21 AM
Hi qrli!

You want to keep the .NETish part as much as possible. But when we use scripts, we want to minimize that as much possible. In ideal case, script writers don't need to know .NET at all.

Agreed, but it isn't ClearScript's goal to provide ideal script APIs. Its goal is to make existing .NET APIs fully scriptable, and to make ideal script APIs easy to implement.

Consider a typical, non-trivial .NET API, complete with all its ".NETish parts". ClearScript aims to make such an API instantly scriptable, with full access to all its ".NET quirks". Replacing it with an ideal script API is something that (a) requires additional design effort by a human being, and (b) results in an API that is ideal only for the target script language. For these reasons it is outside the scope of the ClearScript project.

It is just like a web developer who works with the web page DOM, where the script writer does not need to know C++/COM nor follow C++/COM typing rules.

The DOM is a script API by design; a typical .NET API is not. Nothing stops you from implementing a DOM-like API layer using ClearScript. In fact, making that easy to do is one of ClearScript's goals.

ClearScript is analogous to COM Interop. That framework allows .NET code to access COM APIs. Does it turn COM APIs into ideal .NET APIs? Of course not. It can't, and it shouldn't. Full access comes with quirks.

All that having been said, it is absolutely a goal for ClearScript to make .NET scripting as natural as possible.

print("Key.Space = " + Key.Space); // result is "Key.Space = [object HostObject]"
print(Key.Space); // result is "Space"
You see the different results?

Yes, this makes sense. In the first line, the string concatenation is performed by the JavaScript engine, which calls toString() on each operand. To get the desired result, do this:
print("Key.Space = " + Key.Space.ToString());
It might be a good idea for ClearScript to implement toString() and eliminate the ambiguity. We'll take a look. Interestingly, JScript - which doesn't use HostObject wrappers - produces "Key.Space = undefined".

ScriptEngineException.ErrorDetails is null when an error happened inside an event callback function.
This only happens with JScriptEngine. V8ScriptEngine works fine.

Unfortunately Windows Script engines (JScript/VBScript) don't provide error details unless you enable debugging. To do so, pass WindowsScriptEngineFlags.EnableDebugging into the engine constructor.

Does ClearScript support or intend to support .NET's dynamic/expendo object? I mean, in natural script syntax.

Yes, we're working on this. Please see here.

It is possible to enable V8's harmony features?

Not currently. This is on our long-term to-do list.

Thanks again for all your input!
Coordinator
Aug 14, 2013 at 5:03 PM
Hello qrli!

I cannot call a CLR function with single precesion float parameters, because Javascript numbers are double.

Version 5.3.6 includes a new toSingle() method for passing float arguments. Here's an example using ClearScriptConsole:
-> list = new System.Collections.Generic.List(System.Single)
[HostObject:List<Single>]
-> list.Add(Math.PI)
Error: The best overloaded method match for 'System.Collections.Generic.List<float>.Add(float)' has some invalid arguments
-> list.Add(host.toSingle(Math.PI))
-> list[0]
3.14159274101257
The new version also provides toByte(), toInt16(), etc.

Cheers!