Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Interop medium/WASM Compilation #6

Open
CJBuchel opened this issue Sep 2, 2023 · 5 comments
Open

Interop medium/WASM Compilation #6

CJBuchel opened this issue Sep 2, 2023 · 5 comments

Comments

@CJBuchel
Copy link
Contributor

CJBuchel commented Sep 2, 2023

Problem
The current library setup is perfect for Typescript/Javascript based UI's and Websites.
But an issue occurs if the calculator needs to be ported into a different framework which doesn't support native interop from typescript. For example Dart which I've been trialling for the last year.

This means that a user would need to manually rewrite the calculator for each unsupported framework, or would need to build the framework using a shoddy unknown interop system which is not ideal. It proves to be challenging especially when making API calls to the First Aus server which relies on the data being being the same as the javascript structure.

Possible Solution
Typescript isn't really built for being a medium language to be ported from. Normally that is handled by a more generic language which can then be ported like so medium->typescript->javascript.
One possible solution I've been thinking of is to switch the core section of the calculator (the section which handles the actual score add up and contains the data types) to a medium which can be compiled into different languages and formats. My first initial thought was an XML file or json format which could then be assembled into the javascript library or the dart library.

But a more coherent solution is to use a middle ground language and compile to WASM so each framework can work off the same data structures and logic without compromise. The most common native languages used to compile to WASM is Rust, C and C++.

For example in a Rust based system the following could be used

#[no_mangle]
pub extern "C" fn add(a: i32, b: i32) -> i32 {
    return a + b;
}

In typescript/js

async function loadWasmModule() {
    const wasmModule = await import('./path_to_generated_wasm.js');
    console.log(wasmModule.add(1, 2)); // 3
}
loadWasmModule();

In Dart

import 'package:wasmer/wasmer.dart';

void main() async {
    final instance = await Instance.open('path_to_generated_wasm.wasm');
    final add = instance.exports['add'] as Function;
    print(add(1, 2)); // 3
}

The benefit here is that it allows for the same level of logic and data structures to be used across multiple platforms, and not just js or dart. But many more if we choose to allow another library to be published.

In our new structure we would add the new years game of data structures and calculations in rust, c or cpp and then compile it to WASM before making any platform specific changes to the endpoint library (like a switch around in imports). Then publish normally.

native medium -> wasm compile -> wasm interop -> native library -> publish

@CJBuchel
Copy link
Contributor Author

CJBuchel commented Sep 2, 2023

@fwestling what do you think about this?

It's a bit overkill from what the original intended idea of the library was. But it does provide some benefit and future proofs a common ground calculator for any current and future platforms.

If you like the idea I can try and build a simplistic switch over on a different branch to trial the concept.

@fwestling
Copy link
Collaborator

I could see the benefits in having it be a little more interop, my main concern would be in making sure the calculator itself is easy enough to write each year - setting it up for the 2023 season took only an hour or two because TS is pretty user friendly.

I'm also not super experienced with importing/compiling wasm content - e.g. could we get types out of it etc? I'd be happy for you to put together a little proof of concept, maybe translating just enough of the core functionality and one season's definition so I can see what importing it is like

@CJBuchel
Copy link
Contributor Author

CJBuchel commented Sep 2, 2023

I'm also not very experienced and haven't really tried WASM compilation before, but from what I've seen both data structures and logic can be put into WASM which is why it caught my eye. But I wouldn't accurately say if it's simple or not when adding a game each year.

I can start building a little test to see what it would look like and if it's viable. But my thinking is similar, only the core data types and calculator logic really would need to be made interop. The imports and the library specific requirements are rarely touched when adding a season. And don't prove to be much of an issue.

If it's too complicated to write the additional season in the medium then I'll probably scratch the idea.
In reality the only part that I'm looking at is this

const questions: Score[] = [
  {
    id: 'm00a',
    label:
      'All team equipment fits in one launch area and under 12 in. (305 mm)?',
    labelShort: 'Inspection?',
    options: ['No', 'Yes'],
    defaultValue: 'No',
    type: 'categorical',
  },
  {
    id: 'm01a',
    label:
      "If the 3D cinema's small red beam is completely to the right of the black frame",
    labelShort: 'Beam right?',
    options: ['No', 'Yes'],
    defaultValue: 'No',
    type: 'categorical',
  },
  ...
}

It's such a simple and effective format that only the accompanying calculator logic is really the only thing which is platform dependent. If I could separate the mission structures to just pure json files which could be added into any framework, and then build in the calculator logic in a similar medium like fashion then that would be perfect. Sadly I can't think of anything off the top of my head which would provide scoring logic in a similar medium format as the mission structures.

But I'll give my little idea a test and see if there is a decent way to do it.

@CJBuchel
Copy link
Contributor Author

CJBuchel commented Sep 4, 2023

I'm going to leave this problem on hold for a bit.

I've started a small concept using rust as the native language to compile to wasm (using the 2022 game) https://github.com/CJBuchel/ausfll-score-calculator/tree/Interop

