In my journey to learning C, I’ve come across header files, which are used to (I’m assuming) define a prototype for the source file, as well as structure modules. This feature, in my opinion, is pointlessly not just redundant, but possibly a source for pitfall. The same information can probably be extracted from the source code, if not for the restrictions of the language specification in C.

Say, if I have a GTK project, I will have to use the preprocessor directive, that will require the use of GTK headers that look something like #include <gtk/gtk.h>, and they’re usually in the system path. How do modern languages, like Rust, Zig or Go deal with this situation, where shared libraries are used?

  • zongor [comrade/them, he/him]@hexbear.net
    link
    fedilink
    English
    arrow-up
    3
    ·
    edit-2
    13 hours ago

    The header file was not originally made for the purpose it is used for today. In previous languages (like Fortran or COBOL) they had a preprocessor which was used for defining constants and macros and the like. The preprocessor is like a glorified cut and paste machine, it can’t do any complex processing by itself. (In fact the C preprocessor is not even Turing complete although it is close)

    The reason why the headers are included at the top is also for historical reasons. In single pass compilers a file is read line by line and parsed into an Abstract Syntax Tree; the function has to be declared before it can be used but sometimes it may be declared in a different file or later in the file. So it’s convenient to put that information in the header.

    Many modern languages use compilers that take multiple passes to generate the code. They will also use internal databases for the objects and their prototypes like a v-table to store data about the program to do optimizations and the like.

    Languages like rust, zig, and go use modules where they have namespaces where specific definitions of code are declared and able to be used later. They also had a series of built in tools like dynamically managing dependencies, linking, etc.

    For most languages they also have a Foreign Function Interface which allows them to call functions written in a different languages (like C shared libraries). All of the managers you mentioned have great FFI functionality and work well with C shared libraries. You can often use C header files in these since they give the function prototype without needing to read the whole source code and find all those definitions (often if the library is proprietary you will only have access to the shared library and the header files).

  • Perhyte@lemmy.world
    link
    fedilink
    English
    arrow-up
    2
    ·
    23 hours ago

    I’ve never used Rust or Zig, but for Go: (disclaimer: this is all from memory, so there may be inaccuracies or out-of-date information here)

    Go does not allow circular references between modules. That restriction allows the compiler, when compiling a module, to not only put the compiled machine code in the resulting object file for that module but also the information that in C would be required to be in a header file (i.e. type definitions, function signatures, and even complete functions if they’re considered candidates for inlining, etc.). When compiling a module that imports others, the compiler reads that stuff back out of those files. Essentially a compiled Go library has it’s auto-generated “header file” baked-in.

    In older versions this was actually human-readable: an early part of the object file would essentially look like trimmed-down Go when opened in a text editor. IIRC they’ve switched to a binary serialization format for this some years back, but AFAIK it still essentially works the same.

    I guess when comparing to C or C++, you could compare this to automatically generating pre-compiled headers for every module, except the headers themselves are also auto-generated (as you alluded to in your post).

    If by “shared library” you mean a dynamically linked one: IIRC Go does allow shared libraries to be used, but by default all Go code is linked statically (though libraries written in other languages may be dynamically linked by default, if you import a module that requires it).

    • Ephera@lemmy.ml
      link
      fedilink
      arrow-up
      1
      ·
      21 hours ago

      If by “shared library” you mean a dynamically linked one: IIRC Go does allow shared libraries to be used, but by default all Go code is linked statically (though libraries written in other languages may be dynamically linked by default, if you import a module that requires it).

      This is the same for Rust.

      • JoYo@lemmy.ml
        link
        fedilink
        English
        arrow-up
        1
        ·
        edit-2
        21 hours ago

        go has an abi, if unstable between versions.

        https://pkg.go.dev/internal/abi

        rust foundation has stated that they will never have an abi.

        I think both and zig support C ABI. Because Rust has no internal abi, projects will always be structurally incompatible with dynamic loading. that leaves rust with FFI.

        • Ephera@lemmy.ml
          link
          fedilink
          arrow-up
          2
          ·
          20 hours ago

          Ah yeah, I guess, my interpretation of that quote wasn’t quite right.

          You can build a shared library in Rust, but it will need to be called via the C ABI or the WebAssembly ABI.
          It is also possible to call a C ABI library in Rust (and virtually any other language), as well as a WebAssembly ABI library.
          So, technically you can do shared libraries that way, but because the C ABI and WebAssembly ABI are significantly more limited compared to what you want to be passing around internally in Rust, you’ll only really want to use these in special cases.

          Rust doesn’t particularly like dynamic libs. It compiles libraries you use on your dev machine, so it knows how you’ll use the libraries, which allows it to make various assumptions and optimizations.
          For example, Rust can do generics via monomorphization (as opposed to vtables), which means it will generate the generic library code for each type that’s passed into the generic type argument. This is useful, because it can fully optimize that code, but also because it retains type information, despite the use of generics.

  • Ephera@lemmy.ml
    link
    fedilink
    arrow-up
    1
    ·
    21 hours ago

    To my knowledge, they generally don’t. Shared libraries are a massive pain from a programming perspective, so it’s largely been avoided (even though there are some advantages for Linux distro maintainers and sysadmins).

    Modern languages usually bring along their own package manager, which downloads the libraries you want to use from a central repository and then bundles them into your build artifact that you hand to users.
    In ‘semi-modern’ languages, like the JVM languages and I think also Python, you get the dependencies in a folder as part of a larger archive.
    With Go and Rust, they decided to include those libraries directly into the executable, so that many programs can just ship a single executable.