Jint: using CLR object 's properties in Javascript functions - javascript

I have been testing the jint library and hit a snag. Given this class in C#:
public class Foo
{
public string Name { get; } = "Bar";
}
And this code:
Engine engine = new Engine(x => x.AllowClr());
object rc = _engine
.SetValue("foo", new Foo())
.Execute("foo.Name.startsWith('B')")
.GetCompletionValue()
.ToObject();
I get the error: 'Jint.Runtime.JavaScriptException: 'Object has no method 'startsWith'''
This works, however:
"foo.Name == 'Bar'"
So can I get the former to work?

Support for extension methods is added here. But can't get it to work with the .NET string extension methods directly, it does work with an intermediate extensions class.
Update: The string methods like StartsWith aren't real extension methods indeed.
Looks like startsWith is already supported natively now. Replaced GetCompletionValue with the suggested Evaluate
// Works natively with Jint 3.0.0-beta-2032
Engine engine = new Engine();
bool result = engine
.SetValue("foo", new Foo())
.Evaluate("foo.Name.startsWith('B')")
.AsBoolean();
I've tried to add the string extension methods, but that doesn't seem to work. But using your own class for the extension methods and use it that way does work.
public static class CustomStringExtensions
{
public static bool StartsWith(this string value, string value2) =>
value.StartsWith(value2);
}
Engine engine = new Engine(options =>
{
options.AddExtensionMethods(typeof(CustomStringExtensions));
});
bool result = engine
.SetValue("foo", new Foo())
.Evaluate("foo.Name.StartsWith('B')")
.AsBoolean();
I've asked about the native extension methods support here, as I'm curious if and how it is supposed to work.

Related

Migrate Nashorn to GraalVM

I am using Nashorn JS engine from OpenJDK 12. Nashorn seems to be deprecated. I am looking which are the available alternatives. I found GraalVM, but I am not sure if this is the best.
How can I execute a GraalVM JavaScript from Java ? Do you have any example ?
With Nashorn was using from Java:
NashornScriptEngineFactory nsef = new NashornScriptEngineFactory();
ScriptEngine engine = nsef.getScriptEngine( BasicDBObject.class.getClassLoader() );
final Bindings binding = engine.getContext().getBindings(ScriptContext.ENGINE_SCOPE);
In Nashorn I create a WrappedMongoDatabase which extends AbstractJSObject. There I add some 'virtual' methods to simulate the MongoDB Query language, which does for example getCollection('persons').find()...
Do you know a way to replace the AbstractJSObject in GraalVM?
I had a look to ProxyObject, somehow I couldn't find a way to override the call(Object thiz, Object... args) like in AbstractJSObject.
public class WrappedMongoDatabase extends AbstractJSObject {
#Override
public boolean hasMember(String name) {
return "getCollection".equals( name ) || "createCollection".equals(name)||...;
}
#Override
public Object getMember(final String name) {
if ( hasMember( name ) ){
return new AbstractJSObject() {
#Override
public Object call(Object thiz, Object... args) {
switch( name ) {
case "getCollection":
if (args.length == 1 && args[0] instanceof String) {
return getCollection((String) args[0]);
}
break;
...
}
}
}
}
}
}
GraalVM is a solid way to go. We've been using it for a while now, it runs well, and the JavaScript implementation is far better than Nashorn (or Rhino). In particular, it is ECMA2020 compliant, it supports Node requires (that's huge!), it performs much better, etc...
GraalVM is a big step forward if you're using Nashorn, but it does require some adjustments, which are covered reasonably well in the GraalVM documentation.
Follow GraalVM ScriptEngine
ScriptEngine engine = new ScriptEngineManager().getEngineByName("JavaScript");
Bindings bindings = engine.getBindings(ScriptContext.ENGINE_SCOPE);
bindings.put("polyglot.js.allowHostAccess", true);
bindings.put("polyglot.js.allowHostClassLookup", (Predicate<String>) s -> true);
bindings.put("javaObj", new Object());
engine.eval("(javaObj instanceof Java.type('java.lang.Object'));"); // would not work without allowHostAccess and allowHostClassLookup
Notice nashorn compatibility mode:
These options control the sandboxing rules applied to evaluated JavaScript code and are set to false by default, unless the application was started in Nashorn compatibility mode (--js.nashorn-compat=true).

