mach icon indicating copy to clipboard operation
mach copied to clipboard

libmach: Compile to shared library?

Open zack466 opened this issue 3 years ago • 7 comments

Firstly, I want to say that this is a great project, and I really appreciate the effort being put into a truly cross platform graphics toolkit. Are there any plans (or is it even possible) to compile mach into a shared library? I think this would allow for easy integration with any programming language that supports a C FFI.

zack466 avatar Jul 06 '22 23:07 zack466

I've thought about this a bit, I think it would be good to have something like this, yes.

Supporting a broad range of bindings in other languages would be an explicit non-goal (i.e., you'd be on your own, because we just don't have time to support that) but I think that having a well-defined C ABI could be a good way to enable adoption of Mach from other languages.

I also think that having an option to script Mach using WASM modules (so, whichever languages can compile to WASM and use the Mach API through that) is compelling to me.

How this would look exactly? That's a bit up in the air. Since Mach is composed of a bunch of smaller projects, and isn't one big API, I think what we would need to do is create a subproject (let's call it libmach) which defines a C ABI, and imports all of the standard Mach libraries in order to implement it.

How far we go with it, what gets supported vs. what does not.. that gets trickier. I don't know. I think ultimately, the thought here would be: is someone interested in contributing/maintaining such a library?

emidoots avatar Jul 09 '22 22:07 emidoots

I'd be willing to contribute.

I made a hacky proof of concept here. If you run zig build, cd into libmach, then run make run, it should run the boids example. All it took was adding src/exports.zig and then generating a shared library in build.zig. I think that exporting mach internals should be relatively easy.

However, I'm not exactly sure how exactly the mach API should be exposed. From what I can tell, mach currently requires a Zig package called "app" to be linked at compile time, but I don't think this is desirable in the context of dynamic linking. Exposing the high-level API would probably require App to be more decoupled from mach internals. Otherwise, I don't think runtime linking would really make any sense (unless only low-level API functions are exposed?).

Also, Zig currently can't automatically emit C header files (but it should be supported by stage 2), so those would have to be written by hand for the moment.

edit: oops, for any future readers, the above link is no longer accurate (thanks, Mr.git push --force)

zack466 avatar Jul 11 '22 04:07 zack466

cool!

The reason Mach requires an App is because we aim to support browser and android/ios. In these, you don't really get a main function - so the standard idea of "I will call into the library" doesn't really work. Instead, you get callbacks (basically) where your app can do things, render, etc. App reflects this constraint, I don't think we can change it.

So there are a few ways we can do this for libmach..

  1. We could copy the ECS app, like this, and define an app that has no ECS modules at all (they'll all have to be added at runtime via libmach API). In this model, when you link against libmach it would have it's own pub fn init(engine: *ecs.World(modules)) !void { which calls your exported mach_init callback. Therefor, your C code must export such a function so it can call it.

    • Benefit: It works on (at least) Desktop, Android, and iOS. Might work on WebAssembly too.
    • Benefit: Supports Mach engine apps ("I want everything")
    • Con: Doesn't support Mach core apps ("I want a window, input, and WebGPU API. That's it")
  2. We could define an app that exposes init, deinit and update functions similar to the triangle example but again, just have them directly call a C function mach_init, mach_deinit, mach_update and expect that those are defined by the user linking libmach into their program.

    • Benefit: It works on (at least) Desktop, Android, and iOS. Might work on WebAssembly too.
    • Benefit: Supports Mach core apps ("I want a window, input, and WebGPU API. That's it")
    • Con: Doesn't support Mach engine apps ("I want everything")
  3. We could say "Truly? I don't care about iOS/Android/WebAssembly." - In this model, we could define a custom platform type for "library" mode, in which main is not defined for you but rather it is exported as mach_main or similar.

    • Benefit: Supports both Mach engine and Mach core apps.
    • Con: Can't ever work on Android/iOS/WebAssembly.

My advice would be that we go with both #1 and #2, naming them libmachengine and libmachcore. Depending on which one you link against, you choose whether it expects to find just a single init function (Mach engine apps), or init, deinit and update (Mach core apps.)

Then the rest of this is just sorting out how to write/expose C ABIs for Mach APIs. Shouldn't be too hard, but definitely some work to do there. Stage2 will help with headers in the future as you mentioned.

emidoots avatar Jul 12 '22 14:07 emidoots

Ok, thanks for the explanation.

However, I'm a little confused. When you say "C code exporting a function", what exactly do you mean? Do you mean that a C file can define something like extern void mach_init() and then libmach will figure out the right function(s) to call through some linker magic? (I haven't been able to get this to work yet). Or do you mean having C code explicitly pass function pointer(s) to libmach at runtime to use as callbacks?

I was thinking the second approach makes more sense, since then other languages with a C FFI could load libmach and then provide their own callback function(s) for libmach to call in its internal core loop. That's the sort of use case I was envisioning (similar to libraries like Raylib, but you use mach's core loop instead of your own). But would this satisfy the constraints of iOS/Android/WASM?

If we go with the first approach instead, then I just don't really see how C bindings would be all that useful - I thought the whole point was so that Mach could sort of be used like a library from multiple languages. Is there something that I'm missing?

zack466 avatar Jul 12 '22 22:07 zack466

Ooh, sorry, I guess my brain kind of glazed over on that part. For some reason I was thinking of languages that can export C functions as part of their FFI approach (e.g. Go can do this) but not languages that use e.g. dlopen-based FFI.

OK, so I think what we'll need to do in order to support this then is something like this:

  • libmach (single library)
  • Rename platform/native.zig to something like native_common.zig and rename main in there to run_main.
  • Add a new platform/native.zig which exposes main and just calls run_main.
  • Add a new platform/libmach.zig which will expose our main C ABI. It can be used in two ways:
    • Mach engine apps:
      • mach_engine_set_init (takes a single argument, a function pointer like this). If you use this, you're writing a Mach engine app.
      • mach_run - takes control from here on out, and calls native_common.zig's run_main function.
    • Mach core apps:
      • mach_core_set_init (function pointer like this)
      • mach_core_set_deinit (like this)
      • mach_core_set_update (like this)
      • mach_run - takes control from here on out, and calls native_common.zig's run_main function.

In theory, I think that libmach.zig could maybe look something like this, except it would also need to expose those mach_* functions above.

Then inside of the mach_run implementation, it would look at whether you used mach_engine_set_init or mach_core_set_init and decide what to do from there:

  • If you've used mach_core_set_init, it can just delegate to those function pointers you've set.
  • If you use mach_engine_set_init, then it just needs to delegate those 3 function calls to these 3 functions and call the user's init

Let me know if any of that doesn't make sense, there might be other things I've not thought about here. We can start small, maybe with just a PR to do the file renaming + expose mach_run when building a shared library?

Feel free to join the Matrix chat BTW, we do a lot of collaboration/discussion like this in there: https://matrix.to/#/#hexops:matrix.org

emidoots avatar Jul 13 '22 00:07 emidoots

Just to track progress: #406 #420 #423

zack466 avatar Jul 19 '22 04:07 zack466

Rename platform/native.zig to something like native_common.zig and rename main in there to run_main.

Why is this necessary? The name main doesn't mean anything in Zig, it's just what std/start.zig looks for in @import("root") when you compile an executable.

silversquirl avatar Jul 22 '22 12:07 silversquirl

new approach hexops/mach#858

emidoots avatar Jul 13 '23 00:07 emidoots