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

C language support (prototype) #3347

Draft
wants to merge 24 commits into
base: main
Choose a base branch
from

Conversation

alexswan10k
Copy link
Contributor

@alexswan10k alexswan10k commented Feb 15, 2023

If it's in C it runs anywhere, right? 😆

Inspired by @dbrattli attempting to do Go (and in the interest of transparency), here is a little experiment I was doing over the holidays and got bored of. Maybe I will come back to it someday, or even if it inspires someone else, I would be happy.

It covers some basic functionality including imports, and an attempt to do reference types using a managed Rc pointer (basically how rust does it but all dispose calls have to be explicitly written).

In its current state, you can build and run a few tests, however, it is not remotely useful yet. creates lots of memory leaks, and falls over with the most trivial of cases. I had a brief attempt at closures too, but this is clearly non-trivial.

Will keep this posted if I ever move it forward. For reference, it is based on the #3290 experiment.

Here is a sample of the output:

#ifndef C__source_github_Fable_tests_C_tests_src_RunTests_fs
#define C__source_github_Fable_tests_C_tests_src_RunTests_fs
#include <stdio.h>
#include <assert.h>
#include "../../fable-lib/native.c"
#include "../../fable-lib/closure.c"
#include "../../fable-lib/rc.c"
struct RunTests_Simple1 {
    int X;
    int Y;
};
struct RunTests_Simple1 RunTests_Simple1_new(int X, int Y) {
    struct RunTests_Simple1 item;
    item.X = X;
    item.Y = Y;
    return item;
}
struct RunTests_Simple1 RunTests_create(int y) {
    return RunTests_Simple1_new(1, y);
}
struct RunTests_Simple1 RunTests_m() {
    return RunTests_Simple1_new(2, 2);
}
int RunTests_another(int x) {
    return x + 1 + 2;
}
struct RunTests_Simple2 {
    int X;
    int Y;
};
struct Rc RunTests_Simple2_new(int X, int Y) {
    struct RunTests_Simple2 item;
    item.X = X;
    item.Y = Y;
    struct Rc rc = Rc_New(sizeof(item), &item, NULL);
    return rc;
}
struct Rc RunTests_addBoth(struct Rc a, struct Rc b) {
    struct Rc ret = RunTests_Simple2_new(((struct RunTests_Simple2* )(a).data)->X + ((struct RunTests_Simple2* )(b).data)->X, ((struct RunTests_Simple2* )(a).data)->Y + ((struct RunTests_Simple2* )(b).data)->Y);
    Rc_Dispose(a);
    Rc_Dispose(b);
    return ret;
}
struct Rc RunTests_forwardToAddBoth(struct Rc x) {
    struct Rc ret = RunTests_addBoth(RunTests_Simple2_new(1, 2), Rc_Clone(x));
    Rc_Dispose(x);
    return ret;
}
struct Rc RunTests_addMore(int i, struct Rc a, struct Rc b) {
    /*Number (Int32, Empty)*/;
    int first = ((struct RunTests_Simple2* )(a).data)->X + ((struct RunTests_Simple2* )(b).data)->X + i;
    struct Rc ret = RunTests_Simple2_new(first, ((struct RunTests_Simple2* )(a).data)->Y + ((struct RunTests_Simple2* )(b).data)->Y + i + first);
    Rc_Dispose(a);
    Rc_Dispose(b);
    return ret;
}
int RunTests_condition1(struct Rc x) {
    if (((struct RunTests_Simple2* )(x).data)->X == 1) {
        if (((struct RunTests_Simple2* )(x).data)->Y == 3) {
            int ret = 2;
            Rc_Dispose(x);
            return ret;
        }
        else {
            int ret = 4;
            Rc_Dispose(x);
            return ret;
        }
    }
    else {
        int ret = 3;
        Rc_Dispose(x);
        return ret;
    }
}
struct RunTests_DU_A {
    int tag;
};
struct Rc RunTests_DU_A_new() {
    struct RunTests_DU_A item;
    item.tag = 0;
    struct Rc rc = Rc_New(sizeof(item), &item, NULL);
    return rc;
}
struct RunTests_DU_B {
    int tag;
    int Item;
};
struct Rc RunTests_DU_B_new(int Item) {
    struct RunTests_DU_B item;
    item.tag = 1;
    item.Item = Item;
    struct Rc rc = Rc_New(sizeof(item), &item, NULL);
    return rc;
}
struct RunTests_DU_C {
    int tag;
    int a;
    int b;
};
struct Rc RunTests_DU_C_new(int a, int b) {
    struct RunTests_DU_C item;
    item.tag = 2;
    item.a = a;
    item.b = b;
    struct Rc rc = Rc_New(sizeof(item), &item, NULL);
    return rc;
}
struct Rc RunTests_stuff() {
    return RunTests_DU_B_new(4);
}
int delegated_975225780(struct Rc _arg) {
    if (((struct RunTests_DU_A* )(_arg).data)->tag == 1) {
        int ret = ((struct RunTests_DU_B* )(_arg).data)->Item;
        Rc_Dispose(_arg);
        return ret;
    }
    else {
        if (((struct RunTests_DU_A* )(_arg).data)->tag == 2) {
            int ret = 1;
            Rc_Dispose(_arg);
            return ret;
        }
        else {
            int ret = 0;
            Rc_Dispose(_arg);
            return ret;
        }
    }
}
int RunTests_matchstuff(struct Rc _arg) {
    int ret = delegated_975225780(_arg);
    Rc_Dispose(_arg);
    return ret;
}
struct Rc RunTests_genericMap(struct Rc f, struct Rc x) {
    /*LambdaType (GenericParam ("$a", false, []), GenericParam ("$b", false, []))*/;
    struct Rc ret = ((struct FnClosure1* )(f).data)->fn(Rc_Clone(f), Rc_Clone(x));
    Rc_Dispose(f);
    Rc_Dispose(x);
    return ret;
}
typedef struct Rc function_1282299497 (struct Rc p_0, struct Rc p_1);
struct closure_struct_2134664835 {
    function_1282299497*  fn;
};
struct Rc fn_with_closed_2134664835(struct Rc self, struct Rc x) {
    struct Rc ret = RunTests_Simple2_new(((struct RunTests_Simple2* )(x).data)->X + 1, ((struct RunTests_Simple2* )(x).data)->Y + 1);
    Rc_Dispose(self);
    Rc_Dispose(x);
    return ret;
}
struct Rc closure_struct_2134664835_new() {
    struct closure_struct_2134664835 item;
    item.fn = fn_with_closed_2134664835;
    struct Rc rc = Rc_New(sizeof(item), &item, NULL);
    return rc;
}
void RunTests_testGenericMap() {
    /*DeclaredType
  ({ FullName = "RunTests.Simple2"
     Path = SourcePath "C:/source/github/Fable/tests/C/tests/src/RunTests.fs" },
   [])*/;
    struct Rc res = RunTests_genericMap(closure_struct_2134664835_new(), RunTests_Simple2_new(1, 1));
    assert(((struct RunTests_Simple2* )(res).data)->X == 2);
    assert(((struct RunTests_Simple2* )(res).data)->Y == 2);
    Rc_Dispose(res);
}
typedef struct Rc function_494203216 (struct Rc p_0, struct Rc p_1, struct Rc p_2);
struct closure_struct_captures_2026899426 {
    struct Rc capt;
};
struct closure_struct_2026899426 {
    function_494203216*  fn;
    struct Rc captures;
};
struct Rc fn_with_closed_2026899426(struct Rc self, struct Rc x) {
    struct Rc captures = Rc_Clone(((struct closure_struct_2026899426* )(self).data)->captures);
    struct Rc capt = Rc_Clone(((struct closure_struct_captures_2026899426* )(captures).data)->capt);
    struct Rc ret = RunTests_Simple2_new(((struct RunTests_Simple2* )(x).data)->X + 1 + ((struct RunTests_Simple2* )(capt).data)->X, ((struct RunTests_Simple2* )(x).data)->Y + 1 + ((struct RunTests_Simple2* )(capt).data)->Y);
    Rc_Dispose(capt);
    Rc_Dispose(captures);
    Rc_Dispose(self);
    Rc_Dispose(x);
    return ret;
}
struct Rc closure_struct_2026899426_new(struct Rc capt) {
    struct closure_struct_2026899426 item;
    item.fn = fn_with_closed_2026899426;
    struct closure_struct_captures_2026899426 captures;
    captures.capt = capt;
    item.captures = Rc_New(sizeof(captures), &captures, NULL);
    struct Rc rc = Rc_New(sizeof(item), &item, NULL);
    struct Rc ret = rc;
    Rc_Dispose(capt);
    return ret;
}
void RunTests_testGenericMapWithClosure() {
    /*DeclaredType
  ({ FullName = "RunTests.Simple2"
     Path = SourcePath "C:/source/github/Fable/tests/C/tests/src/RunTests.fs" },
   [])*/;
    struct Rc capt = RunTests_Simple2_new(3, 4);
    /*DeclaredType
  ({ FullName = "RunTests.Simple2"
     Path = SourcePath "C:/source/github/Fable/tests/C/tests/src/RunTests.fs" },
   [])*/;
    struct Rc res = RunTests_genericMap(closure_struct_2026899426_new(Rc_Clone(capt)), RunTests_Simple2_new(1, 1));
    assert(((struct RunTests_Simple2* )(res).data)->X == 5);
    assert(((struct RunTests_Simple2* )(res).data)->Y == 6);
    Rc_Dispose(res);
    Rc_Dispose(capt);
}