How to create a JavaFX WebBrowser javascript listener without a Member name

I have been using this snippet for creating js listener in my javafx browser:
engine.getLoadWorker().stateProperty().addListener(
(observable, oldValue, newValue) -> {
if (newValue == Worker.State.SUCCEEDED) {
System.out.println("changed event detected");
JSObject win = (JSObject) engine.executeScript("window");
win.setMember("java", new JavascriptListener());
}
}
);
But I would like to be able to call my functions without prefixing them with "java".
Is that possible?
Generally speaking, no.
There are no (working) java/javascript functions to do this on a native scale. I tried the javascript Object.assign function, but it didn't work for variables or methods. Other, similar things wouldn't work properly on Java objects either
If you are willing to expose yourself to the horrors of Java reflection, though, be my guest:
public class JavaVariableAssigner {
//javascript-stored objects do not appear to the
private static ArrayList<Object> gcLock = new ArrayList(); //garbadge collector so we need to make sure
//they don't get deleted by always keeping
//a reference to them from this list
public static void addAllMembers(WebEngine browser, Object obj) {
gcLock.add(obj);
((JSObject) browser.executeScript("window")).setMember("java", obj);
for(Method m : obj.getClass().getDeclaredMethods()) {
ArrayList<String> parameterNames = new ArrayList();
for(Parameter p : m.getParameters()) parameterNames.add(p.getName());
String params = String.join(",", parameterNames.toArray(new String[0]));
browser.executeScript("function "+m.getName()+"("+params+"){ return java."+m.getName()+"("+params+"); };");
}
for(Field f : obj.getClass().getDeclaredFields()) {
browser.executeScript("var "+f.getName()+" = java."+f.getName()+";");
}
}
}
All your code using the 'java' prefix will continue to work, but every method can now (hopefully) also just be accessed directly by typing its name.
Like I (sort of) said, though, this method relies entirely on just stitching together javascript and evaling it so I would not recommend it.

Supply JavaScript date to Nashorn script

