tweag / asterius

DEPRECATED in favor of ghc wasm backend, see https://www.tweag.io/blog/2022-11-22-wasm-backend-merged-in-ghc
1.98k stars 55 forks source link

Fixes for the FFI blog post #899

Open gelisam opened 2 years ago

gelisam commented 2 years ago

I found a number of issues in the FFI blog post. I am guessing it will be easier to fix them if I just enumerate them in the order in which they appear, rather than following the strict "Describe the bug / To Reproduce / Expected Behaviour" template. Please let me know if you'd prefer separate tickets and proposed fixes following that template.

  1. The blog post refers to the type JSRef several times, but I believe this type has since been renamed to JSVal.

  2. Missing import in the "Haskell calling JavaScript" code snippet:

    Here is what Asterius lets you do today:

    import Control.Monad
    
    [...]
    foreign import javascript "new Date()" js_current_time :: IO JSRef
    [...]

    If you would like to try this at home, put the above snippet in a file called jsffi.hs in the current directory.

    However, if we follow these instructions and replace the JSRef with JSVal, we get the error Not in scope: type constructor or class ‘JSVal’, because the code snippet is missing the line import Asterius.Types.

  3. First broken link in the "Haskell calling JavaScript" section:

    Then, you can invoke ahc-link to compile it to .wasm/.js files (using the pre-built Asterius Docker images, as explained in the Asterius documentation): [...]

    "Asterius documentation" links to https://tweag.github.io/asterius/, which 404's. Should probably link to https://asterius.netlify.app/images.html instead.

  4. Second broken link in the "Haskell calling JavaScript" section:

    Our current implementation supports a variety of primitive types, such as, Bool, Char, and Int as argument and result types of imported functions. See the reference documentation for a full list.

    "reference documentation" links to https://tweag.github.io/asterius/jsffi/, which 404's. Should probably link to https://asterius.netlify.app/jsffi.html#directly-marshalable-value-types instead.

  5. If the previous link is indeed changed to https://asterius.netlify.app/jsffi.html#directly-marshalable-value-types, then this introduces a new issue. The text surrounding the link promises that the linked documentation provides the full list of supported primitive types, but https://asterius.netlify.app/jsffi.html#directly-marshalable-value-types is also limited to a partial list:

    Regular Haskell value types like Int, Ptr, StablePtr, etc.

    An easy fix would be to change the text to see "See the reference documentation for more information" instead of "for a full list", but a much better fix would be to change https://asterius.netlify.app/jsffi.html#directly-marshalable-value-types to provide a full list, as it's not at all obvious what this etc. stands for.

  6. Outdated flag and code snippet in the "JavaScript calling Haskell":

    The tool ahc-link provides a flag --asterius-instance-callback=, which takes a JavaScript callback function, which is to be called once an Asterius instance has been successfully initiated. [...] Continuing with the above example, in order to call mult_hs in JavaScript, the callback that we need to supply would be:

    i => {
        i.wasmInstance.exports.hs_init();
        console.log(i.wasmInstance.exports.mult_hs(6, 7));
    }

    However, ahc-link no longer supports the --asterius-instance-callback= flag, the exported haskell functions are no longer nested inside a wasmInstance field, the hs_init call no longer seems necessary, and mult_hs now returns the Promise of an Int rather than an Int. This section of the blog post thus needs some major changes. Here is a suggested rewrite:

    By default, the tool ahc-link generates a file <HaskellFileName>.mjs which loads the generated wasm code, creates an Asterius instance named i, and calls i.exports.main() in order to run the Haskell file's main function:

    import * as rts from "./rts.mjs";
    import module from "./<HaskellFileName>.wasm.mjs";
    import req from "./<HaskellFileName>.req.mjs";
    
    module
      .then(m => rts.newAsteriusInstance(Object.assign(req, { module: m })))
      .then(i => {
        i.exports.main();
      });

    This default setup assumes that your program's entry point is your Haskell file's main function. If you instead want your program's entry point to be on the JavaScript side, you can write your own mjs file which does something different. Here is one which calls our exported mult_hs function:

    $ cat Example.hs
    module Example where
    foreign export javascript "mult_hs" (*) :: Int -> Int -> Int
    
    $ cat Example.mjs
    import * as rts from "./rts.mjs";
    import module from "./Example.wasm.mjs";
    import req from "./Example.req.mjs";
    
    module
      .then(m => rts.newAsteriusInstance(Object.assign(req, { module: m })))
      .then(i => i.exports.mult_hs(6, 7))
      .then(r => {
        console.log("6 * 7 = ", r);
      });
    
    $ ahc-link --input-hs=Example.hs --no-main --export-function=mult_hs --input-mjs=Example.mjs --run
    [...]
    6 * 7 =  42
  7. Outdated types and code snippet in "Using Haskell closures as JavaScript callbacks" section.

    The section talks about StablePtr and makeHaskellCallback, but makeHaskellCallback has been replaced with wrapper, which has a simpler API which doesn't require StablePtr. Also, the beforeExit example loops forever, because a JSFunction now returns a Promise, and scheduling a Promise inside a beforeExit handler causes the Node.js process not to exit after all, but instead to wait for that Promise to complete, at which point the beforeExit handler is called again, etc. This section of the blog post is thus also in need of some major changes. Here is a suggested rewrite:

    One limitation of the foreign export javascript approach is that it is only possible to export top-level definitions. Asterius also supports dynamically converting a Haskell function into a form which can be called from the JavaScript side. Here is a simple example:

    $ cat Example.hs
    import Asterius.Types
    
    foreign import javascript "wrapper"
      wrapDoubleToDouble :: (Double -> Double) -> IO JSFunction
    
    foreign import javascript "$1(42).then(console.log)"
      js_call_with_42_then_print :: JSFunction -> IO ()
    
    main :: IO ()
    main = do
      js_double <- wrapDoubleToDouble (*2)
      js_call_with_42_then_print js_double
    
    $ ahc-link --input-hs=Example.hs --run
    [...]
    84

    On the JavaScript side, the JSFunction can be called like a regular JavaScript function which returns a Promise. wrapper is also able to wrap functions of multiple arguments as well as IO actions.

  8. The name makeHaskellCallback is still mentioned at the end of the RTS documentation, even though the new name is now wrapper.