Generatlised Ports

Hey y’all!

I had a bit of free time and I create this small project that exposes just two ports that allow running arbitrary javascript code in a “safe” way. By safe I mean that ports do not crash the app, but not safe from information security perspective.

I suggest a similar yet more elaborate to support small scale javascript FFI. It should be enough to do the following:

  • Invoke methods accessible from traversing the window object, such as document.scrollTo or globals exposed by a library. My implementation also supports functions that return a value, and functions that return a promise.
  • Register and unregister event listeners on various objects, for example to access future APIs like web-bluetooth or the audio API.
  • It could also be possible to use it to handle a small amount of persistent objects.

What I hope to achieve is to continue the discussion on how we handle FFI and how to make more accessible to newcomers on the one hand, while still keep it safe on the other hand. Also, I hope that this small experiment will allow us to rethink how to integrate Javascript only API with Elm applications.

The code is accessible via github. It uses one incoming and one outgoing port, both pass JSON values and are “fronted” by functions that handle the decoding and encoding.

type ForeignFunctionInvocation
    = Call { id : FfiId, cmd : List String, args : List JE.Value }
   -- ... truncated non-supported stuff
    | ListenOn { target : FfiId, id : FfiId, cmd : List String, args : List JE.Value }

ForeignFunctionInvocation represents the invocation we want to execute on the JS side. It uses and “id” field that would be attached to the result object (if any) to allow us to correlate invocation and result.

type FfiResult a
    = Success FfiId a
    | SuccessNoValue FfiId
    | NotFound FfiId (List String)
    | ExceptionThrown FfiId { message : String, cmd : List String, args : List JE.Value }
    | DecodingFailed FfiId JD.Error

foreignResult : JD.Decoder a -> (FfiResult a -> msg) -> Sub msg

foreignResult subscribes to the results of an FFI. It is supplied with a decoder to decode the result to Elm value and with a message constructor. FfiResult can be a success (whether synchronous or not), a success without a return value (doesn’t work ATM), NotFound to indicate that the requested function does not exist, an exception if an exception is thrown or a decoder error if the result can not be decoded. I think this should cover all possible results and ensure that code in the JS side doesn’t crash our app.

The demo I linked supports two things:

  1. Invoking a single argument function accessible from the window object, or
  2. Registering an event listener on any object accessible from the window object (poorly tested).

I think this can lead to “lightweight” ports - still safe, but require less work handle, and less concepts to understand. For example, using a port can be as simple as:
JS:

window.doSomething = function(arg) {
  // Do something effectual
  return result
}

Elm:

update msg model =
  case msg of
    -- ...
       (model, invokeFFI "doSomething" "result-name" [Json.Encode.string "arg1"])

subscriptions _ =
  subscribeFFI "result-name" resultDecoder GotResult

Thoughts?

3 Likes

This topic was automatically closed 10 days after the last reply. New replies are no longer allowed.