gen_js_api

repository·master·Indexed 41 days ago

https://github.com/lexifi/gen_js_api

A tool for creating OCaml bindings to JavaScript libraries, primarily targeting the js_of_ocaml ecosystem. It automates value translation and calling convention management by generating implementation code from annotated OCaml signatures (.mli) or by acting as a ppx preprocessor on implementation files.

Tokens
8.3K
Snippets
27
Records
34
Agent score
52%

What's inside gen_js_api

  1. Overview of gen_js_api

    master

    gen_js_api is a tool designed to simplify the creation of OCaml bindings for JavaScript libraries. It is primarily intended for use with the js_of_ocaml compiler, though it is designed with the possibility of supporting other runtimes (like Bucklescript or embedded JS engines) in the future.

    Core Principles

    • Signature-driven generation: Authors write OCaml signatures (.mli) for JavaScript libraries, and the tool generates the implementation code using implicit conventions and explicit annotations.
    • Automatic translation: The generated code handles the translation of values between OCaml and JavaScript and manages JavaScript calling conventions.
    • Standard OCaml client code: All syntactic processing is handled during the binding generation phase. This ensures that the final client code is standard OCaml and does not require custom syntax or JS-specific types.
  2. How class wrapping works in gen_js_api

    master

    gen_js_api provides two ways to handle JavaScript objects in OCaml:

    1. Opaque Bindings (Abstract Types): You bind JS objects to OCaml abstract types with associated functions (getters, setters, and method calls). This is highly efficient because the OCaml values are just the underlying Ojs.t handles with no mapping overhead. This approach allows you to spread methods of a single JS class across multiple OCaml modules, which is useful for mimicking JS library add-ins that extend prototypes.

    2. Class Wrapping (OCaml Objects): You wrap JS objects into actual OCaml objects. This allows you to use standard OCaml OO syntax and leverage inheritance to mimic the JS hierarchy. This approach has a small runtime overhead and requires all methods to be defined within the class declaration at once.

  3. Configure JavaScript access paths with [@js.scope]

    master

    The [@js.scope "path"] attribute on a module changes the reference to the global object by following the provided property path. Nested scopes are concatenated with dots.

    Usage Patterns

    • Singleton/Class members: Bind to values inside a specific object.

      module[@js.scope "console"] Console: sig
        val log: string -> unit [@@js.global]
      end

      This is equivalent to console.log.

    • Nested Scopes: Use tuple notation [@js.scope (s1, ..., sn)] to define nested paths. This is equivalent to applying [@js.scope sn]...[@js.scope s1].

    • Module Name Inference: When attached directly to a module, the payload may be omitted; it is implicitly filled with the module name (preserving capitalization).

    module NestedScope0 : sig
      val f: string -> unit [@@js.global "outer.inner.f"]
    end
    
    module [@js.scope ("outer", "inner")] NestedScope1 : sig
      val f: string -> unit [@@js.global]
    end
    
    module NestedScope2 : sig
      val f: string -> unit [@@js.global]
    end [@js.scope "inner"] [@js.scope "outer"]
  4. Use first-class modules for polymorphic JS/OCaml conversion

    master

    To enforce JS/OCaml value conversion on polymorphic functions, use first-class modules annotated with [@js]. This tells the generator to use the module's conversion functions rather than passing the module itself to JS.

    Requirements:

    1. Annotation: Modules must be annotated with [@js].
    2. Ordering: The first-class module must appear before any other normal types in the function signature.
    3. Form: To convert a type variable 'x, it must be in the form (module[@js] Ojs.T with type t = 'x).
    4. Location: They can only be used within value bindings, not type aliases.

    Example:

    module[@js.scope "Person"] Person : sig
      type t
      val t_to_js: t -> Ojs.t
      val t_of_js: Ojs.t -> t
      val create: string -> t [@@js.create]
    end
    
    let p = Person.create "Foo";;
    Console.log (module Person) p;; (* Person { name: 'Foo' } *)

    Built-in Modules:

    Use Ojs modules for standard types:

    • module Ojs.String
    • module Ojs.Int
    • module Ojs.List(Ojs.String)
    module[@js.scope "console"] Console: sig
      val log: (module[@js] Ojs.T with type t = 'a) -> 'a -> unit [@@js.global]
    end
    
    (* Usage *)
    Console.log (module Ojs.String) "hello, world!";;
  5. Understand JS-able types in gen_js_api

    master

    A JS-able type is an OCaml type that can be automatically mapped to and from JavaScript objects.

    Supported Types:

    • Basic built-ins: string, int, bool, float, and Ojs.t.
    • Tuples: Mapped to JS arrays.
    • Sequences: array and list are both mapped to JS arrays (indexed 0..length-1).
    • Options: option types map to the underlying type. None maps to JS null; both null and undefined map back to None.
    • Polymorphic Variants: Used for enums, unions, and discriminated unions.
    • Type Variables: Free variables like 'a involve no runtime mapping (identity coercion).

    Manual Mapping

    You can make an arbitrary non-parametrized type M.t JS-able by providing two functions in module M:

    • t_to_js: t -> Ojs.t
    • t_of_js: Ojs.t -> t

    For parametrized types 'a t, the mapping functions must accept a mapper for the parameter:

    • t_to_js: ('a -> Ojs.t) -> 'a t -> Ojs.t
    • t_of_js: (Ojs.t -> 'a) -> Ojs.t -> 'a t
  6. How type variables and unsafe coercion work

    master

    Unbound type variables (e.g., 'a) are processed via unsafe coercion to and from Ojs.t. This means the identity function is used during conversion.

    Warning: Use with Caution

    This approach should not be used for JS functions that expect the values to be converted to specific JS types. For example, a JsArray.t containing OCaml strings will not work with Array.join because the elements are OCaml string representations, not JS strings.

    Safe Alternatives:

    1. Functors: Pass the conversion logic via a functor argument to enforce value conversion.
    2. First-class modules: Use module[@js.scope "..."] to provide safe bindings.
    3. Manual bindings: Use the low-level functions in the Ojs module.
    (* Unsafe approach: elements are not converted to JS types *) 
    module JsArray : sig
      type 'a t = private Ojs.t
      val create: int -> 'a t [@@js.new "Array"]
      val push: 'a t -> 'a -> unit [@@js.call]
    end
    
    (* Safe approach: uses a functor to enforce conversion *) 
    module JsArray (E: Ojs.T): sig
      type t
      val t_to_js: t -> Ojs.t
      val t_of_js: Ojs.t -> t
      val create: unit -> t [@@js.new "Array"]
      val push: t -> E.t -> unit [@@js.call]
    end
  7. Supported declarations in .mli interfaces

    master

    When writing .mli files for gen_js_api, the following declarations are supported:

    • Type declarations: type t = ... produces an identical definition in the .ml file, along with associated *_to_js and *_of_js functions.
    • Value declarations: val f: tyexpr produces a value definition based on the name, type, and any attached [@@js.xxx] attributes.
    • Sub-modules: module M : sig ... end generates a corresponding module M = struct ... end in the implementation.
    • Module aliases: module M = <Module Path> is reflected directly in the implementation.
    • Module inclusion: Using include (module type of M) in the interface includes module M in the generated implementation.
    • Class declarations: Supported via specific syntax (see CLASSES.md).
  8. How to use gen_js_api

    master

    gen_js_api provides two complementary workflows depending on your needs:

    1. Generating .ml implementations from annotated .mli interfaces: Use this method to create code for stub libraries. This is suitable for creating reusable, distributed bindings.
    2. Using as a ppx preprocessor on implementations: Use this method to define local bindings directly within your implementation files. This is useful for quick, ad-hoc, or highly specific bindings.
  9. Expose Node.js modules as runtime primitives

    master

    To bind Node.js libraries that use require(...), you must first expose them as js_of_ocaml runtime primitives.

    1. Create a JavaScript file (e.g., imports.js) that requires the desired modules.
    2. Use the //Provides: <name> comment above each variable to register it with the js_of_ocaml linker.
    3. If targeting WebAssembly, create a .wat shim to allow wasm_of_ocaml to import these primitives.
    4. Include these files in your dune stanza under both js_of_ocaml and wasm_of_ocaml sections.
    // runtime_primitives/imports.js
    'use strict';
    
    //Provides: node_path
    var node_path = require('path');
    
    //Provides: node_fs
    var node_fs = require('fs');
    
    //Provides: node_version
    var node_version = require('process').version;
    
    //Provides: node_console
    var node_console = console.log;
    ;; runtime_primitives/imports.wat
    (global (export "_node_path") (import "js" "node_path") anyref)
    (global (export "_node_fs") (import "js" "node_fs") anyref)
    (global (export "_node_version") (import "js" "node_version") anyref)
    (global (export "_node_console") (import "js" "node_console") anyref)
    ; runtime_primitives/dune
    (rule
     (targets bindings.ml)
     (deps bindings.mli)
     (action (run gen_js_api %{deps})))
    
    (executable
     (name example)
     (libraries ojs)
     (preprocess (pps gen_js_api.ppx))
     (modes js wasm)
     (js_of_ocaml (javascript_files imports.js))
     (wasm_of_ocaml (javascript_files imports.js imports.wat)))
  10. Use gen_js_api with Dune

    master

    When using Dune, you can use gen_js_api for two main purposes: generating .ml files from .mli files using the standalone tool, or compiling bindings and user code that utilize the Ojs module or the gen_js_api.ppx processor.

    To generate JavaScript, build the *.bc.js target.

    ;; Rule to invoke the standalone tool (.mli -> .ml generator)
    (rule
      (targets my_unit.ml)
      (deps my_unit.mli)
      (action (run %{bin:gen_js_api} %{deps})))
    
    ;; Executable configuration using Ojs and the gen_js_api ppx
    (executables
      (names test_jquery)
      (js_of_ocaml)
      (libraries ojs js_of_ocaml)
      (preprocess (pps gen_js_api.ppx))
      (modes byte))

    To compile to JavaScript:

    dune build test_jquery.bc.js
  11. Generate OCaml implementations from .mli interfaces

    master

    The primary use case for gen_js_api is to automatically generate .ml implementation files from annotated .mli interfaces. These interfaces define which JavaScript components to import and how they map to OCaml types and values.

    You can run the tool directly via the CLI or through ocamlfind.

    # Direct usage
    $ gen_js_api my_module.mli
    
    # Using ocamlfind
    $ ocamlfind gen_js_api/gen_js_api my_module.mli