And while the theory seems to work out it does pose some problems. 2 main ones

  • effort of building a new game. It's quite similar but it doesn't have the same dynamic ability that typescript has for json format and import/export types. It's not too bad to work around it and it does provide a good solid interop for web frameworks, but it doesn't seem worth the extra effort at the moment.
  • The second issue is interoperability. Sadly for my application requirements, I've just come to realise that WASM doesn't always work out. It's great for web based systems like websites and web ui's. But it doesn't play ball very nicely with frameworks like flutter running on mobile and desktop devices natively. Flutter on web works great though.

So even if I did use wasm compiling from rust, I wouldn't be able to use it all the time and for mobile and desktop applications I would need to setup bindings to the source code anyway, likely using https://github.com/fzyzcjy/flutter_rust_bridge

Which is an excessive amount of work and defeats the purpose for the one size fits all idea.
I still like the idea of interop but more thought is probably needed to be put into it, for now I just need something that converts typescript to dart.
Which is still a problem, but I'm going to do the best thing I can do. Pretend the problem doesn't exist, and cross that bridge when I'm implementing the scoring ipads.

@CJBuchel
Copy link
Contributor Author

@fwestling

Proof of concept -> https://github.com/CJBuchel/ausfll-score-calculator/tree/interop
Just the core parts I tried separating out, and I haven't provided a ts or dart library that contains the specifics for each platform.

Type/Structure Interop

It's a bit messy but the theory pans out, games are created each year like normal but in a native language. I've chosen rust just because it has a few niche abilities to serialize and deserialize data.

For instance the specific year code and logic can look like this https://github.com/CJBuchel/ausfll-score-calculator/blob/interop/ausfll-rs/schema-utils/src/schemas/games/fll_2023.rs

Sadly rust isn't a big fan of having structures and types that aren't named so every Score type must be a aptly named score, and every question_input is an enum which is either a string options array or a number depending on the type of question. Rust also requires strings to be formatted correctly. So in total both the questions and missions definition come out to be longer than if it was done in ts.
As for the scoring and validation functions, they're quite similar with minimal differences.

In the case of interop I couldn't find a decent solution for a one shoe fits all, wasm is nice but only works on the web. And bindings are great except usually they only work for functions, and low level structures. Not to mention they're an absolute pain to work with.

In the end I figured out that JSON Schemas work rather well for serializing the type data and structures into a simple json format. And because it's standardised most high level languages like, dart, ts, js, python, rust etc... can both serialize and deserialize data using the schema as a reference.

For instance the rust project I have generates 3 json files from rust code and puts it into a schema directory which can be used in other projects https://github.com/CJBuchel/ausfll-score-calculator/tree/interop/schema.
For the proof of concept I also made it automatically convert those schema files from json to both ts and dart using quicktype. But a wide variety of languages support the conversion. When it get's converted into a ts file for instance, it comes with all the structures as interfaces exported as modules which can be accessed. But it also provides a serialize and deserialize set of functions to convert json to the interfaces and vice versa.

2 of the 3 json files are schemas, the 3rd is just a bunch of data containing season specific information.

1

The ausfll_base_schema.json contains the definitions of all the base types needed for the library, ScoreError, ScoreAnswer, QuestionInput etc... These are generated from the following rust code https://github.com/CJBuchel/ausfll-score-calculator/blob/interop/ausfll-rs/schema-utils/src/schemas/base_types.rs,

2

The ausfll_game_schema.json just defines the structure of what a game is i.e

#[derive(JsonSchema, Deserialize, Serialize, Clone)]
pub struct Game {
  pub name: String,
  pub program: String,
  pub missions: Vec<Mission>,
  pub questions: Vec<Score>,
}

I separated it out from the base types specifically so it could be easily mapped from a serialized json file into that structure from every language. For instance a json file containing the name, program, missions and questions for a season can easily be mapped using the schema.

3

For instance the 3rd json ausfll_games.json is not actually a schema that's generated, it's a json map of each season game, although this one only contains the one game for 2023 as the proof of concept.
So for a user wanted the 2023 game questions and missions, they can import the schema file (i.e the converted ausfll_game_schema.json to ausfll_game_schema.ts) and use that to deserialize the ausfll_games.json to an actual set of data.

An working example of this is inside the test ts project. https://github.com/CJBuchel/ausfll-score-calculator/tree/interop/test_ts_project

import { Convert, AusfllGameSchema } from "./schema/ausfll_game_schema";
import { ScoreAnswer, ScoreError } from "./schema/ausfll_base_schema";
import jsonData from "./schema/ausfll_games.json";

const rawGameData = JSON.parse(jsonData);
const masterpiece = rawGameData["2023"]; // get the 2023 game from the file

try {
  const games_data: AusfllGameSchema = Convert.toAusfllGameSchema(JSON.stringify(masterpiece));

  console.log("Displaying Question 1");
  console.log(games_data.questions.find((question) => question.id === "m14a"));
  console.log(games_data.questions.find((question) => question.id === "m14b"));

} catch (error) {
  console.error(error);
}

The above project parsed the ausfll_game.json, filtered out the 2023 game, then deserialized the structure using the AusfllGameSchema, which provides the proper typescript structure of a season game

