Best Practices for C Marco Resolutions in Bindings?

Hello!

I have been working on a very limited/basic binding for OpenSSL and have a question regarding best practices when interacting with C libraries.

There is a design pattern within OpenSSL that relies on macros in header files to create compatibility between versions (at least this is my best guess after reading some of the comments throughout various header files). Importing the required libraries and attempting to call some functions will result in “undefined reference to ‘<function_name>’” linking errors. These errors were only present for macro mapped functions as these signatures are not present within the libraries themselves due to resolution by the c preprocessor. The following is an example from ssl.h.in that may help better describe this pattern:

// include/openssl/ssl.h.in
# define SSL_CTRL_SET_MIN_PROTO_VERSION          123

# define SSL_CTX_set_min_proto_version(ctx, version) \
        SSL_CTX_ctrl(ctx, SSL_CTRL_SET_MIN_PROTO_VERSION, version, NULL)

long SSL_CTX_ctrl(SSL_CTX *ctx, int cmd, long larg, void *parg);

My initial plan was to do a wrapper procedure that matches the macro. This was working fine, however, this design pattern has been used extensively and the wrapper functions are starting to rapidly accumulate, I am left wondering if there is a more elegant solution. The following is an small vertical segment of the current state of the binding:

when ODIN_OS == .Linux {
	LIB    :: #config(OPENSSL_LIB,    "system:libssl.so.3")
	CRYPTO :: #config(OPENSSL_CRYPTO, "system:libcrypto.so.3")
} else { 
    // ... 
}

foreign import openssl { CRYPTO, LIB }

SSL_CTX :: distinct rawptr

OSSL_Version :: enum i32 {
	// ...
}

CTX_CTRL_Command :: enum i32 {
	// ...
	SetMinProtoVersion = 123,
	// ...
}

ssl_ctx_set_min_proto_version :: #force_inline proc "c" (ctx: SSL_CTX, version: OSSL_Version) -> i32 {
	return ssl_ctx_ctrl(ctx, .SetMinProtoVersion, transmute(i32) version, nil)
}

@(default_calling_convention="c")
foreign openssl {
	@(link_name="SSL_CTX_ctrl")
	ssl_ctx_ctrl :: proc(ctx: SSL_CTX, command: CTX_CTRL_Command, larg: i32, parg: rawptr) -> i32 ---
}

I am currently left with the following ideas for how this could possibly be addressed:

  1. There is some way to include the header file locations when declaring the foreign import resources. This could alleviating the need for wrapper procedures. Possibly something like:
when ODIN_OS == .Linux {
	LIB     :: #config(OPENSSL_LIB,     "system:libssl.so.3")
	CRYPTO  :: #config(OPENSSL_CRYPTO,  "system:libcrypto.so.3")
	HEADERS :: #config(OPENSSL_HEADERS, "<header_paths>")
} else { 
    // ... 
}

foreign import openssl { CRYPTO, LIB, HEADERS }
  1. The location of the header files could be passed as an attribute, like additional linker flags, etc., on a per platform basis. Possibly something like:
when ODIN_OS == .Linux {
	LIB     :: #config(OPENSSL_LIB,    "system:libssl.so.3")
	CRYPTO  :: #config(OPENSSL_CRYPTO, "system:libcrypto.so.3")
	
	@(extra_linker_flags="<clang_include_header_flags_and_path>")
	foreign import openssl { CRYPTO, LIB }
} else { 
    // ... 
    @(extra_link_flags="<...>")
    foreign import openssl { ... }
}
  1. Continue using the wrapper procedures. They more accurately describes the behaviors of the library and are not actually that big of an issue.
  2. Something entirely different.

I am not certain if options 1 and 2 are valid in Odin, but felt like they could be possible. Looking through the work of other community bindings for projects written in c, the need to include header files does not seem to be an issue or concern.

What would you recommend as the best way forward?

Trying to include the original headers isn’t really an option, because Odin can’t parse C headers and can’t use them. At this point you really only have two options, pick one that makes the most sense for any given macro:

  • Constants
  • Force-inline procedures

There probably isn’t another mechanism in Odin that would allow capturing the behavior of macros. Depending on what exactly macro does you can add things like when blocks for different compile-time conditions, or use generics inside the procedure.

2 Likes

Thanks for responding! It indeed looks like the forced-inline procedures will be the best way forward for this particular library.