#endif

@weebs
Copy link
Contributor

weebs commented Feb 22, 2023

If it's in C it runs anywhere, right? 😆

Good to hear I'm not the only one who's been thinking this 😄

I've also been playing with a C backend lately and am debating how far I want to take it. I started the project with the intention of learning more about compilers & runtimes, and writing audio software in F# without having to fight with a GC that wasn't intended for real time latency. I've been realizing along the way a C backend creates some cool opportunities for F# code, although I'm not yet sure if it's worth the effort vs something like the Rust backend that I think has more relevance for the F# community. I've been meaning to introduce myself & share my progress so far in case anyone finds a use for it, but as of right now there's quite a bit missing from the GC/OOP/Exceptions/etc side of things. I have a demo I'm working on that I'm excited to share with you all SoonTM, but I figured it'd be best if I say hello now rather than later

If anyone is interested in working on a C backend I'm more than happy to share what I've found so far and I hope to get what I have finished already into a better state & share that work. I haven't really had any experience with Fable or compilation before getting started on this and I got a bit sidetracked along the way hacking together a pseudo-live reload setup, so my code is a bit disorganized to put it lightly 😆

I do want to try and contribute to the Rust and Go backends once I'm in a good spot to put this project on hold for a while. Since I'm mostly interested in using a F# -> C backend for low level tasks like real time audio or writing F# in places I might otherwise use C, I don't know if I'll have the motivation to follow through on features like inheritance/etc. While I have some ideas[1] on how the C backend may be useful to others, I think it might be more fruitful for me to try and help with some of the other language targets. In particular I've been thinking a lot about interop and a cross-target-language library for things like networking/File IO, so the recent discussion posts were good timing since I've been wondering what others opinions on those topics are.