I am working on an API in Java that allows users to write scripts and access a specific set of methods that are passed in (in the form of an API object) by the Nashorn script engine.
I want to, in the JavaScript, call a function getDate(), which will return some arbitrary date (as a native JavaScript date) that's provided from the Java side.
I have tried simply putting an org.java.util.Date on the API object, but that won't behave like a JS date. The goal is to make this as simple as possible for end-users who are experienced with JS.
Java Example:
public class MyAPI {
public void log(String text){
System.out.println(text);
}
public Date getDate(){
// Return something that converts to a native-JS date
}
public static void main(){
// MyClassFilter implements Nashorn's ClassFilter
ScriptEngine engine = new NashornScriptEngineFactory().getScriptEngine(new MyClassFilter());
((Invokable) engine).invokeFunction("entryPoint", new MyAPI());
}
JavaScript example
function entryPoint(myApi){
var date = myApi.getDate();
myApi.log(date.getMinutes());
}
jdk.nashorn.internal.* packages are nashorn internal implementation classes. There is no guarantee that these won't be changed or be removed between JDK versions. Besides with a security manager around, accessing these packages from java code directly would result in SecurityException being thrown! With jdk9 modular jdk, these packages are not exported packages from nashorn module and so javac won't even compile your code in jdk9!
I'd recommend using JS wrapper (solution 1) in the answer by user "ug_". If you do have to call from Java, you can use API exported from jdk.nashorn.api.scripting package.
If "engine" is your javax.script.ScriptEngine of nashorn, then you can do something like the following:
import jdk.nashorn.api.scripting.*;
..
public Object getDate() {
// get JS Date constructor object - you can get once and store
// as well/
JSObject dateConstructor = (JSObject) engine.eval("Date");
// now do "new" on it
return dateConstructor.newObject();
}
With that, your JS script can call "getDate()" on your API object and get a JS
Date object. Note that you can also pass constructor arguments to newObject
method call (it is a Java variadic method).
The Nashorn engine has objects it uses internally which represent the Javascript objects. As you have guessed the java.util.Date != new Date() (in javascript). The engine uses a class called jdk.nashorn.internal.objects.NativeDate to represent its JS date.
If I were building this out I would not have the NativeDate constructed in the Java but instead have a wrapper in Javascript for the MyApi object which would contain a few other native JS methods, such as getDate().
var MYAPI_JAVASCRIPT = {
log: function() {
print(arguments);
},
getDate: function() {
return new Date();
}
}
You could then pass that object as the method parameter.
However if your really set on using the NativeDate in your Java code then you can construct one like so:
public NativeDate getDate() {
return (NativeDate) NativeDate.construct(true, null);
}

How to create an C#.Net DLL to use in JavaScript

I have created a .Net DLL with few simple classes. I have registered the DLL using RegAsm and I got a message that types were registered successfully.
RegAsm Syntax Used :
C:\Windows\Microsoft.NET\Framework\v4.0.30319>RegAsm.exe "D:\Projects\TestDLL.Core.dll"
C# Code:
namespace MyTestDLL.Core
{
public class PacketInfo
{
// constructor
public PacketInfo()
{
}
public string HostName { get; set; }
// and so on ......
}
}
I have set the ComVisible property to true in AssemblyInfo.cs file of this DLL.
// [assembly: ComVisible(true)]
However when I create an object out of it in JavaScript and run the script in Command prompt , I'm getting either it is not an object or null.
JS Code :
var testObj = new ActiveXObject(MyTestDLL.Core.PacketInfo);
testObj.HostName = "Test";
Can anyone advise me how to resolve this ?
You need to add a ProgId attribute to the class, something like this:
[Guid("some guid that you will never change")]
[ProgId("MyLib.PacketInfo")] // this is arbitrary
public class PacketInfo
{
....
}
The guid is optional, but if you don't set it yourself, it will be something you won't control, so it's better to define it. And of course I did not add the ComVisible because you already defined it at assembly level (I personnally prefer to set it at class level).
Then, once registered, you shall be able to use it like this (use the progid as a string):
var testObj = new ActiveXObject('MyLib.PacketInfo');
testObj.HostName = "Test";
I was able to achieve that by adding the following line to My DLL just above the class,
[Guid("A32AB771-9967-4604-B193-57FAA25557D4"), ClassInterface(ClassInterfaceType.None)]
Earlier I wasn't having the ClassInterfaceType part in my code.
Also ensure that each of your classes are having unique GUID.
FYI : You can create GUID using Visual Studio Tools GUID Creator GUI.

How to build an API with QJSEngine?

I am starting with Qt and one of my projects is using QJSEngine to evaluate javascript and I want to provide an entire API to the script, with classes and global functions.
Right now my program provides only the ECMAScript default stuff (eval, encodeURI, parseInt, etc...), but I need to expose some custom classes to the code, like the browsers API (WebSocket class, Image class, document object). For example:
var obj = new CustomClass("", 0);
var ret = obj.customClassMethod("[...]!");
customFunction(ret);
I need to define the behavior of the classes in C++, it wouldn't help evaluate the classes definition and let the user code run.
In contrast to QScriptEngine, where you can add custom classes if they inherit from QObject by using the Q_SCRIPT_DECLARE_QMETAOBJECT macro, the QJSEngine does not directly provide this functionality.
You can still use the Qt Meta-object system to provide interfaces for Javascript, but you have to instantiate the object in C++ and add it to the Javascript context.
Then its slots, methods defined with Q_INVOKABLE, and properties defined with Q_PROPERTY are all accessible from within the Javascript runtime.
Now you can create a factory which creates instances of your custom class CustomClass for a given QJSEngine wrapped as Javascript objects:
class CustomClassFactory : public QObject
{
Q_OBJECT
public:
CustomClassFactory(QJSEngine* engine) : m_engine(engine) {}
Q_INVOKABLE QJSValue createInstance() {
// The engine takes ownership and destroys the object if no longer required.
return m_engine->newQObject(new CustomClass());
}
private:
QJSEngine* m_engine;
}
A factory instance needs to be constructed and added to the global object of the Javascript runtime:
QJSEngine engine;
QJSValue factoryObj = engine.newQObject(new CustomClassFactory());
engine.globalObject().setProperty("_customClassFactory", factoryObj);
Now we can construct objects in Javascript with:
var obj = _customClassFactory.createInstance()
As we've come this far, lets additionally inject a constructor for the custom class into the Javascript runtime:
QJSEngine engine;
// Again, the QJSEngine will take ownership of the created object.
QJSValue factoryObj = engine.newQObject(new CustomClassFactory());
engine.globalObject().setProperty("_customClassFactory", factoryObj);
engine.evaluate(
"function CustomClass() {"
" return _customClassFactory.createInstance()"
"}");
Et voilà, now you can construct C++ object in Javascript, like you would custom Javascript classes:
var obj = new CustomClass()
For the mentioned WebSocket API you could wrap QtWebSocket for that purpose – that was exactly what I required when I came up with the proposed approach.
Note that for the sake of simplicity I omitted parameters for the constructor, but they can simply be added as well.
PS: I would have added more links to the official documentation, but due to the lack of reputation I'm not allowed to.
In Qt 5.8 a new feature was added to QJSEngine: newQMetaObject
You simple add the static meta object e.g. &MyQObjectDerivedClass::staticMetaObject to the JSEngine using the above function.
You will then be able to new those objects in Javascript from within QML. I have found this a very neat solution.
As the documentation says you must mark you constructor Q_INVOKABLE or you won't be able to instantiate an object of your class.
The property system (setters/getters) works as expected as do slots.
https://doc.qt.io/qt-5/qjsengine.html#newQMetaObject
Here is my test code - first is the C++ part that adds the meta object
QQmlApplicationEngine engine;
QJSValue jsMetaObject = engine.newQMetaObject(&MyClassOfObject::staticMetaObject);
engine.globalObject().setProperty("MyClassOfObject", jsMetaObject);
I can now write JS that news an object of that type and use setters/getters etc. This code actually exists in a MouseArea onClicked handler for manual testing.
var bob = new MyClassOfObject();
print(bob.x);
bob.x = 918264;
print(bob.x);
print(bob.words);
And here is the class definition...
class MyClassOfObject : public QObject
{
Q_OBJECT
Q_PROPERTY(int x READ getX WRITE setX)
Q_PROPERTY(int y READ getY WRITE setX)
Q_PROPERTY(QStringList words READ getWords)
public:
Q_INVOKABLE explicit MyClassOfObject(QObject *parent = nullptr);
public slots:
int getX() const { return x; }
int getY() const { return y; }
void setX(int x) { this->x = x; }
void setY(int y) { this->y = y; }
QStringList getWords() const;
private:
int x = -113;
int y = 616;
QStringList stringList;
};
As of Qt5.5 QScriptEngine has been deprecated, so only QJsEngine should be used in the future.
https://wiki.qt.io/New_Features_in_Qt_5.5#Deprecated_Functionality
If you look up Documentation of QScriptEngine, or by searching "QScriptEngine examples" you can find some stuff about making Custom C++ Classes available to QScriptEngine.
Here is a good place to start:
link to example
QScriptEngine is very similiar to QJsEngine, so it shouldn't be a big problem for you.
Hope this helps :)

Categories

Resources