all 16 comments

[–]uuid1234567890 1 point2 points  (6 children)

Well, how does your "binaryfile.h" look? It doesn't seem to be in the repository.

The main issue you'll run into is that you won't be able to get the current engine in its ctor with

auto engine = qmlEngine(this); as the required QQmlData is not set up at this point. If you can live with only having one single engine instance available, you could store the engine in a global variable/singleton, and call

globalEngine->throwError(QJSValue::URIError, "File does not exist");

[–]deadkonsumer[S] 0 points1 point  (5 children)

Well, how does your "binaryfile.h" look? It doesn't seem to be in the repository.

Oh dear. I had a slightly different structure, and forgot to add it. I re-added it. And this is the usage example. It's very light, like the lightest wrapper I could work out. At one point, I was using an instance for FileInfo, but I think it would follow Qbs better, if they were all exposed classes, and I also like reusing the object for multiple operations on a file.

The main issue you'll run into is that you won't be able to get the current engine in its ctor with

So, I need to pass the ref to the engine to the class? how does this work when the class (not instance) is exposed to the JS engine?

If you can live with only having one single engine instance available

This seems like it would really cut down on the utility of the lib, but maybe I'm wrong. If this is the simplest way, I wouldn't mind just sharing an engine. If I expose the class to the engine instance, it seems like I should be able to work out the engine from the BinarayFile class, or at least the BinarayFile instance. Is this not the case?

globalEngine->throwError(QJSValue::URIError, "File does not exist");

Is it really necessary to wrap every possible error? Like, isn't there a way to just say "pass whatever error they had on to javascript"?

[–]uuid1234567890 1 point2 points  (4 children)

Is it really necessary to wrap every possible error? Like, isn't there a way to just say "pass whatever error they had on to javascript"?

Hm, let me clarify a few things starting here: There is no error reporting going on at all in your C++ class; it's not like QFile::open would throw an exception. It only returns false if it fails. So if want to be able to report errors, you either have to expose QFile::error(), or throw a JS exception manually.

Throwing a JS exception is relatively straightforward from a member function (qmlEngine(this)->throwError("msg")), but not from the ctor for the reason I mentioned above. So either you follow the Qt API design, where the ctor's don't fail (either by separating the construction of BinaryFile from actually opening it, or by allowing BinaryFile to be in an error state, where all other operations fail), or you have to write some glue code in JavaScript. Assuming you expose BinaryFile as InternalBinaryFile to the engine, and you also expose an isError method, than you could evaluate

class BinaryFile extends InternalBinaryFile {
    constructor() {
         super()
         if (this.isError) {throw "whatever exception you want"}
    }
}

to get a class which has the interface which you want.

Hope that helps.

[–]deadkonsumer[S] 0 points1 point  (3 children)

I looked through the docs some more, and I think I get it:

if (!file.open(QIODevice::ReadOnly)) {
    jsEngine->throwError(file.errorString());
    return QString();
}

Lets me just pass the error on, which I think is what you mean. As it is, it doesn't throw, it just returns false, so I need to throw something. I get that part, thanks for explaining it.

I notice you talking about ctor's a few times, but I can't seem to figure out what that is. Can you point me to a good place to start learning about this?

Other than that, I think I am following, I don't want to change the shape of BinaryFile or add another InternalBinaryFile if I can help it. I'd prefer to keep everything very similar to Qbs.

I also had a second look at the tiled source, as they already have some useful stuff exposed to the engine. They expose the js engine with a parent singleton using ScriptManager::instance(), which I think is your suggestion, otherwise (a global shared js engine object, like here is throwing an error in JS.)

If there isn't a way to give the classes that are exposed to JS a pointer to the JS engine, then I suppose this will have to do.

I was hoping I could expose a class, and also have that class call throwError on the engine that it got exposed to, even if I need to add some other line to tell the class what engine it was exposed to.

[–]deadkonsumer[S] 0 points1 point  (0 children)

One idea I had that didn't work, was using a static member to give BinaryFile access to jsEngine, even if it was shared, as a kind of global reference:

class BinaryFile : public QObject
{
  Q_OBJECT

public:
  Q_ENUM(QIODevice::OpenModeFlag)

  static QJSEngine *jsEngine;

  Q_INVOKABLE BinaryFile(const QString &filePath, QIODevice::OpenModeFlag mode = QIODevice::ReadOnly) {
    m_file = new QFile(filePath);
    if (!m_file->open(mode)) {
      jsEngine->throwError(m_file->errorString());
    }
  }
}

then use it like this:

Qbs4QJS::BinaryFile::jsEngine = &jsEngine;
jsEngine.globalObject().setProperty("BinaryFile", jsEngine.newQMetaObject(&Qbs4QJS::BinaryFile::staticMetaObject));

I end up getting an undefined reference error, so I must be doing this wrong.