[1] So far the main advantage of C to me has been being able to use F# in the small bits of a project where small code size and control over runtime behavior are necessary. The amount of effort to get F# -> C to match the capabilities of .NET or the other language targets is intense to say the least, but being able to compile some F# code dealing with mostly structs comes in handy when you need it. One idea I've had is to create a Fable plugin that can compile [<Wasm>] or [<Native>] functions so it's straightforward for someone to offload computationally intensive logic without writing complex FFI to bridge JS <-> WASM / F# <-> C

I have some other ideas as well but I'm trying my best to not write a novel here, so I'll dive into those when I share my demo 🤘

Cheers, and happy hacking!
Weebs

@alexswan10k
Copy link
Contributor Author

Hello and welcome :)

This is great to hear. I for one am excited to see it but do feel free to take your time and get things into a state you are happy with. My apologies for not putting this up sooner for visibility reasons, other priorities just got in the way.

I agree with your points. For me, C is quite compelling because it runs basically everywhere and has almost 0 overhead. Pretty much any other language can bind to it too.

With respect to Rust, I absolutely agree that it is probably the place to focus efforts as it adds the most value to Fable, and is already pretty useable. It has one of the best ecosystems too. That being said, choice is always good, and Rust can be quite heavy-handed (slow compiler), and for small low level projects, it is perhaps not the best fit. I also think Fable has a unique opportunity to help with typed high-level language C FFI binding generation as it conceptualises high-level types. I have been researching this in Rust for an unrelated project for C# interop and it's pretty cool what is possible there.

As for what is needed to get something "mergeable", this can seem quite daunting.

I completely agree that any high-level standard library stuff, along with a lot of the OOP concepts and certainly inheritance should be left at the door if possible. The problem we found with Rust was that a lot of the shared standard library kind of requires it implicitly (IEnumerable and IEnumerator are used all over the place in Array/List etc), and it is nice to be able to just reuse the shared code for this rather than rebuild it from scratch.

I think the real hard problems are:

  • Garbage collection substitution - My thoughts were Rc's like Rust, but this requires explicit scope tracking, which is not that straight forward! It is also not something Fable gives you out of the box, so your Fable2C transform needs to have an entirely new mechanism to deal with this.
  • Closures and variable capture, especially when considering GC substitution. If this can be solved, you can extend this logic to classes easily enough (as a class can be thought of as a closure where its ctor params are the captured variables).

With those out of the way, I imagine the rest will be far more manageable.

Anyway do feel free to shout if you want someone to bounce ideas off.

@weebs
Copy link
Contributor

weebs commented Mar 9, 2023

Hey @alexswan10k , no worries about the timing of shraing the PR! I started working with Fable as a weekend project that I kept coming back to since I was having a lot of fun with it (hope you did too!). I initially pulled the repo to take a look at how the JS backend worked and once I realized how much of the difficult bits Fable takes care of with cracking+parsing+simplifying the AST I had to try out writing something myself. Having all of those bits already setup with a project-watch system saved me a lot of energy 😄

I'm pretty close to having a demo ready to share, and I'm also starting to look at implementing an automatic ref counting system. I've broken down where the checks need to happen in my notes and tried a couple of more trivial examples. scope tracking isn't as bad as I thought but structs & unions containing class fields and functions that return objects they've just created can be a little cumbersome to track. I was considering using Boehm GC earlier on but it doesn't seem to work with WebAssembly without significant effort, although it sounds like a good temporary solution for native assemblies.

I also think Fable has a unique opportunity to help with typed high-level language C FFI binding generation as it conceptualises high-level types. I have been researching this in Rust for an unrelated project for C# interop and it's pretty cool what is possible there.

I'm glad to hear this too, something like this has been on my mind as well if I'm understanding you right. Cross language FFI seems like it could be implemented with reasonable effort once there's a couple of backends stable enough w/ respect to FFI. I'm not quite sure how one would achieve sharing of data structures like List, but being able to pass simple struct records and unions between two F# backends would be really valuable to me.

Anyway do feel free to shout if you want someone to bounce ideas off.

What would be the best way for me to reach out about progress and collaboration? I tend to check the F# slack the most and I try to stay up to date with the issues+discussions on this repo, but not as frequently. I have other socials as well but I rarely log into them, although I'm definitely okay with reaching out there as well. I've found with some of the bits I'm working over now the hard decisions aren't as purely technical and more open ended, with things like implementing OOP as simple nested structs vs integrating with GNOME's GObject system (ideally both would be an option behind a flag), or how to approach FFI bindings for constructs that don't fit in well with F#/.NET's expectations. It'd be great to get some more input from other folks on these since it's easy to fall into analysis paralysis when I don't have all the answers or the C experience to spot the right paths to follow in the design phase

@voronoipotato
Copy link
Contributor

This is a great idea! I'm going to keep puttering along with the lua branch because I want to make noita mods in F#, but I think C makes a better target in most cases.

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

Successfully merging this pull request may close these issues.

3 participants