Ease bindings to other languages providing internal interface for relaying messages and errors and taking care of sanitizers issues#608
Conversation
|
@aadler I noticed you forked nlopt as well last month. Just pinging you into this PR in case you had the same idea. |
|
This PR should address #604. |
|
Last commit brings a new |
|
So instead of a simple flag option of the type set (NLOPT_R_BINDIR "" CACHE STRING "Directory containing the R executable")When set to the directory containing the R executable, it triggers: if (NOT "${NLOPT_R_BINDIR}" STREQUAL "")
find_package(R REQUIRED)
target_include_directories(${nlopt_lib} PRIVATE ${R_INCLUDE_DIR})
target_compile_definitions(${nlopt_lib} PRIVATE NLOPT_R)
endif ()which makes all headers from R's C API available to nlopt code and defines |
aitap
left a comment
There was a problem hiding this comment.
I understand that the perfect is the enemy of the good, but since it seems that nlopt needs to go over its [f]printfs anyway, wouldn't it be better served by a layer of indirection? The optimisation methods would call an internal interface nlopt_log(opt, "format", args...), and the caller of the library would supply appropriate callbacks (such as Rprintf) to make sure that the user sees the output. The defaults for the callback (printf) could be disabled at compile time. This would avoid duplication. (Imagine if another interface needs to introduce an #ifndef NLOPT_SOME_OTHER_LANGUAGE in the same place for the same reason.)
Calling R[E]printf is not exactly safe, by the way. For example, on Rgui.exe, this checks for interrupts and would longjmp() out of the calling frame if an interrupt is found. This will at least leak memory, or wreak complete havoc if code ever becomes multi-threaded.
| find_package(R REQUIRED) | ||
| target_include_directories(${nlopt_lib} PRIVATE ${R_INCLUDE_DIR}) | ||
| target_compile_definitions(${nlopt_lib} PRIVATE NLOPT_R) | ||
| endif () |
There was a problem hiding this comment.
Will this link on Windows without an explicit reference to R.dll?
There was a problem hiding this comment.
I will double check but I am pretty sure I checked against win-builder machines. I will do it again to be sure.
| else() | ||
| set(R_PATH_LOC "${R_HOME_TEXT}/lib") | ||
| MESSAGE(STATUS "R_PATH_LOC = ${R_PATH_LOC}") | ||
| find_library( |
There was a problem hiding this comment.
At least on Linux, R defaults to --disable-R-shlib, so there won't be a libR.so in R.home('lib'). (But a distro-built R will usually be compiled with --enable-R-shlib because distros have a strong preference for dynamic linking.) This won't prevent you from linking and loading a shared library (because the dynamic loader will resolve the symbols from the ones exported by the R executable), but any example applications will fail to link.
There was a problem hiding this comment.
Thanks for this feedback. Do you know how I could improve the FindR.cmake file to circumvent this issue?
| #ifndef NLOPT_R | ||
| #define ASRT(c) if (!(c)) { fprintf(stderr, "DIRECT assertion failure at " __FILE__ ":%d -- " #c "\n", __LINE__); exit(EXIT_FAILURE); } | ||
| #else | ||
| #define ASRT(c) if (!(c)) { Rf_error("DIRECT assertion failure at " __FILE__ ":%d -- " #c "\n", __LINE__); } |
There was a problem hiding this comment.
Rf_error would longjmp out of the call frame, bypassing any destructors owned by nlopt and your own destructor. Could there be a way to return an error code from the function instead of this? Still, leaking some memory is better than crashing the process.
There was a problem hiding this comment.
Originally I wanted to avoid the need to call for Rcpp or cpp11 functions within nlopt codebase because this would make a forced choice for R users to use one of them.
But I think it could make sense to force the use of cpp11 as I recall they carefully dealt with longjmps.
| extern const char *nlopt_set_errmsg(nlopt_opt opt, const char *format, ...) | ||
| #ifdef __GNUC__ | ||
| #ifndef NLOPT_R | ||
| __attribute__ ((format(printf, 2, 3))) |
There was a problem hiding this comment.
Are you sure this is needed? R CMD check goes through the names of the imports of the package shared library and compares them to entries of tools:::so_symbol_names_table, it doesn't look at the attributes.
I agree with you. However, there are already existing bindings for many languages. I do not know how these have been achieved in the past. A reflection on this together with a layer of indirection to accommodate any past and future languages (maybe Julia aficionados) could be a long-term project on which we could work as a small group. On a shorter term, nloptr does not pass CRAN checks anymore because in the case of it being built from included sources (which are the latest 2.10), CRAN now checks for calls to these functions ( Of course, I will answer the questions you raised in your review first.
You are right indeed. I think posit folks have come up in their cpp11 package with ways to deal with this. I will investigate. |
… avoid further source code modification when a new language wants to provide a binding.
…to downstream language the way output and error messages should be delayed or by fixing the code base directly to use C functions that are compatible with CRAN rules.
|
@aitap is this something more along the lines of what you had in mind? And to provide an example with the R language, I updated the sandbox package {nloptrbundled}: https://github.com/astamm/nloptrbundled. Now there is no mention of R and the message and error relays are handled by downstream languages which seek to provide a binding. The R package example passes all CRAN checks on windows, Mac and linux successfully. Let me know if this can be considered for the main branch. |
| double rho, *lambda, *mu; | ||
| double *restmp, *gradtmp; | ||
| nlopt_stopping *stop; | ||
| nlopt_func f; |
There was a problem hiding this comment.
All of these extraneous formatting and whitespace changes make this PR very difficult to review.
There was a problem hiding this comment.
Oh my. So sorry about that. Automatic formatting in my IDE that I forgot to turn off. I can try to revert that.
There was a problem hiding this comment.
I can't seem to separate well functional changes from formatting ones. What are the linting rules for nlopt? 4 spaces, no whitespaces, curly open braces on new line?
There was a problem hiding this comment.
I see different conventions in different source files. Am I wrong? If we can agree on conventions, I can reformat everything accordingly.
There was a problem hiding this comment.
Something like that should match the convention used in most files:
---
# Clang-format configuration derived from the observed style in bobyqa.c and
# other NLopt C source files under src/libs/.
#
# Key characteristics:
# - 4-space indentation (spaces in function bodies; tabs only for parameter-
# list continuation alignment are left as-is via UseTab: ForIndentation)
# - Linux brace style: function-definition opening brace on its own line,
# control-flow opening brace on the same line
# - No column-limit enforcement (f2c-generated code has very long lines)
# - Pointer qualifier attached to the variable name (double *x)
# - No include sorting (includes are grouped intentionally)
Language: Cpp
BasedOnStyle: LLVM
# Indentation
IndentWidth: 4
TabWidth: 8
UseTab: ForIndentation
ContinuationIndentWidth: 4
# Braces — Linux style: function body brace on new line, rest on same line
BreakBeforeBraces: Linux
# Pointer / reference qualifier attached to the variable name
PointerAlignment: Right
# Line length — disabled; legacy f2c code has lines well beyond 80 columns
ColumnLimit: 0
# Short constructs
AllowShortFunctionsOnASingleLine: None
AllowShortIfStatementsOnASingleLine: Never
AllowShortLoopsOnASingleLine: false
AllowShortBlocksOnASingleLine: Empty
# Alignment — keep off to avoid churning legacy code
AlignConsecutiveAssignments: None
AlignConsecutiveDeclarations: None
AlignTrailingComments:
Kind: Never
# Spaces
SpaceAfterCStyleCast: false
SpaceBeforeParens: ControlStatements
SpaceInEmptyParentheses: false
# Includes — do not sort; order is intentional in NLopt sources
SortIncludes: Never
IncludeBlocks: Preserve
Should I apply this everywhere? Would that suit you?
It can be stored in a .clang-format hidden file at the root of the project so that we always follow these conventions. Let me know if this is something you would consider.
There was a problem hiding this comment.
However, whitespaces after commas are automatically (in the sense hardcoded) emitted by clang formatter.
Also, the formatting style differs from the one adopted by the f2c tool that has been used for translating Fortran code. Do you have strong formatting rules for the nlopt project? Defining such rules in a file like .clang-format as I propose would avoid future formatting issues we are facing here.
There was a problem hiding this comment.
Here's what can be tried to avoid digging the problem even deeper:
- Create a new branch from
git merge-base master r-compatibility, let's call itmaster-formatted - Format it using the closest
clang-formatsettings you can find - Format
r-compatibilityusing the same settings - Compute the diff from
master-formattedto formattedr-compatibility - Apply it on top of the original
git merge-base master r-compatibility, solve the few remaining conflicts, restore the original code style, split into logical commits
This is probably easier than disentangling the formatting changes from semantic changes in the current branch.
There was a problem hiding this comment.
Once found closest clang-format settings, don't we want to stick with it?
Do you already have strong formatting conventions for nlopt?
There was a problem hiding this comment.
That's not for me to decide. I'm sorry I did not make it clear enough that I'm only giving advice (motivated by what I think is best for NLopt and the R community). In my experience, PRs that reformat the entire code base don't go well.
|
Don't use For this PR, use a different IDE if you have to — get an editor where you can make localized changes without doing wholesale reformatting. I'm simply not going to a merge a PR that includes 10,000 lines of extraneous formatting changes. |
|
On it, on it. |
|
We should be good now. |
| { | ||
| double *volatile _v = w; | ||
| w = _v - 1; | ||
| } |
There was a problem hiding this comment.
This change seems unrelated to the topic of the PR?
There was a problem hiding this comment.
You'll find this pattern repeatedly. It fixes GCC undefined behavior sanitizer issues that were spotted when I copy-pasted nlopt sources in the nloptrbundled package so I provided the fix. I believe it should improve compatibility with any downstream language that would like to provide a binding of nlopt. Hence, its presence here. The name of the PR though could be changed to better reflect what the provided solution actually does wrt the original intent.
|
Please let me know if I can do something else to get this PR through. It would be really helpful for downstream packages in other languages that provide bindings to nlopt. Thanks! |
|
Can I do more to get this PR going on? Please let me know @stevengj. Thanks a lot! |
|
Any update on this? What else can I do to move this PR forward please? I'd like to get a new release of nloptr out soon on CRAN with this improvement. Thanks. |
This is useful for providing nlopt to the R community while satisfying CRAN requirements.
Specifically,
CMakeLists.txtgains the optionUSE_R_COMPATIBILITY:which, when turned ON, triggers the following definition:
The variable
CRAN_COMPATIBILITYis then used in source files to prevent the use of functions forbidden by CRAN such asstderr,abort,printf.This PR provides the necessary fixes for the C API. There are also known uses of
std::cerrandstd::coutnotably in the STOGO implementation that could also be avoided usingCRAN_COMPATIBILITY. It might later be good to make also the C++ API R-safe.