export interface AusfllGameSchema {
    missions:  Mission[];
    name:      string;
    program:   string;
    questions: Score[];
    [property: string]: any;
}

And the process is pretty much the same for most languages, if they support json parsing and there is a package to convert the schema to their native language (i.e for npm i use quicktype). Then it can pretty much be a one stop solution for all types and structures that need to be shared across multiple languages.

In theory rust doesn't even need to be the base language for creating the structures, If I recall typescript can also do it to an extent. And all of it can be serialized into a json schema, and the process is the same afterwards.

Logic/Functional Interop

The type and structure interop is perfectly fine and it's the same technique that I've already been using to share type and structure code between my current FLL Management System, so both the server (in rust) and client (in flutter) can use the same code when sending and receiving request data over the network.

What isn't so fun is logical interop, there is no good way to provide multi language support for the functions validate(), and score(). There were a few different routes I did try, one was using a rule engine. Where the logic was done on the same level as the question definition so it could be serialized in json and a rule engine written in the required platform language. Like rust, cpp, java, ts, dart, js etc.. would interpret those into a format which can generate the "equivalent" of a validate and score function. But, it's complex, causes too many unknown issues and while sounds cool because everything about it would be interop (if you built a rule engine to accompany every platform), it doesn't really seem like it's worth the effort. And it starts to breach on the same territory the old TMS software used with those massive XML files https://github.com/FirstLegoLeague/scoresheet-builder/blob/master/challenges/xml/2019_US.xml, personally I wasn't a big fan of 1000+ line game logic every year XD.

Another thought I had was building a code generator in rust which simply just converts the function into actual ts and dart as needed. Which honestly doesn't sound as dumb as it seems, rust has quite the ability for this using macros and really you just need to switch out i32's to int and Vec<> to List<> or [] for ts. Not much would need to be programmed for it too because the validate and score functions don't really change year to year. Comparative, iteration and so on. They're very similar each each with slight variations depending on the questions. The main problem with it is rust has no way to determine if the function is correct or not even in it's own language. So only in production/testing the logic would you be able to tell if it's worked or not.

The final solution and the one I ended up using anyway which is by far the simplest but sadly not cross compatible for every project is wasm. The rust project I have generates both the schemas in json, dart and ts as well as generates the wasm file in a folder named ausfll_wasm_pkg. There are a few ways to compile a wasm project, in my case I did it just for node/ts. So along with the .wasm file, it also creates the .ts and .d.ts files for deceleration of the functions. It also produces a package.json and can be installed as a local dependency for npm projects which is pretty neat.

For instance the full test project I used both the schema files for types as well as the wasm package for logic worked quite well and was surprisingly efficient. The full code needed to test the generated interop code is pretty short. I.e

import { Convert, AusfllGameSchema } from "./schema/ausfll_game_schema";
import { ScoreAnswer, ScoreError } from "./schema/ausfll_base_schema";
import jsonData from "./schema/ausfll_games.json";

// import wasm
import * as wasm from "ausfll-wasm";

const rawGameData = JSON.parse(jsonData);
const masterpiece = rawGameData["2023"]; // get the 2023 game from the file

try {
  const games_data: AusfllGameSchema = Convert.toAusfllGameSchema(JSON.stringify(masterpiece));

  console.log("Displaying Question 1");
  console.log(games_data.questions.find((question) => question.id === "m14a"));
  console.log(games_data.questions.find((question) => question.id === "m14b"));
  
  const answers: ScoreAnswer[] = [
    {
      id: "m14a",
      answer: "1",
    },
    {
      id: "m14b",
      answer: "0",
    }
  ];

  // test wasm compiled for nodejs
  let errors: ScoreError[] = wasm.wasm_masterpiece_validate(answers);
  let score: number = wasm.wasm_masterpiece_score(answers);
  
  console.log("Validating answers");
  for (const error of errors) {
    console.log("Validation Error: " + error.id + ", m: " + error.message);
  }
  
  console.log("Scoring Answers: " + score);

} catch (error) {
  console.error(error);
}

And that worked really well, the only caveat being that wasm which I completely forgot about stands for Web Assembly. So it's great, if you're compiling for any web based project or node based project, i.e php, js, ts, even dart in some cases. But anything native it doesn't like so much. Except for electron/react native i believe. Most projects that are desktop based or mobile based don't support wasm. So it's almost fully cross compatible, but not quite.

Still quite decent though.
Personally for me it doesn't matter too much, because the native code i wrote it in was rust. So I can just have the score calculations server side, and it's not too much of an issue. But dart is also a fairly new language, but possibly over time there are better ways for logical interop.

The next best option is using proper bindings for each platform, which is likely the proper way to do it, i.e in flutters case https://github.com/fzyzcjy/flutter_rust_bridge.

But if we are interested in interop, using schemas for all the core structures and wasm for logic is probably the best bet if needed. I at the very least will continue to build up the interop version over time just because I'm currently using rust and flutter for the foreseeable future, and I'll need something that can be pushed relatively easily to other platforms.

But I'm curious on your thoughts if you have any.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants