2

I came across this strange behavior while using javascript code modules for a firefox addon. I'm not sure if this is a bug or a bad design doing mutual imports.

So let's say there are 3 modules a, b and c.

a.js

var EXPORTED_SYMBOLS = ["a"];                                                       
Components.utils.import("resource://mymodule/c.js");                                 
Components.utils.import("resource://mymodule/b.js");                                 

var a = {                                                                           
    init: function() {                                                              
        dump("A init\n");                                                           
        b.init();                                                                   
    }                                                                               
};     

b.js

var EXPORTED_SYMBOLS = ["b"];
Components.utils.import("resource://mymodule/c.js");

var b = {
    init : function() {
        try {
            dump("B init\n");
            dump(c.foo() + "\n");
        } catch (e) {
            dump("Error C init : " + e.message + "\n");
        }
    }
};

c.js

var EXPORTED_SYMBOLS = ["c"];
Components.utils.import("resource://mymodule/b.js");

var c = {
    foo : function() {
        return "C Foo";
    },
};

a.init() is called externally. Now with the above code, I hit an undefined for 'c' from b.

A init
B init
Error C init : c is undefined

After some troubleshooting, I realized that to correct this,

  • I can either swap the imports inside a.js (b is imported before c)
  • Or I can remove the mutual import ( remove the import of b from within c) With either of these, things go fine.

In my actual code b and c represent some UI related stuff and they have mutual dependencies. I can totally get rid of a mutual import of modules, and register a callback function for one of them. But I wish to know what is causing this behavior. As far as I understand the documentation do not have any strict guidelines for import among modules. I am also aware that one module when imported multiple times will be shared because of caching of modules. But somehow can't explain this. What am I doing wrong here ?

Alavalathi
  • 713
  • 2
  • 9
  • 21

1 Answers1

1

I have a theory about what is happening, though I haven't tried running the code in a debugger to make sure:

  • a.js runs and imports c.js
  • c.js runs and imports b.js (before c is defined!)
  • b.js runs and imports c.js. But c.js does not run again, because it has already been imported once before.
  • Since c is still undefined in the c.js scope (because c.js has not continued running yet; it is still waiting for the import call on line 2 to return), c = undefined is injected into the b.js scope.
  • b.js finishes executing.
  • c.js finishes executing.
  • a.js finishes executing.

So b.js never receives a valid binding for c, and b.init() throws an exception when it tries to access c.foo.

If this theory is correct, I think you could fix the error by moving the import calls in b.js and c.js to the bottoms of those files.

mbrubeck
  • 564
  • 3
  • 7
  • Thanks. Looks like your theory is correct. If I move the imports to the bottom, c is defined. However, 2 things which I'm still not clear - also makes me wonder if this is a bug with imports. 1. MDN says, "a given module will be shared when imported multiple times. Any modifications to data, objects, or functions will be available in any scope" - I can access 'c' from 'a', before or after a b.init(). If the modules are shared, shouldn't that be available for b too ? 2. A function in 'c.js' external to 'var c' is accessible from b! Do you think variables and functions are handled separately? – Alavalathi Feb 12 '14 at 10:10
  • The *objects* exported from a JSM are shared by all code that imports them, but *variable bindings* are not shared. In your example, you can think of `Components.utils.import("b.js")` as a shorthand for `let b = Components.utils.import("b.js", {}).b;`. It creates a binding called `b` in a local scope. If `b` points to a mutable object then you can use that object to share state across modules. But in your original test case, no object had actually been exported. – mbrubeck Feb 12 '14 at 16:12
  • Another example: if c.js contained `let c = {foo: 1}; import("b.js"); c = {foo: 2}; import("d.js");` then b.js and d.js could end up importing different objects. They would each be able to modify those objects, but they wouldn't see *each other's* modifications. (c.js would be able to see both files' modifications, if it held onto a reference to each object.) – mbrubeck Feb 12 '14 at 16:20
  • Sorry, one more example to show what I mean by "variable bindings are not shared." If you do `import("c.js"); c.x = 1;` then you are modifying the shared object and your modification will be visible in other scopes. But if you do `import("c.js"); c = 1;` then you are rebinding the *local* `c` variable to a different value, and this will have no effect in other scopes. – mbrubeck Feb 12 '14 at 17:23
  • Yes, got it. But modifying variables wasn't a concern for me. Also, variable modification behavior is documented at MDN wiki. – Alavalathi Feb 12 '14 at 18:51
  • In my test case, the reason for confusion was when I have an external function, say `cfunc()` in module `c.js` but outside variable `c`, it was accessible from `b`. The way I understand the whole thing is 1. Your explanation of order of imports 2. Function and variable symbols are handled differently - ie. function symbols are shared across all scopes with the first import, but variables have a 'stricter' import criteria, possibly as per the order of imports. Any ways, thanks for all your inputs. I will mark this as solved. – Alavalathi Feb 12 '14 at 18:51