Access C++ library dependency & linkage from plugin? Trying to get dllimport, dllexport working correctly


(Eric W. Barndollar) #1

Hi all,

I’m trying to get multi-project C++ builds working with Gradle, but I can’t find a way to support dllimport and dllexport __declspec declarations in a way that will work for both shared and static library linkage. I looked at the gradle sample project in samples/native-binaries/multi-project/, but it only works with shared (not static) linkage. I want both to work.

I can get the library itself to compile with right declarations (dllexport in the VisualCpp shared library case & omitted otherwise), but I can’t find a way to define a pre-processor macro that varies based on whether the executable using the library declares it like this (where I need dllimport in the #included header):

sources {
    hello {
      cpp {
        lib project: ':hellolib', library: 'greetings', linkage: 'shared'
      }
    }
  }

or like this (where it should NOT use any dllimport declarations in the #included header):

sources {
    hello {
      cpp {
        lib project: ':hellolib', library: 'greetings', linkage: 'static'
      }
    }
  }

So is there a way to get programmatic access to this library dependency and its linkage? Or is there other planned support coming that will let me write a library that can be linked either way? Thanks for any help!

  • Eric

(Eric W. Barndollar) #2

Found a way to make this work. By adding an afterEvaluate action to the project, I can iterate over project.sources and then src.cpp.libs for each source.

So my plugin now does something like this:

public class MyCppPlugin implements Plugin<Project> {
  void apply(Project project) {
    project.apply(plugin: 'cpp')
      project.afterEvaluate {
      // Setup PROJECT_LIBRARY_DLLSPEC for exported libraries.
      project.libraries.each { lib ->
        setupDllExportSpec(project, lib.name,
            toMacroName(project.name, lib.name, "DLLSPEC"))
      }
        // Setup PROJECT_LIBRARY_DLLSPEC for imported libraries.
      project.sources.each { src ->
        src.cpp.libs.each { lib ->
          if (lib in Map) {
            setupDllImportSpec(project, src.name, lib.get("linkage"),
                toMacroName(lib.get("project"), lib.get("library"), "DLLSPEC"))
          }
        }
      }
    }
  }
    // ...
}

Then my setupDllImportSpec() and setupDllExportSpec() methods just iterate over project.binaries and look for matching library/binary names, defining the appropriate cppCompiler preprocessor macro to be __declspec(dllimport), __declspec(dllexport), or the empty string based on linkage.

Note also that I’m using PROJECT_LIBRARY_DLLSPEC macros instead of a single non-namespaced LIB_FUNC macro, since one exported shared library could depend on a different static library.

If there’s a simpler way to do this, I’m definitely open to suggestions. Otherwise, hope this answer helps anyone else trying to accomplish the same thing.


#3

Glad to hear you found a way to get it working, but using ‘project.afterEvaluate’ is not all that nice!

Just to clarify:

  1. You are trying to produce 2 different executable variants from the same source code, and these vary based on whether they link to the static or shared variant of a dependency library? 2) The executable variants need to be compiled with different parameters in order to selectively include ‘__declspec(dllimport)’ in the header files when it’s linking to the dll, but omit this when linking to the static library?

To produce the different variants, you’ll need to either define these as 2 different executables built from the same source code, or as 2 different ‘flavors’ of the same executable. Assuming the latter, then you could simply specify the correct defines when compiling the variant:

binaries.all {
    if (flavor.name == 'sharedlink') {
        cCompiler.define 'DLLIMPORT'
    }
}

Alternatively, you could do what you’re doing above, but do it inside a ‘binaries.all’ block instead of ‘project.afterEvaluate’.

binaries.withType(ExecutableBinary) {
    source.withType(DependentSourceSet) { src ->
       src.libs.each { lib ->
            if (lib in Map && lib['linkage'] != 'static') {
                cppCompiler.define 'DLLIMPORT'
            }
       }
    }
}

Quite possibly this doesn’t solve the complexities of your use case. I’d be interested to hear about ways we might make this simpler.


(Eric W. Barndollar) #4

Hi Daz,

  1. I’m not necessarily trying to produce 2 executable variants (but I’d certainly like to have that flexibility). I’m just approaching this from the standpoint of: how do I write a C++ library in Gradle? Since the library itself isn’t declared to be either a “static library” or a “shared library” (since the ‘cpp’ plugin creates tasks for both linkage types), the linkage of the library is up to the binary that depends on it. So I’m trying to figure out how to write library source code & headers that can be correctly linked as either a static or a shared library.

  2. Yes.

Also, it shouldn’t be a single global preprocessor definition like DLLIMPORT, because I could, say, link libraryA as a static library but libraryB as a shared DLL. So I probably need unique defines for each library (maybe LIBRARY_A_DLLIMPORT and LIBRARY_B_DLLIMPORT).

I can look into using ‘binaries.all’ instead of ‘project.afterEvaluate’.

As for simpler alternatives, I can think of 2:

(a) The library configuration itself could dictate linkage. Something like:

staticlibraries {
  mystaticlib {...}
}
sharedlibraries {
  mysharedlib {...}
}
apilibraries {
  myheaderonlylib {...}
}

and then library binaries would only be created for a single linkage type.

(b) Maybe infeasible, but the simplest solution as a developer would be for me to not have to specify any __declspec declarations at all. Then the Gradle C++ plugin could insert the correct declarations in the source and headers when building the library and when exporting headers based on the build file specified linkage.

Thanks for the help!


#5

The way I’ve had this working (and works in all samples except the multiproject one) is to avoid using ‘dllimport’ entirely. But I can get away with that because my code is dead simple.

The way I see it, the header file belongs to the library. And in this case, there is effectively a different header file for the static or shared linkages of the library. I would prefer a solution that made it easy for libraries to be packaged in this way: so that when linking to the static variant the executable would automatically get the ‘declspec-free’ version of the library. This should work for project dependencies like we have now, as well as for downloaded and resolved dependencies.

Or perhaps the library itself contributes to the compiler defines of the consuming executable. So the metadata for a shared library would say “here are my headers, you must compile with /Dmylib_dll_import”, and Gradle would automatically define that macro.

For now, you don’t need to write your library code so that it can be consumed as both a static and shared library. If it’s always a shared library, and you only consume it as such, then your code can be simpler. If you want the ability to produce both, you need to do some extra work, and there is going to be some knowledge in the consumer of how to set the correct compiler args for the headers.

(a) The library configuration itself could dictate linkage.

At some stage we’ll make it possible to declare that a library is always static, always shared, or both. Then you won’t need to declare a linkage in the consumer when there is only one option.