Closed lax1dude closed 1 month ago
It's a known issue. Asyncs in WebAssembly BE de facto does not work. They should be completely redesigned and re-written, the way how they are written right now is completely wrong. However, I never had enough time and motivation to do that, so you better give up. Also, I believe it's not that easy to get rid of asyncs in your code base, as you did it with JSO. You can assume that I'll never fix this issue in Wasm BE ever, sorry.
Anyway, right now I'm working on Wasm GC BE, which relies on Wasm GC proposal, currently supported in Chrome and Firefox, you can learn which versions here. I also suggest you to use Chrome dev tools to profile your app, perhaps a lot can be done without switching to WebAssembly. For example, you app is primarily GPU-bound, not CPU-bound. And I remember that I saw how glError
calls make significant impact (and BTW calling glError
on production should be avoided), so instead of trying to compile your app to WebAssembly, I suggest to profile it first.
Its very easy for me to remove asyncs from the code if you mean @Async
and just use the normal java synchronization primitives (sychronized/wait/notify) for that piece of the code instead, currently I think I only use @Async
in one place in the entire WASM runtime I created because I'm trying to make it easy to run with cheepj as well. I don't think WASM will make it any faster, I've never thought that and that's why I never touched it in the past, I'm just experimenting right now. I already suspected it was the issue from the beginning though, and when I tried removing it, as in compiling it with absolutely no @Async
anywhere in my code I got the exact same error. If you read what I wrote its literally throwing that error while compiling org.teavm.backend.wasm.runtime.WasmSupport.initClasses()V.
its got nothing to do with me using @Async
in my code.
So if I'm not using @Async
in my code, and the exception is coming from what appears to be a generated function instead of some mess in my own code, I don't see that as grounds for me to give up due to my codebase being hopelessly bound to the JS runtime. There's probably some kind of bug here we can fix.
In the most recent versions, we did profile it and discover the issue was setTimeout
was being artificially throttled to 4 milliseconds after several recursive calls, so we used the message event a MessageChannel
instead to insert the async delay between each frame which resulted in 2333 FPS in chrome under optimal conditions.
Oh it is a little faster without glGetError (that was put in the game by the original developers not me), I get about 2800 FPS max in my browser now so it adds like 0.071 milliseconds per frame on my hardware.
Its very easy for me to remove asyncs from the code if you mean @Async and just use the normal java synchronization primitives (sychronized/wait/notify)
No, this won't help. Java synchronization primitives use @Async
under the hood, so it still counts as "you application uses asyncs". I don't know you code base well, but I suspect that some of asyncs needed just to load resources on demand. If you manage to preload resources prior application, that at least some asyncs go. But I'm not sure if it's easy to get rid of them.
Oh it is a little faster without glGetError (that was put in the game by the original developers not me), I get about 2800 FPS max in my browser now so it adds like 0.071 milliseconds per frame on my hardware.
I suppose that impact on old slow Chromebooks is even much more significant. As for my own experience, with our app we can't rely on our developers' PCs when testing performance, because they are not just faster, but also their GPUs are much more advanced and do a lot of performance work for you. With those poor GPUs on Chromebooks patterns are quite different and things that you haven't noticed on good hardware, become making a tremendous impact on performance.
No, this won't help. Java synchronization primitives use @Async under the hood, so it still counts as "you application uses asyncs".
I see, currently there’s no part of my codebase that really depends on being async, it’s designed to run in both a browser and ordinary JVM using LWJGL, so all the higher-level code is already synchronous and only needs a single thread to work. The only part I’m using asyncs is for blocking the thread in the browser runtime while certain async browser functions complete.
In the case of eliminating all async, images and sounds can just be loaded in WASM (my code already supports JOrbis for synchronous OGG file decoding on unsupported browsers, and images are all PNGs which is easy to parse). File IO can just use the WASI filesystem interface, and for requestAnimationFrame I can just make the application’s main loop an exported function. None of these things are especially difficult to implement in my opinion.
I have two questions though, if you could clarify it would be great:
Final note: I am aware that my PC I test on is terrible for realistically profiling something meant to run on a chromebook, however it’s just a project I work on in my free time it’s not something I work on professionally, therefore I have no intention of actually going out and spending potentially hundreds of dollars on a test device I’m never even gonna use otherwise.
Thank you for responding.
In the case of eliminating all async, images and sounds can just be loaded in WASM
I recommend to load them as separate files, but prior to the main game logic. So at the moment the game logic begins, all resources are ready and can be read synchronously. Rationale: when you update your game, you make your user to download all resources again.
Does the C backend support asyncs?
No, it does not. C backend shares same architecture as Wasm BE, so both suffer from inability to properly support coroutines.
If this is the case I could just try compiling to C, and then using emscripten to compile the C to WASM to get async support
Don't be so naive. I wish you good luck even with compiling simple Hello, world app using this pipeline. If this was a working solution, I never developed Wasm BE at the first place.
Is there any way at all to “sleep” in WASM/WASI backend
No, it does not work this way.
So please give up. All you can do is to wait and hope that eventually I support asyncs in Wasm GC BE.
So please give up. All you can do is to wait and hope that eventually I support asyncs in Wasm GC BE.
I've already thought of synchronous ways to do everything that's currently @Async
and I don't rely on multiple threads in my code so the inability to save/resume is not an issue either. You obviously don't want to help me so I'll close this and stop asking about it but I'm fairly confident I can make it work with no asyncs by embedding pure WASM PNG and OGG decoders in it and using the WASI filesystem instead of IndexedDB. I've already converted the base structure of the game so that the main loop can be exported and spun from the JavaScript realm instead of spinning the loop from within the WASM portions, I'll email you the results if I do get it to work but for now there is still quite a bit of JavaScript I need to finish first.
You obviously don't want to help
No, I want to help you. But please note that many years ago I spent a lot of time and effort to make asyncs work in JavaScript. Right now I don't have this amount of time. Also, I don't want to maintain classical Wasm BE, because Wasm GC is more appropriate target. While classical Wasm binary is 5 times bigger than JS for the same codebase, Wasm GC BE produces binary of comparable size to JS. Also, it's a bit faster (because there's no overhead on supporting shadow stack) and it's possible to support entire JSO (except for passing arrays by reference) with it. So why would I invest my time into classical Wasm? I'm determined to support all the features of JS in Wasm GC BE, including reflection and asyncs, but it still requires time. Eventually you'll be able to convert your project into wasm without any effort. But please note that there no way to work around this. You either have the compiler that can turn you code into state machine to be able to save/restore its state, or just proceed with callbacks. No Wasm or WASI features or some sort of magic can help. So the only way I can help you is to implement this in TeaVM, but I can't do it just in couple days.
using the WASI filesystem
There's no such thing as "WASI filesystem" in the browser. WASI is intended to interact directly with OS from headless Wasm environments. There's WASI polyfill for the browser, but it still relies on browser APIs. I don't know how exactly WASI polyfill for filesystem works, but to be synchronous it has to rely on keeping entire FS in memory (and perhaps syncing with IndexedDB on the background). Nothing magical, everything you could do yourself.
however it’s just a project I work on in my free time it’s not something I work on professionally, therefore I have no intention of actually going out and spending potentially hundreds of dollars on a test device I’m never even gonna use otherwise.
Yeah, but what you can do here is ask your users. Perhaps there are some who agree to test two different versions and report FPS change. For example, one is with this fix regarding setTimeout
and another without it, and so on.
No, I want to help you
Sorry let me rephrase, what I mean is you obviously don't want to spend time attempting to get my app working on the WASM BE in its current state, and that the WASM BE also is in the middle of being rewritten, so I won't bother you with any more questions on how to get it working until sometime in the distant future when WASM GC is complete and has async support.
WASI is intended to interact directly with OS from headless Wasm environments
I see, when I first read up on it I was under the impression that WASI was also supported by browsers for the purpose of eliminating all the boilerplate JavaScript code, since that was the only way I saw it even being a useful thing in the first place for most people who write apps that run in WASM.
but to be synchronous it has to rely on keeping entire FS in memory
Okay thanks for the advice, I might settle for a localized version of this this but I'm also thinking if there's just some "green" way to sit in an infinite loop in WASM without taking 100% of an underlying CPU core from the OS that I could perform asynchronous filesystem tasks in a worker and just watch the contents of a SharedArrayBuffer from the client side in order to exchange blocks with the worker or wait while any asynchronous IO tasks complete.
Whatever I end up doing though will be done in a way that I can transition to using @Async
in my WASM builds again once it becomes available, and it won't involve changing the way my app's JS runtime works. I'm not making substantial changes to how the JS builds of my app work, and I don't plan for WASM to be a replacement for JS either, I'm just experimenting.
Is there any reason I can't use asyncify? It looks like it's able to be injected into an already fully compiled WASM binary. I don't need the ability to create multiple TeaVM threads to switch between, I just need to be able to save the current state of the WASM while I execute some async JavaScript then have the WASM resume from where it was saved.
https://kripken.github.io/blog/wasm/2019/07/16/asyncify.html
I've now completely eliminated all @Async
from my code, and also eliminated all Thread.sleep
and Object.wait
. I can't find any other hidden uses of @Async
anywhere else in TeaVM or my own code, and it still crashes with the same exception. I think my codebase is just significantly larger than anything it gets regularly tested with, and that this compiler generated initClasses
method is just absolutely massive. I'm talking like 4000+ classes including anonymous and inner classes.
Ahaha, our project is somewhere between 0.5M and 1M LOC of code, with around 100K classes, resulting in 16M of minified JS, and everything works just fine. You can try to find async method using following technique. Run TeaVM in debugger and set breakpoint on AsyncMethodFinder
class, method private void add(MethodReference methodRef, CallStack stack)
.
Ahaha, our project is somewhere between 0.5M and 1M LOC of code, with around 100K classes, resulting in 16M of minified JS
I meant the WASM backend, you're not saying you can 100K classes with the WASM backend, right? The JS backend has worked great for me, I have absolutely no complaints, I'm wondering what amount of code the WASM backend has been proven to be able to compile.
I'm wondering what amount of code the WASM backend has been proven to be able to compile.
Ah, you meant that. I just compile tests (some of which are relatively big, but still far from 4000K classes) and couple of small demos, so I'm really not sure if it's able to compile big projects. However, size does not really matter here if you use poorly supported features like asyncs. Coroutine transformation won't do anything if you don't have any async methods. So this particular error means that you still have some.
Ah, you meant that. I just compile tests (some of which are relatively big, but still far from 4000K classes) and couple of small demos, so I'm really not sure if it's able to compile big projects
Okay, thanks, I will keep that in mind.
Coroutine transformation won't do anything if you don't have any async methods.
I'll spend a few more hours hunting for asyncs tomorrow, I'm pretty confident I've completely eliminated them from my source folders so I'll start looking into what other TeaVM classlib functions could potentially be indirectly calling Thread.sleep
or one of the others and causing it to be enabled that way. There are still some synchronized
blocks in my code too but that is something I'm a lot more hesitant to remove because I have a "desktop runtime" where the code that normally runs in a worker in JS runs in a daemon thread sharing the same context with the client thread so there are a few sync blocks scattered around the shared source folders to account for that.
I'll spend a few more hours hunting for asyncs tomorrow
Please, use the trick I proposed. It would only take few minutes to clone TeaVM sources and set up remote debugger, but then you save hours.
Please, use the trick I proposed. It would only take few minutes to clone TeaVM sources and set up remote debugger, but then you save hours.
The issue is I physically cannot compile my (still WIP) WASM runtime for my app to the JS backend, it's made specifically for WASM and will not work in JS. I went all out with the Address class. In order to use the debugger I would have to take my app's JS runtime and breakpoint that instead and try to make assumptions from it about what is making it into the WASM builds.
he issue is I physically cannot compile my (still WIP) WASM runtime for my app to the JS backend, it's made specifically for WASM and will not work in JS. I went all out with the Address class. In order to use the debugger I would have to take my app's JS runtime and breakpoint that instead and try to make assumptions from it about what is making it into the WASM builds.
Sorry, I still don't understand how it's related to debugging TeaVM. I didn't propose you to compile anything to JS
I misunderstood what you meant sorry, 3am moment, I thought you said compile it to JS and breakpoint the JS. I think what you actually meant was breakpoint the compiler while compiling it to WASM, which I'll do tomorrow.
It was Thread.yield()
, I forgot the devs of this game like that function. I have now successfully compiled my WASM image. It's 20 MB uncompressed but with LZMA2 its only 3.1 MB, which is good because I already planned to compress it with XZ and then have a "loader" WASM image in C that decompress it at startup. Hopefully asyncify doesn't break it, however I'm actually fairly confident it will have no side effects on the code besides increasing the size and slowing it down more, since WASM doesn't seem that fragile.
I have not tested it in browser yet but wasm-opt
(the component of binaryen for injecting asyncify) can't parse it, I guess I need to find what function this is
[parse exception: attempted pop from empty stack / beyond block start boundary at 647144 (at 0:647144)]
And I do fully understand I will not be receiving support for any issues that asyncify causes on the legacy WASM backend, I'm just reporting my findings.
Sure, I won't fix these issues immediately or help you directly with your project. However, I can help with some hints. For example, recent TeaVM has it own secret disassembly. Clone and in core
module find class org.teavm.backend.wasm.disasm.Disassembler
. It's runnable. The only reason I made it was the inability of existing tools, like wabt or emscripten, to disassemble TeaVM's output.
I hope disassembler could help to identify the reason of this issue. It might be issue in asyncify or in TeaVM. And if it's issue in TeaVM, perhaps I can fix it.
However, I still don't fully understand why you need this asyncify. You successfully eliminated all async calls, right? So why don't you just proceed this this? FYI, async transformation not only increases binary size, but kills performance as well. BTW, if you want to increase JS performance, you can try to reduce number of direct or indirect calls to asyncs. For example, you can remove any Thread.yield
calls without any significant effect in semantics, but presumably some performance boost.
For example, recent TeaVM has it own secret disassembly. Clone and in core module find class org.teavm.backend.wasm.disasm.Disassembler. It's runnable. The only reason I made it was the inability of existing tools, like wabt or emscripten, to disassemble TeaVM's output.
Okay thanks, I will try that.
However, I still don't fully understand why you need this asyncify. You successfully eliminated all async calls, right?
I thought I could eliminate all async calls if I used the WASI filesystem, but that was before I was aware that WASI isn't supported in browsers. I thought WASI was supported in browsers to help eliminate all the boilerplate JavaScript, but I know that's wrong now. I've returned to the approach of having my own "promise" synchronization primitive that I can implement with asyncify and then eventually convert to using @Async
if that feature becomes available.
BTW, if you want to increase JS performance, you can try to reduce number of direct or indirect calls to asyncs
One of my rules is to completely avoid threads and async as much as possible, generally only a single async call happens per frame and it's for requestAnimationFrame
or setTimeout
(or the MessageChannel hack I described in the case where we want an async delay below 4ms), I've completely rewritten the parts of the game that did originally use threads. The Thread.yield
I missed was in a loading screen, originally they had a yield in the main loop of the game but I removed that years ago, I simply forgot the loading screen had one too.
I managed to hunt down the source of the offending function, wasm-opt explodes on this line in the compiled WASM image
}catch(Throwable t) {
throw new IOException("Failed to read packet type '" + pkt.getClass().getSimpleName() + "'", t);
}
It crashes on the end of the catch block, as in the last instructions to successfully decode before it crashes appears to be a break (br
) instruction directly after TeaVM's $meth_otr_ExceptionHandling_throwException
function is called for the throw
that's inside this catch block.
(; 0009dfd7 ;) call (; 490 ;) $meth_otr_ExceptionHandling_throwException
(; 0009dfda ;) local.get 21
(; 0009dfdc ;) i32.load
(; 0009dfdf ;) i32.const 12620
(; 0009dfe3 ;) i32.sub
(; 0009dfe4 ;) br $label_96 // Last decoded instruction
(; 0009dfe6 ;) end (; $label_144 ;) // wasm-opt crashes here?
(; 0009dfe7 ;) end (; $label_96 ;)
(; 0009dfe8 ;) i32.const 1
(; 0009dfea ;) i32.sub
(; 0009dfeb ;) br_table $label_0 $label_95 $label_94
This fragment gives absolutely nothing. Could you post the entire function? Is it somewhere inside your code, or it's Java library code? If it's former, you can use any appropriate tool to strip name
section to obfuscate output prior to disassembly.
I'm sorry. I included the link to the source, its my code, the disassembly of this functions is several thousand lines so I've uploaded it here with the comment if you really feel like getting a migraine. I realized it calls a function that uses certain reflection that isn't supported in WASM (yet), so I'm eliminating that before I keep trying to make it work.
realized it calls a function that uses certain reflection that isn't supported in WASM (yet)
No-no-no, this fragment is totally fine. When I claim that Wasm BE does not support reflection, I mean advanced reflection like Method
and Field
class.
No-no-no, this fragment is totally fine.
I don't mean the fragment I posted, I mean another function called by this function. I know that getSimpleName is well supported.
There were errors printed about a few other functions that I didn't notice because it was late at night and I was compiling it with --debug
so it was buried in hundreds of messages of gradle nonsense
I've eliminated the unsupported reflection (Class.newInstance
and a few others) from my codebase (which has actually reduced the size of the JS file in my JS runtime by over a megabyte) but unfortunately that was not the issue. wasm-opt appears to be crashing at the same point of the same function. I'll keep investigating this weekend and actually test loading this WASM file in a browser and see if it complains about the same parsing error or not.
@konsoletyper Google Chrome also says the WASM image is invalid when I parse it using WebAssembly.compileStreaming
, however it appears to be for a completely different reason, it says:
Uncaught (in promise) CompileError: WebAssembly.compileStreaming(): Compiling function #758:"meth_oj_JSONObject_stringToNumber" failed: i64.reinterpret_f64[0] expected type f64, found local.get of type i32 @+185210
I think I'm just gonna wait until WASM GC is available before I try to compile my app to WASM with TeaVM. I don't think its worth distracting you with issues in the legacy WASM backend when WASM GC will make it irrelevant anyway. Thank you for helping me with this, I will definitely be back once WASM GC is ready.
I figured out the correct way to implement asyncs, I just had to stick to my delusions of finding a way to block the WASM without taking 100% of an underlying CPU core. If I just run the entire app in a worker I can use Atomics.wait
to block the thread synchronously while a different worker completes the async function and uses a SharedArrayBuffer to exchange parameters and use as a mutex. It's not very straightforward but will completely eliminate the need for asyncify or any save/restore logic in the main WASM image. It turns I was going in the right direction with my "blocking the WASM somehow that doesn't take 100% of an underlying CPU core while a worker completes the function" nonsense, it was the right answer all along I just have to move the entire app into a worker.
Google Chrome also says the WASM image is invalid when I parse it using WebAssembly.compileStreaming, however it appears to be for a completely different reason, it says
I think you should definitely report this. Reimplementing asyncs would take weeks, which I don't have, but there's a chance that this particular issue is easy to fix. There's also a chance that it's inside some code that's shared between Wasm and Wasm GC, so it will help me anyway. So, does it reproduce without FULL (AGGRESSIVE) optimization? Can you turn off optimization and check if issue persists? Can you try fresh TeaVM from master
branch? How can I reproduce this issue myself?
I will definitely be back once WASM GC is ready
It turns I was going in the right direction with my "blocking the WASM somehow that doesn't take 100% of an underlying CPU core while a worker completes the function" nonsense
Yes, but please note that you can't perform any WebGL rendering from within worker, as well as getting any input events. But I still don't understand your problem. Do you need a game loop like:
while (true) {
takeInput();
performLogic();
renderFrame();
waitALittle();
}
Then you just don't do it this way in the browser. Instead, you do something like this:
function gameLoop() {
takeInput();
performLogic();
renderFrame();
requestAnimationFrame(() => gameLoop());
}
So, does it reproduce without FULL (AGGRESSIVE) optimization? Can you turn off optimization and check if issue persists?
It will only compile on BALANCED, the others crash and don't produce any WASM. I was gonna try again once WASM GC is done and complain then if it also happens on the latest instead of the legacy WASM backend.
Yes, but please note that you can't perform any WebGL rendering from within worker, as well as getting any input events.
I'm very well aware of this and my plan is to put the WebGL (and audio, and returning input events) all in a queue based on a SharedArrayBuffer to be handled by the page instead of the worker.
And in the meantime.
Congratulations
Then you just don't do it this way in the browser. Instead, you do something like this
One of the main goals of my project is to create a runtime environment modeled after LWJGL that can be compiled to run in both desktop and browser. Several people use my codebase to port other versions of the same game to run in a browser and by changing the app to need to swap buffers this way instead of having the ability to do it on demand without completing the current loop would require the codebases of the games to be fundamentally restructured in order to work right (trust me I did it last week before realizing it's more beneficial to people to find a way not to). By doing things synchronously and blocking the thread while async functions complete, suddenly ANYTHING that is built on my current codebase can then run in WASM without fundamentally restructuring. I think it's completely worth the pain of inventing this SharedArrayBuffer queue if it means that EVERYONE will get WASM instead of just those who want to waste their time rewriting all the synchronous loading screens and chopping their main loops up.
I think you should definitely report this.
If you would like me to email you a copy of the gradle project that caused this I can, however please be aware that it is for the legacy WASM backend and from when I was still planning to use asyncify. There aren't any asyncs in the java portions that I know of. I don't have the JavaScript imports to go with it finished either and in light of deciding to invent this "queue" idea with the SharedArrayBuffer I probably won't be using most of this code. The queue-based system will be WASM GC for Java, but the queue encoder/renderer and atomics will be separate and use conventional WASM instead and not compiled with TeaVM (probably written in C and compiled with Emscripten). I will not have any asyncs in the Java code in hope that the backend won't apply async transformations to the code for better performance, so that feature will not be a requirement for me for now in WASM GC.
@konsoletyper I just wanted to let you know that JSPI exists, on chrome it still appears to be locked behind a chrome://flags
but as far as I can tell this completely eliminates the need for async transformation, and any functions imported into WASM that return a promise should just work.
@lax1dude I know about this proposal, but this has nothing to do with async transformation. The relevant proposal is called "stack switching".
this has nothing to do with async transformation
What I mean is JSPI can be used instead of async transformation for imported functions that deal with promises. I know that the JSPI proposal is probably not fit for creating green threads, but for my specific app I am not using multiple threads anyway and I only need the ability to suspend/resume the app's main thread while some promises complete.
@lax1dude nope, it does not work this way. JS promise integration is completely irrelevant for your case
nope, it does not work this way. JS promise integration is completely irrelevant for your case
How is it irrelevant, one of the main problems we've discussed is that my app loads resources and uses IndexedDB in the main loop using async methods, I could implement those portions of my code in JavaScript instead using functions that return a Promise, and complete the promise with the loaded resource or whatever the result is of the IndexedDB operation.
@lax1dude sure, but how are you going to transform a synchronous-style methods into a method that stops its execution and instead provides a callback that continues execution? This is what coroutine transformation for. The proposal you are referring is completely unrelated
@lax1dude sure, but how are you going to transform a synchronous-style methods into a method that stops its execution and instead provides a callback that continues execution? This is what coroutine transformation for. The proposal you are referring is completely unrelated
Because I’m not gonna implement these async portions in Java, I’m gonna implement them in a JavaScript file, as imported functions that return Promises. I’m not as dependent on @Async
as you probably think, my codebase has a working “desktop runtime” where these things are all synchronous and do not use coroutines. My plan does not involve reusing any of the existing Java implementations for resource loading and such that currently use coroutines in the browser, I’m gonna completely redo those small portions in JavaScript. I don’t need to call into the suspended WASM to resume, I just need to complete the promise that I returned from the imported JavaScript function to provide the result of the async operation, it’s no big deal.
I think you misunderstood what I was trying to say last week when I said that "JSPI eliminates the need for async transformation", I meant purely in the context of my specific app that JSPI can eliminate all my needs for async transformation, I didn't mean to say that you could be using JSPI in TeaVM to add support for the existing @Async
annotation to the WASM backend. What I'm saying is I can (with a little effort) rewrite the @Async
functions in my app to be written entirely in JavaScript using a Promise
, instead of writing those portions of my app in Java using the @Async
annotation the way they currently are. I'm not asking you to do anything, I'm just saying I've found a workaround for the parts of my app that currently use asyncs. Please consider this issue resolved.
TeaVM Version: 0.10.x branch
Backend: WASM
I'm compiling a very large program (about 3000 classes) and the compiler is crashing due to some internal issue, I have no idea what to do about it because it doesn't appear to be caused by any code that I wrote.
At first, there was no way to tell at all what was causing the crash, so I cloned the 0.10.x source and added a print to
optimizeMethodCacheMiss
to print out the name of the method that was crashing it. It turns out the name of the method that causes it to throw the exception isorg.teavm.backend.wasm.runtime.WasmSupport.initClasses()V
.That function completely empty in the WasmSupport.java source file so I assume its generated by the compiler.
I am not using the JSO in my program, I have taken the time to completely rework all parts that used to use the JSO to instead use
@Import
and@Export
functions that take only numbers and memory addresses. If you would like the source code of the project to try compiling yourself I can email it to you.