Repository navigation
C ABI missing some functions that are available in gdxcc.h #6
Description
Activity
Good question, @kdheepak. The GDX dynamic library is built with the GDX sources and
gdxcclib.cppand this does not include somegdxcc.{h,c}-exclusive functions likegdxGetExceptionIndicator. Both are generated from a YAML description of the API by our custom apigenerator-tool.The purpose of the
gdxccis having a convenient and portable interface to load and use the GDX DLL/.so/.dylib from a C project. As such, it offers some extra functionality related to library loading on top of exposing the actual API surface of the GDX library. A core feature is robust loading of API entrypoints (and error handling) that tries different casing which I believe has also something to do with the Delphi origins of GAMS.So I would say it is intentional the dynamic library does not contain these functions in its ABI. The intended API is only the gdx* functions available as
C__gdx*in the dynamic library. And the "single source of truth" reference of what is part of the API (and what is not) is what is in thesrc/gdxapi.yamlwhich should be consistent with thegdx*-methods in theTGXFileObjwhich can be used in C++ either directly or through the library viagdxcppwrap.hpp.Does this help? I see this is somewhat confusing and am considering, if this should be also explicitly mentioned in the docs somewhere.
Reacted by Dheepak Krishnamurthy(I'm assuming you meant to link to gdxapi.yaml and apigenerator)
Thanks that helps! And thanks for the pointers and for the fast responses!
The reason I was asking was that Pyomo appears to be assuming this function will always exist, e.g. see this line in the GAMS.py solver in Pyomo. I'm assuming this code will fail if the user has the bundled Python GAMS API in their path, because pyomo will import that first, and then will call the unload function (which would work on the
gdxccpackage but not ongams.core.gdxpackage).Specifically, this kind of issue arises when a programming language can generate bindings from the C code or header files directly. For example, when using SWIG in Python can generate a new DLL that exposes python friendly functions by generating C code. So if the SWIG interface binds with
gdxLibraryUnload, that will be exposed in python as well, like how it is done here (generated code here). I imagine this will also happen in Rust's bindgen, Zig, Java, Nim etc when not using dynamic loading.I'll report this particular issue to Pyomo (and link to this issue), I'm not sure if others are using the same pattern so maybe the issue is not widespread, just something to be aware of.
I think documenting this would be nice. Looking through the source code more, all these functions that are missing in the dynamic library seem to be associated with just loading the library (there's functions for loading, checking whether it is loaded, setting error messages during loading, mutexes for locking the variable holding a reference etc). I think even if I wanted to make a Rust interface for example, I'd be able to use
dlopenand not bother with any of these functions. Let me know if I'm mistaken about any of this.(I'm assuming you meant to link to gdxapi.yaml and apigenerator)
Oooops. Yes, I mixed up our internal copies and the mirrored GitHub and put in the wrong links. Thank you for pointing that out!
Specifically, this kind of issue arises when a programming language can generate bindings from the C code or header files directly.
Good point. Maybe the
generated/gdxcwrap.hcould be a more suitable source for generating bindings as its interface isn't including the "noise" from the dynamic library loading/unloading related code and logic (afaics). But I am now curious, how the extra function endpoints ingdxcc.hwill mess with binding generators of popular languages like Rust and if we should adapt our docs to point people interested in generating bindings for their language of choice to a suitable header file or if we can modify the template forgdxcc.h, so automatic binding generators ignore the superfluous functions.I think even if I wanted to make a Rust interface for example, I'd be able to use dlopen and not bother with any of these functions. Let me know if I'm mistaken about any of this.
Yes, that's my understanding. I haven't tried, though.
I will leave the issue open to remind myself to look into ways to make GDX more approachable for other wrapper/binding generators. If we don't maintain bindings ourselves, we at least should remove unnecessary hurdles. 👍
Maybe the generated/gdxcwrap.h could be a more suitable source
That seems like a good idea to me, with the caveat that I have a limited understanding of the goals of the generated code.
But I am now curious, how the extra function endpoints in gdxcc.h will mess with binding generators of popular languages
My memory of this is a little hazy at the moment but I remember it being dependent on the generator. For example, SWIG parsers a header file using their own built in parser, and you can provide it rules for what the header should should contain. You can be explicit in this "interface" file, or use SWIG specific rules. Julia on the other hand uses Clang.jl to generate bindings, so it can handle more nuances of a header file correctly. I think the "export" was important to get right, for it to show up in the generated code and in the shared library (it has to be an exported function to show up in the shared library, but ignored or a noop in binding generators).
In past work I've been involved in we generated just one header file from the cmake build process, and that header file has:
- A
..._EXPORTmacro - An
extern Cblock for the entire file - opaque pointers that are typedef'd
- Every C function namespaced
- Every C function documented using doxygen conventions
That makes it somewhere between trivial to relatively easy to port to Python / Julia / Rust etc.
This header file can also easily be consumed by libraries like
cffifor Python. cffi for example can read the headers directly as a string:from cffi import FFI ffibuilder = FFI() # cdef() expects a single string declaring the C types, functions and # globals needed to use the shared object. It must be in valid C syntax. ffibuilder.cdef(""" float pi_approx(int n); """)
In Julia we used
Clang.jlto generate the Julia code from the header files to build idiomatic interfaces on top of that.And we only need to ship the single header file and the shared library for this to work. We also shipped static libraries but that was in case someone wanted to build a static version.
My past experience was focused primarily on dynamic linking using dlopen. I'm sure there are many ways to do this and may depend on the initialization of the library + the goals of the bindings. Just sharing all this for your reference, and happy to talk about it more too :)
Reacted by James FosterReacted by André Schnabel- A
I noticed that some functions are not available in the dynamic library (inspected with
nmon MacOS) but are available as part of the gdxcc.h header file.I'm not sure if this is intended or if this should be a bug. Can you clarify is the intended API is
c__gdx...handle-oriented API, ORNotes:
c__gdx...symbols seem to be present in the dynamic library.gdxcreate,gdxcreated,gdxfree,xcreate, andxfreeare present. TheseIs this intentional? If yes, should API interfaces not expose functions like
gdxLibraryLoadedorgdxLibraryUnload?