docs/bridging

Opal Bridging

Intro

Opal’s “bridging” makes JavaScript objects become Ruby object in Opal, boosting language interoperability and performance by using native JavaScript versions of certain Ruby classes.

Bridging Basics

Opal modifies the prototype chain of a JavaScript constructor function by injecting a Ruby class and its superclasses, enabling bridged JavaScript classes to inherit Ruby behaviors.

Prototype chain pre-bridging:

  • constructor (window.String)
    • JavaScript prototype chain (window.Object)
    • null

After bridging with ::Object:

  • constructor (window.String)
    • Ruby superclass chain (Opal.Object, Opal.Kernel, Opal.BasicObject)
    • JavaScript prototype chain (window.Object)
      • null

The Opal.bridge function is used for this, allowing the JavaScript class to become a Ruby class. You can also use a syntax like: class MyCar < `Car` which counterintuitively doesn’t mean inheritance - MyCar‘s superclass will be Object, but MyCar will be bridged into native JavaScript Car class.

The bridged JavaScript classes are usable in Ruby, but with an adapted interface: JavaScript methods accessed from Ruby use x-strings and Ruby methods accessed from JavaScript use $-prefixed method names (e.g., reduce->$reduce).

This strategy avoids type casting for bridged objects and boosts performance by utilizing native JavaScript objects.

Example:

JavaScript Car class:

class Car {
  constructor(make, model) {
    this.make = make;
    this.model = model;
  }

  getCarInfo() {
    return `${this.make} ${this.model}`;
  }
}

Bridged Ruby MyCar class:

class MyCar < `Car`
  def self.new(make, model)
    `new Car(make, model)`
  end

  def car_info
    `self.getCarInfo()`
  end
end

car = MyCar.new('Toyota', 'Corolla')
puts car.car_info  # Outputs: "Toyota Corolla"

This bridges Car to MyCar, creating a Car instance with new Car(make, model). We call getCarInfo() via self, referencing the same JavaScript and Ruby object due to bridging. Thus, creating a MyCar instance makes a Car instance, and car_info on the car object outputs "Toyota Corolla". This shows seamless Ruby-JavaScript interaction via bridging.

Inheritance & Bridging

Creating a subclass of a native JavaScript class is possible. Bridging doesn’t affect JavaScript’s behavior or inheritance hierarchy.

Bridging Considerations

Bridged Class Methods

Access JavaScript class methods as properties on the constructor with class.$$constructor, where class is a Ruby class. You can access it using self.

Performance

Bridging is quicker than wrapping as no wrapper object is used, but altering the prototype chain could affect JavaScript engine performance.

Exception Handling

JavaScript Error is bridged to Ruby Exception for unified error handling.

Native JavaScript Interactions

Use x-strings for interacting with native JavaScript. The Native class wraps JavaScript objects and offers a Ruby-like API.

Type Conversions

With bridging creating equivalent Ruby and JavaScript objects, no explicit type conversion is needed. The Native library’s #to_n is how you convert non-bridged objects.

Bridged Classes in Corelib

Opal’s core library bridges several commonly used JavaScript classes, including Array, Boolean, Number (think: Float which also may act as an Integer), Proc, Time, RegExp, and String, allowing interaction with these JavaScript instances as their Ruby equivalents. Also Hash bridges the Javascript Map class and works very well between Javascript and Opal if primitives of the Javascript types “string”, “number” and “symbol” (which must be global, generated with Symbol.for()) are used as keys. When other types or objects are used as keys, there may be issues retrieving the Map entries from each language, because the original references of the objects that have been used as keys are required for retrieval. When using a Map with object keys passed from Javascript as Hash in Opal, calling Hash#rehash may resolve issues and make entries accessible.

However, Opal doesn’t bridge common JavaScript classes like Set due to significant differences in behavior and interfaces between Ruby and JavaScript.

Drawbacks

Bridging effectively utilizes JavaScript objects within Ruby, but it does have its downsides.

The main drawback of Opal bridging is that it modifies the prototypes of JavaScript classes, potentially leading to prototype pollution. This can cause conflicts with other JavaScript code expecting the prototype in its original state, potentially causing hard-to-trace bugs or slower code execution due to changes impacting the JavaScript engine’s performance.

Additionally, bridging requires comprehensive knowledge of both Ruby and JavaScript, and understanding how Opal implements the bridge. Incorrect usage can lead to complex, hard-to-debug issues.

An alternative to bridging is using Native::Wrapper. This module in Opal’s standard library allows interaction with JavaScript objects without modifying their prototypes. It offers a Ruby-friendly API for accessing and calling JavaScript methods, as well as handling JavaScript properties, thus avoiding the prototype pollution issue.

In conclusion, bridging provides powerful functionality for Ruby-JavaScript code interaction, but it should be used cautiously and with a deep understanding of its implications. Depending on your needs, Native::Wrapper may be a safer and more intuitive alternative.

Conclusion

Opal’s bridging mechanism offers an enticing method for combining Ruby and JavaScript’s strengths into a single environment. Despite requiring caution due to potential drawbacks, bridging enables developers to create Ruby idiomatic APIs for accessing JavaScript code. By leveraging this mechanism, we can redefine conventional web programming paradigms and create more vibrant, dynamic applications.