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:
- 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 }
- 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 { ... }
}
- Continue using the wrapper procedures. They more accurately describes the behaviors of the library and are not actually that big of an issue.
- 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?