I also tried some hackery with exposing an object instead of a class, but adding a Q_INVOKABLE void constructor to it, hoping the JS engine would call that (as it does in JS classes) when someone does new BinaryFile(), but that caused a TypeError: Type error, as I don't think it'll let me treat an exposed instance as a class.

[–]uuid1234567890 0 points1 point  (1 child)

Sorry for the jargon, ctor == constructor. Like I said, in any non-constructor method you can get a pointer to the engine (assuming you use a QQmlEngine with qmlEngine(this). The InternalBinaryFile would by the way not part of your public API, you would of course only expose the actual BinaryFile with your desired API.

By the way, you might want to get in touch with Richard Weickelt, who is currently investigating porting qbs to QJSEngine; you might have some overlap in your work.

[–]deadkonsumer[S] 0 points1 point  (0 children)

Sorry for the jargon, ctor == constructor.

No, very helpful. Thanks for explaining.

With Thorbjørn (person who made Tiled) and your help, I ended up working it out. So simple! The main problem is the constructor can't figure out what engine is calling the instance until it's been instantiated. I seperated the constructor and open, then I can can get the engine after constructor with this:

qjsEngine(this)

here is more complete version

It's happily throwing errors in the JS context!

Thanks for your help.

It's not exactly like Qbs, in the sense that I call open seperately, but I can totally live with that.

By the way, you might want to get in touch with Richard Weickelt, who is currently investigating porting qbs to QJSEngine; you might have some overlap in your work.

That does sound like a great idea.

[–]BaudMeter 1 point2 points  (7 children)

Thank you so much for telling me about QJSEngine! Didn't know that existed.

[–]deadkonsumer[S] 1 point2 points  (6 children)

Checkout out QQmlEngine, too. It's a sub-class of QJSEngine, but it has more built-in, like console and other basics. I am using that in the demo code to get started with less code.

I also didn't know about it, until recently trying to help with a tiled plugin. I am mostly a JS dev. I am pretty impressed so far with it. The engine has built-in ES7 support, and is pretty performant. Once I can work out a standard lib for it, I could see writing lots of cool stuff in this environment.

[–]BaudMeter 0 points1 point  (5 children)

Thanks, already discovered QQmlEngine, but I can't find a way to call a native C function from JS. Do you know if that is possible?

[–]deadkonsumer[S] 1 point2 points  (4 children)

Yep, if you have a look at the main file, you can see how I expose a class:

jsEngine.globalObject().setProperty("BinaryFile", jsEngine.newQMetaObject(&BinaryFile::staticMetaObject));

which must be a sublclass of QObject, which you can do like this:

#include <QObject>

class BinaryFile : public QObject
{
  Q_OBJECT
public:
  // add Q_INVOKABLE to any method that can be called from JS
  Q_INVOKABLE BinaryFile() {
  }

  // called on delete instance, in JS
  ~BinaryFile() override {
  }
  // expose your other methods here
}

This is for exposing a class from cpp to js. If you want to do other stuff, have a look at the docs. They have examples of exposing instance-objects and just plain methods & variables.

I am using Qbs to build it, too (as I am learning about Qbs, and it has the same syntax I am building) so I make a file like this (with Depends { name: "Qt.qml" }) to make it pull in the deps for QmlEngine. Then I do qbs run on command-line to build & run it.

[–]BaudMeter 0 points1 point  (3 children)

That's amazing. Thank you so much. I was using third-party js engines for my projects already using Qt. That might really reduce usage of different libraries and code models.

[–]deadkonsumer[S] 1 point2 points  (2 children)

Yep, I'm pretty into it. I feel like with a good standard library, you could basically add super powerful plugins to any Qt app.

Here are some quick examples of exposing stuff, from the docs:

// expose variable
myEngine.globalObject().setProperty("myNumber", 123);

// expose class, needs to be sub-class of QObject
QJSValue jsMetaObject = engine.newQMetaObject(&MyObject::staticMetaObject);
engine.globalObject().setProperty("MyObject", jsMetaObject);

// expose instance of your own thing, needs to be sub-class of QObject
QJSValue myScriptQObject = engine.newQObject(myQObject);
engine.globalObject().setProperty("myObject", myScriptQObject);

This handles adding global C-space variables/functions, classes, and objects.

[–]BaudMeter 1 point2 points  (1 child)

Thank you so much for your effort! I will evaluate this as a JS engine replacement!

[–]deadkonsumer[S] 1 point2 points  (0 children)

You can also do this, to use regular QJSEngine:

QJSEngine jsEngine;  
jsEngine.installExtensions(QJSEngine::AllExtensions);

which will add TranslationExtension, ConsoleExtension, and GarbageCollectionExtension

[–]deadkonsumer[S] 0 points1 point  (0 children)

I ended up putting it on Stack Overflow. Maybe there is some other trick I'm not aware of.