Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Calling JavaScript instance functions from Lua does not respect the JS function's scope #12

Closed
TannerRogalsky opened this issue Apr 2, 2014 · 5 comments

Comments

@TannerRogalsky
Copy link

When calling an instance of a JS prototype's function from Lua, this always refers to Window when it should instead refer to the instance.

Consider this code:

var employee = function(name) {
  this.name = name;
}

employee.prototype.say_name = function(){
  console.log(this.name);
}

var fred = new employee("fred");

When calling fred.say_name(); from JS, this refers to the fred object.
When calling fred.say_name() from Lua, this refers to Window.

I believe this to be because of this line setting the first argument to apply to be null. It's not obvious to me how to fix this because the instance does not seem to be in scope when executing the instruction.

Perhaps the caller needs to be closed over when creating the instruction? I'm a bit out of my depth here.

I've created a small branch to showcase this issue in action if it helps: https://github.com/TannerRogalsky/moonshine-love2d/tree/prototype_scope_bug

@josedonizetti
Copy link

+1

@paulcuth
Copy link
Contributor

paulcuth commented Apr 3, 2014

This is a tricky one, but I believe that Moonshine is acting as it should in this situation.

In JavaScript, functions are passed around by value and the context is only applied at the point at which it is called. So, the function value is passed into the Lua environment and passed around the Lua environment by value too. The function is then called in Lua, but Lua does not have the same concept of context.

In Lua, context is applied using the colon operator, fred:say_name(), which is simply syntactic sugar for fred.say_name(fred).

So, there are two solutions to this situation. Neither are perfect, but you can choose which you think is best.

  1. Bind all methods on an object to the instance.
var employee = function (name) {
  this.name = name;
  this.say_name = this.constructor.prototype.say_name.bind(this);
}

and in Lua:

fred.say_name()
  1. Support the Lua way of passing context.
employee.prototype.say_name = function (self) {
  console.log(self.name);
}

and in Lua:

fred:say_name()

Personally, i've chosen the second option for the projects that I've built because that the results in Lua code that uses context correctly. I haven't needed to reuse the same objects in JavaScript, but I'm aware that could be pretty ugly. A third option could be to create a layer between the JavaScript and the VM, that handles this for you while keeping your JS objects clean.

Some other points that I've noticed along the way:

  1. This isn't a scope issue, but a context issue; the scope is maintained correctly when a JS function is called from Lua.
  2. There is a --watch (or -w) switch to moonshine distil that will observe a file/path and reapply the action when a file changes, eg: $ moonshine distil *.lua -w. See $ moonshine help distil for more.
  3. Many thanks for integrating Moonshine with Love2d. It's something that has been on my list for a long time, but sadly it never made it to the top.

If you need any more help, please shout.

@TannerRogalsky
Copy link
Author

I thought I'd follow up on this a little. I understand your perspective even though I think it's unfortunate that it precludes seamless interop with most JS libraries. The solution that I've come to is to use a form of method binding but doing it implicitly via the "fat arrow" notation in CoffeeScript.

https://github.com/TannerRogalsky/moonshine-love2d/blob/master/coffeescript/love.js/graphics.coffee#L38

rectangle: (mode, x, y, width, height) =>
  switch mode
    when "fill" then @ctx.fillRect(x, y, width, height)
    when "line" then @ctx.strokeRect(x, y, width, height)

becomes

https://github.com/TannerRogalsky/moonshine-love2d/blob/master/js/love.js#L55

var __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };

function Graphics() {
  this.rectangle = __bind(this.rectangle, this);
  // ...
}

Graphics.prototype.rectangle = function(mode, x, y, width, height) {
  switch (mode) {
    case "fill":
      return this.ctx.fillRect(x, y, width, height);
    case "line":
      return this.ctx.strokeRect(x, y, width, height);
  }
};

I've found this to be an easy and reasonable way to bridge the existing gap for my purposes.

@paulcuth
Copy link
Contributor

Yep, the CoffeeScript __bind function is the same as Function.prototype.bind in modern browsers.

Keep me posted with your progress and shout if you need help. I look forward to seeing the result.

@paulcuth
Copy link
Contributor

It's also worth looking at the implementation of the DOMAPI extension for interop between JS and Lua. There is a drawback that you are not able to iterate over JS object properties in Lua using pairs(), but context is taken care of quite nicely.

See DOMAPI demo.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants