Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

build: Improvements to symbol visibility logic on Windows #1362

Closed

Conversation

real-or-random
Copy link
Contributor

@real-or-random real-or-random commented Jun 27, 2023

This is a somewhat more elaborate alternative to #1346.

As I said there:

Many hours of researching and experimenting went into that piece of code, so I'll probably follow up with a PR that adds extensive comments.

@real-or-random real-or-random added user-documentation user-facing documentation build labels Jun 27, 2023
@real-or-random
Copy link
Contributor Author

Ok, ready for review (if CI is happy).

This PR also addresses this item in #1235 now:

[ ] Consider changing DLL_EXPORT TO SECP256k1_DLL_EXPORT (#1113 (comment))

I've chosen to use the SECP256K1_DLL macro instead. The same macro is also used for requesting that one wants to consume libsecp256k1 as a DLL, i.e., when !defined(SECP256K_BUILD). I think this is convenient for the user. If a user builds a DLL, then it's likely that the user also intends to consume the DLL in a program. (This is just for convenience. In the end, building the library and consuming the library are two entirely different build processes, so the reusing the macro name is never ambiguous on a technical level.)

@real-or-random real-or-random marked this pull request as ready for review June 28, 2023 10:22
@real-or-random
Copy link
Contributor Author

cc @theuni

@hebasto
Copy link
Member

hebasto commented Jun 28, 2023

Draft because I still need to incorporate the cmake/autotools changes from #1346.

Outdated?

@hebasto
Copy link
Member

hebasto commented Jun 28, 2023

Out of scope of this PR, but shouldn't CI has a Cygwin build as well?

real-or-random and others added 3 commits June 28, 2023 12:35
This makes uses of the freshly introduced `SECP256K1_STATICLIB` macro
instead of ignoring MSVC linker warnings LNK4217 and LNK4286.

Co-authored-by: Tim Ruffing <[email protected]>
@real-or-random
Copy link
Contributor Author

Draft because I still need to incorporate the cmake/autotools changes from #1346.

Outdated?

Yes, I removed it from the initial comment.

Out of scope of this PR, but shouldn't CI has a Cygwin build as well?

In general, I guess more testing never hurts. Is Cygwin still in wide use?

@hebasto
Copy link
Member

hebasto commented Jun 28, 2023

Out of scope of this PR, but shouldn't CI has a Cygwin build as well?

In general, I guess more testing never hurts. Is Cygwin still in wide use?

I don't have any particular numbers, but it seems WSL undermined Cygwin's purpose :)

Anyway, if code written for a platform that platform should be tested. If it not feasible/reasonable to test a platform, no need to maintain platform-specific code, no?

@real-or-random
Copy link
Contributor Author

Ok, yeah, the reason I included the Cygwin commit is that checking defined(__CYGWIN__) is suggested in https://gcc.gnu.org/wiki/Visibility (and Cygwin does not define _WIN32).

Anyway, if code written for a platform that platform should be tested.

I think that's a good rule of thumb, but I also think that in this case maintaining half a line of code is okay as a best-effort thing, even if we don't test it. In particular, because it's just build system code. (If we used different instructions on Cygwin, then we would need a test for sure.)

Copy link
Member

@hebasto hebasto left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Concept ACK.

[ ] Consider changing DLL_EXPORT TO SECP256k1_DLL_EXPORT (#1113 (comment))

Does it make sense to replace s/DLL_EXPORT/SECP256k1_DLL/ in the src/CMakeLists.txt?

# define SECP256K1_API
# define SECP256K1_API_VAR extern __declspec (dllimport)
# elif defined DLL_EXPORT
# elif defined(DLL_EXPORT)
Copy link
Member

@hebasto hebasto Jun 28, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shoudn't it be OR'ed with defined(SECP256k1_DLL)?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, if SECP256K1 is defined, then we would have hit the #elif defined(SECP256K1_DLL) above.

An alternative option is to drop this "guess" based on EXPORT_DLL entirely. We took this from the libtool manual which justifies it as follows:

For older GNU tools and other proprietary tools there is no generic way to make it possible to consume either of the DLL or the static library without user intervention, the tools need to be told what is intended. One common assumption is that if a DLL is being built (‘DLL_EXPORT’ is defined) then that DLL is going to consume any dependent libraries as DLLs. If that assumption is made everywhere, it is possible to select how an end-user application is consuming libraries by adding a single flag ‘-DDLL_EXPORT’ when a DLL build is required. This is of course an all or nothing deal, either everything as DLLs or everything as static libraries.

(https://www.gnu.org/software/libtool/manual/html_node/Windows-DLLs.html#Windows-DLLs)

But with this PR, we get better methods for telling the tools what is intended. And I believe that "Explicit is better than implicit" and "In the face of ambiguity, refuse the temptation to guess.".

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think? Should we remove this branch entirely?

Copy link
Member

@hebasto hebasto Jun 29, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove it. As a Libtool-only convention, using the DLL_EXPORT macro when consuming the library looks weird.

But with this PR, we get better methods for telling the tools what is intended.

Indeed.

Comment on lines +166 to +167
* the following is that it may provoke linker warnings LNK4217 and LNK4286.
* See "Windows DLLs" in the libtool manual. */
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a recommendation to use the SECP256K1_STATICLIB macro?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had a sentence in an earlier version, but I removed it because it's probably clear by "No method requested explicitly." Let me bring it back.

@real-or-random
Copy link
Contributor Author

Does it make sense to replace s/DLL_EXPORT/SECP256k1_DLL/ in the src/CMakeLists.txt?

Good point, will do.

* imported (i.e., not only variables are imported), which should be the case
* for any meaningful program that uses the libsecp256k1 API. The drawback of
* the following is that it may provoke linker warnings LNK4217 and LNK4286.
* See "Windows DLLs" in the libtool manual. */
# define SECP256K1_API
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While touching this code, we can exploit one more optimization.

From Microsoft docs:

Using __declspec(dllimport) is optional on function declarations, but the compiler produces more efficient code if you use this keyword.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed, but we shouldn't touch it here in the # elif defined(_MSC_VER) branch.

The specific combination

#  define SECP256K1_API
#  define SECP256K1_API_VAR  extern __declspec (dllimport)

is precisely what always works for MSVC (if one is willing to accept the warning):

With Microsoft tools you typically get away with always compiling the code such that variables are expected to be imported from a DLL and functions are expected to be found in a static library. The tools will then automatically import the function from a DLL if that is where they are found. If the variables are not imported from a DLL as expected, but are found in a static library that is otherwise pulled in by some function, the linker will issue a warning (LNK4217) that a locally defined symbol is imported, but it still works.

https://www.gnu.org/software/libtool/manual/html_node/Windows-DLLs.html#Windows-DLLs


But in general, yes, the introduction of SECP256K1_DLL here will make it possible for the user to force __declspec(dllimport) on functions, which is faster according to the docs. We could add this to our linking jobs in autotools/CMake, though I doubt that it's measurable, given that API calls for us typically involve some expensive cryptographic operation anyway -- saving a few CPU instructions is probably not a big deal.

@hebasto
Copy link
Member

hebasto commented Jun 28, 2023

Approach ACK 5267c8b.

While testing a static library using MSVC, the following warning has been noticed:

LINK : warning LNK4217: symbol 'secp256k1_ellswift_xdh_hash_function_bip324' defined in 'libsecp256k1.lib(secp256k1.obj)' is imported by 'bench.obj' in function 'bench_ellswift_xdh' [C:\Users\ContainerAdministrator\AppData\Local\Temp\cirrus-ci-build\build\src\bench.vcxproj]

which is a new one after merging #1129.

To tackle with it, suggesting the following diff (+ #1362 (comment)):

diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt
index a314c17..e2ea473 100644
--- a/examples/CMakeLists.txt
+++ b/examples/CMakeLists.txt
@@ -6,9 +6,6 @@ target_link_libraries(example INTERFACE
   secp256k1
   $<$<PLATFORM_ID:Windows>:bcrypt>
 )
-if(NOT BUILD_SHARED_LIBS AND MSVC)
-  target_compile_definitions(example INTERFACE SECP256K1_STATICLIB)
-endif()
 
 add_executable(ecdsa_example ecdsa.c)
 target_link_libraries(ecdsa_example example)
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 0bba199..4e62f61 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -20,10 +20,9 @@ if(SECP256K1_ASM STREQUAL "arm32")
   target_link_libraries(secp256k1_asm INTERFACE secp256k1_asm_arm)
 endif()
 
-# Define our export symbol only for Win32 and only for shared libs.
-# This matches libtool's usage of DLL_EXPORT
 if(WIN32)
-  set_target_properties(secp256k1 PROPERTIES DEFINE_SYMBOL "DLL_EXPORT")
+  set_target_properties(secp256k1 PROPERTIES DEFINE_SYMBOL SECP256K1_DLL)
+  target_compile_definitions(secp256k1 INTERFACE $<$<NOT:$<BOOL:${BUILD_SHARED_LIBS}>>:SECP256K1_STATICLIB>)
 endif()
 
 # Object libs don't know if they're being built for a shared or static lib.

@hebasto
Copy link
Member

hebasto commented Jun 29, 2023

I'm still thinking about user's definitions during the consuming of the library.

Now, at the master branch, none of them are required or expected from the user. The _MSC_VER is defined by the compiler, the DLL_EXPORT by the Libtool.

This PR (5267c8b) introduces two optional macros: SECP256K1_STATICLIB and SECP256K1_DLL which are mutually exclusive. Then we have three code paths to handle.

Another approach is to make the SECP256K1_STATICLIB macro obligatory when consuming the static library. Not defining it should be considered as consuming the shared library. In this approach the elif defined(_MSC_VER) branch might be dropped altogether. And two macros SECP256K1_API and SECP256K1_API_VAR can be merged into one.

What drawbacks did I miss here?

@real-or-random
Copy link
Contributor Author

Doesn't the following set SECP256K1_DLL even if we're building a static lib?

 if(WIN32)
  set_target_properties(secp256k1 PROPERTIES DEFINE_SYMBOL SECP256K1_DLL)
  [...]
 endif()

And the current code on master does the same despite the comment saying "only for shared libs". I assume having dllexport just doesn't hurt in this case? Or am I getting this wrong?

@hebasto
Copy link
Member

hebasto commented Jun 29, 2023

Doesn't the following set SECP256K1_DLL even if we're building a static lib?

 if(WIN32)
  set_target_properties(secp256k1 PROPERTIES DEFINE_SYMBOL SECP256K1_DLL)
  [...]
 endif()

And the current code on master does the same despite the comment saying "only for shared libs". I assume having dllexport just doesn't hurt in this case? Or am I getting this wrong?

From CMake docs:

DEFINE_SYMBOL sets the name of the preprocessor symbol defined when compiling sources in a shared library.

@real-or-random
Copy link
Contributor Author

Another approach is to make the SECP256K1_STATICLIB macro obligatory when consuming the static library. Not defining it should be considered as consuming the shared library. In this approach the elif defined(_MSC_VER) branch might be dropped altogether. And two macros SECP256K1_API and SECP256K1_API_VAR can be merged into one.

What drawbacks did I miss here?

That's indeed simpler. And I don't think you missed any drawbacks. The only drawback is that MSVC users are required to define SECP256K1_STATICLIB when consuming a static lib. In other words, the advantage of the current approach is that it "just works" on MSVC. I think that's neat, but on the other hand, I tend to agree: it was never truly clean due to the linker warnings (which are hard to debug for the user etc...) and I argued above that explicit is better than implicit.

I wonder how other libraries are doing this?

@real-or-random real-or-random marked this pull request as draft June 29, 2023 08:48
@hebasto
Copy link
Member

hebasto commented Jun 29, 2023

I wonder how other libraries are doing this?

CMake-based projects might exploit the GenerateExportHeader module and use the ${LIB}_STATIC_DEFINE, for instance: google/benchmark#1372 (comment).

@hebasto
Copy link
Member

hebasto commented Jun 29, 2023

... the elif defined(_MSC_VER) branch might be dropped altogether.

Here are two branches to prove this concept:

And I believe that "Explicit is better than implicit" and "In the face of ambiguity, refuse the temptation to guess.".

"Simple is better than complex." :)

@real-or-random
Copy link
Contributor Author

I wonder how other libraries are doing this?

By the way, grep -10 dllimport -r /usr/include on a production system is good to get a feeling for this.... Most libraries have some macro that the user needs to set, and only very few follow the libtool recommendation.

Also, grep -10 dllimport -r /usr/include | grep STATIC shows that suffix _STATIC is more common than _STATICLIB. (https://gitlab.kitware.com/cmake/cmake/-/issues/23195#note_1139808 agrees with this.)

I'll rework the PR based on these observations, but probably not this week. I'd also be happy to pass it back to you @hebasto if you're willing to work on this.

@hebasto
Copy link
Member

hebasto commented Jun 29, 2023

I'd also be happy to pass it back to you @hebasto if you're willing to work on this.

I'll take it :)

@hebasto
Copy link
Member

hebasto commented Jun 29, 2023

I'd also be happy to pass it back to you @hebasto if you're willing to work on this.

I'll take it :)

Done in #1367.

real-or-random added a commit that referenced this pull request Jul 3, 2023
…s (attempt 3)

c6cd2b1 ci: Add task for static library on Windows + CMake (Hennadii Stepanov)
020bf69 build: Add extensive docs on visibility issues (Tim Ruffing)
0196e8a build: Introduce `SECP256k1_DLL_EXPORT` macro (Hennadii Stepanov)
9f1b190 refactor: Replace `SECP256K1_API_VAR` with `SECP256K1_API` (Hennadii Stepanov)
ae9db95 build: Introduce `SECP256K1_STATIC` macro for Windows users (Hennadii Stepanov)

Pull request description:

  Previous attempts:
  - #1346
  - #1362

  The result is as follows:
  1. Simple, concise and extensively documented code.
  2. Explicitly documented use cases with no ambiguities.
  3. No workarounds for linker warnings.
  4. Solves one item in #1235.

ACKs for top commit:
  real-or-random:
    utACK c6cd2b1

Tree-SHA512: d58694452d630aefbd047916033249891bc726b7475433aaaa7c3ea2a07ded8f185a598385b67c2ee3440ec5904ff9d9452c97b0961d84dcb2eb2cf46caa171e
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
build user-documentation user-facing documentation
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants