Skip to content

Commit

Permalink
Implement filtering interop headers by globs and by modulemaps
Browse files Browse the repository at this point in the history
Also do some refactoring
  • Loading branch information
SvyatoslavScherbina committed Apr 26, 2017
1 parent 94ff4d6 commit c5495e7
Show file tree
Hide file tree
Showing 6 changed files with 273 additions and 44 deletions.
46 changes: 46 additions & 0 deletions INTEROP.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,52 @@ output of config script with `--cflags` flag (maybe without exact paths).
Output of config script with `--libs` shall be passed as `-linkedArgs` `kotlinc`
flag value (quoted) when compiling.

### Selecting library headers

When library headers are imported to C program with `#include` directive,
all of the headers included by these headers are also included to the program.
Thus all header dependencies are included in generated stubs as well.

This behaviour is correct but may be very inconvenient for some libraries. So
it is possible to specify in `.def` file which of the included headers are to
be imported. The separate declarations from other headers may also be imported
in case of direct dependencies.

#### Filtering headers by globs

It is possible to filter header by globs. The `headerFilter` property value
from the `.def` file is treated as space-separated list of globs. If the
included header matches any of the globs, then declarations from this header
are included into the bindings.

The globs are applied to the header paths relative to the appropriate include
path elements, e.g. `time.h` or `curl/curl.h`. So if the library is usually
included with `#include <SomeLbrary/Header.h>`, then it would probably be
correct to filter headers with
```
headerFilter = SomeLbrary/**
```

If `headerFilter` is not specified, then all headers are included.

#### Filtering by module maps

Some libraries have proper `module.modulemap` or `module.map` files among its
headers. For example, macOS and iOS system libraries and frameworks do.
The [module map file](https://clang.llvm.org/docs/Modules.html#module-map-language)
describes the correspondence between header files and modules. When the module
maps are available, the headers from the modules that are not included directly
can be filtered out using experimental `excludeDependentModules` option of the
`.def` file:
```
headers = OpenGL/gl.h OpenGL/glu.h GLUT/glut.h
compilerOpts = -framework OpenGL -framework GLUT
excludeDependentModules = true
```

When both `excludeDependentModules` and `headerFilter` are used, they are
applied as intersection.

## Using bindings ##

### Basic interop types ###
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -360,45 +360,30 @@ fun buildNativeIndexImpl(library: NativeLibrary): NativeIndex {

private fun indexDeclarations(library: NativeLibrary, nativeIndex: NativeIndexImpl) {
val index = clang_createIndex(0, 0)!!
val indexAction = clang_IndexAction_create(index)!!
try {
val translationUnit = library.parse(index).ensureNoCompileErrors()
val translationUnit = library.parse(index, options = CXTranslationUnit_DetailedPreprocessingRecord)
try {
memScoped {
val callbacks = alloc<IndexerCallbacks>()

val nativeIndexPtr = StableObjPtr.create(nativeIndex)
val clientData = nativeIndexPtr.value

try {
with(callbacks) {
abortQuery = null
diagnostic = null
enteredMainFile = null
ppIncludedFile = null
importedASTFile = null
startedTranslationUnit = null
indexDeclaration = staticCFunction { clientData, info ->
@Suppress("NAME_SHADOWING")
val nativeIndex = StableObjPtr.fromValue(clientData!!).get() as NativeIndexImpl
nativeIndex.indexDeclaration(info!!.pointed)
}
indexEntityReference = null
}
translationUnit.ensureNoCompileErrors()

val headers = getFilteredHeaders(library, index, translationUnit)

clang_indexTranslationUnit(indexAction, clientData,
callbacks.ptr, IndexerCallbacks.size.toInt(),
0, translationUnit)
indexTranslationUnit(index, translationUnit, 0, object : Indexer {
override fun indexDeclaration(info: CXIdxDeclInfo) {
val file = memScoped {
val fileVar = alloc<CXFileVar>()
clang_indexLoc_getFileLocation(info.loc.readValue(), null, fileVar.ptr, null, null, null)
fileVar.value
}

} finally {
nativeIndexPtr.dispose()
if (file in headers) {
nativeIndex.indexDeclaration(info)
}
}
}
})
} finally {
clang_disposeTranslationUnit(translationUnit)
}
} finally {
clang_IndexAction_dispose(indexAction)
clang_disposeIndex(index)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
package org.jetbrains.kotlin.native.interop.indexer

import clang.*
import kotlinx.cinterop.CValue
import kotlinx.cinterop.*
import java.io.File

/**
Expand Down Expand Up @@ -54,9 +54,8 @@ private fun expandMacroConstants(
try {
val sourceFile = library.createTempSource()
val translationUnit = parseTranslationUnit(index, sourceFile, library.compilerArgs, options = 0)
.ensureNoCompileErrors()

try {
translationUnit.ensureNoCompileErrors()
return names.mapNotNull {
expandMacroAsConstant(library, translationUnit, sourceFile, it, typeConverter)
}
Expand Down Expand Up @@ -214,11 +213,21 @@ private fun collectMacroConstantsNames(library: NativeLibrary): List<String> {
// Include macros into AST:
val options = CXTranslationUnit_DetailedPreprocessingRecord

val translationUnit = library.parse(index, options).ensureNoCompileErrors()
val translationUnit = library.parse(index, options)
try {
translationUnit.ensureNoCompileErrors()
val headers = getFilteredHeaders(library, index, translationUnit)

visitChildren(translationUnit) { cursor, _ ->
val file = memScoped {
val fileVar = alloc<CXFileVar>()
clang_getFileLocation(clang_getCursorLocation(cursor), fileVar.ptr, null, null, null)
fileVar.value
}

if (cursor.kind == CXCursorKind.CXCursor_MacroDefinition &&
library.includesDeclaration(cursor) &&
file in headers &&
canMacroBeConstant(cursor))
{
val spelling = getCursorSpelling(cursor)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@ enum class Language {
data class NativeLibrary(val includes: List<String>,
val compilerArgs: List<String>,
val language: Language,
val excludeSystemLibs: Boolean = true)
val excludeSystemLibs: Boolean, // TODO: drop?
val excludeDepdendentModules: Boolean,
val headerFilter: (String) -> Boolean)

/**
* Retrieves the definitions from given C header file using given compiler arguments (e.g. defines).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ package org.jetbrains.kotlin.native.interop.indexer

import clang.*
import kotlinx.cinterop.*
import java.io.Closeable
import java.io.File
import java.nio.file.Paths

internal val CValue<CXType>.kind: CXTypeKind get() = this.useContents { kind }

Expand Down Expand Up @@ -198,8 +200,9 @@ internal fun NativeLibrary.precompileHeaders(): NativeLibrary {
val index = clang_createIndex(excludeDeclarationsFromPCH = 0, displayDiagnostics = 0)!!
try {
val options = CXTranslationUnit_ForSerialization
val translationUnit = this.parse(index, options).ensureNoCompileErrors()
val translationUnit = this.parse(index, options)
try {
translationUnit.ensureNoCompileErrors()
clang_saveTranslationUnit(translationUnit, precompiledHeader.absolutePath, 0)
} finally {
clang_disposeTranslationUnit(translationUnit)
Expand Down Expand Up @@ -249,9 +252,8 @@ fun List<List<String>>.mapFragmentIsCompilable(originalLibrary: NativeLibrary):
try {
val sourceFile = library.createTempSource()
val translationUnit = parseTranslationUnit(index, sourceFile, library.compilerArgs, options = 0)
.ensureNoCompileErrors()

try {
translationUnit.ensureNoCompileErrors()
while (fragmentsToCheck.isNotEmpty()) {
// Combine all fragments to be checked in a single file:
sourceFile.bufferedWriter().use { writer ->
Expand Down Expand Up @@ -292,4 +294,169 @@ fun List<List<String>>.mapFragmentIsCompilable(originalLibrary: NativeLibrary):
}

return this.indices.map { it !in indicesOfNonCompilable }
}

internal interface Indexer {
/**
* Called when a file gets #included/#imported.
*/
fun ppIncludedFile(info: CXIdxIncludedFileInfo) {}

/**
* Called to index a declaration.
*/
fun indexDeclaration(info: CXIdxDeclInfo) {}
}

internal fun indexTranslationUnit(index: CXIndex, translationUnit: CXTranslationUnit, options: Int, indexer: Indexer) {
val indexerStablePtr = StableObjPtr.create(indexer)
try {
val clientData = indexerStablePtr.value
memScoped {
val indexerCallbacks = alloc<IndexerCallbacks>().apply {
abortQuery = null
diagnostic = null
enteredMainFile = null
ppIncludedFile = staticCFunction { clientData, info ->
@Suppress("NAME_SHADOWING")
val indexer = StableObjPtr.fromValue(clientData!!).get() as Indexer
indexer.ppIncludedFile(info!!.pointed)
null as CXIdxClientFile?
}
importedASTFile = null
startedTranslationUnit = null
indexDeclaration = staticCFunction { clientData, info ->
@Suppress("NAME_SHADOWING")
val nativeIndex = StableObjPtr.fromValue(clientData!!).get() as Indexer
nativeIndex.indexDeclaration(info!!.pointed)
}
indexEntityReference = null
}

val indexAction = clang_IndexAction_create(index)
try {
val result = clang_indexTranslationUnit(indexAction, clientData,
indexerCallbacks.ptr, IndexerCallbacks.size.toInt(), options, translationUnit)

if (result != 0) {
throw Error("clang_indexTranslationUnit returned $result")
}
} finally {
clang_IndexAction_dispose(indexAction)
}
}
} finally {
indexerStablePtr.dispose()
}
}

internal class ModulesMap(
val library: NativeLibrary,
val translationUnit: CXTranslationUnit
) : Closeable {

private val index: CXIndex
private val translationUnitWithModules: CXTranslationUnit

init {
index = clang_createIndex(0, 0)!!
try {
translationUnitWithModules =
library
.copy(compilerArgs = library.compilerArgs + "-fmodules")
.parse(index)

try {
translationUnitWithModules.ensureNoCompileErrors()
} catch (e: Throwable) {
clang_disposeTranslationUnit(translationUnitWithModules)
throw e
}

} catch (e: Throwable) {
clang_disposeIndex(index)
throw e
}
}

override fun close() {
try {
clang_disposeTranslationUnit(translationUnitWithModules)
} finally {
clang_disposeIndex(index)
}
}

data class Module(private val cxModule: CXModule)

fun getModule(file: CXFile): Module? {
// `file` is bound to `translationUnit`, however `translationUnitWithModules` is used to access modules.
// Find the corresponding file in `translationUnitWithModules`:
val fileInTuWithModules =
clang_getFile(translationUnitWithModules, clang_getFileName(file).convertAndDispose())!!

return clang_getModuleForFile(translationUnitWithModules, fileInTuWithModules)?.let { Module(it) }
}
}

internal fun getFilteredHeaders(library: NativeLibrary, index: CXIndex, translationUnit: CXTranslationUnit): Set<CXFile> {
val result = mutableSetOf<CXFile>()
val topLevelFiles = mutableListOf<CXFile>()

indexTranslationUnit(index, translationUnit, 0, object : Indexer {
val headerToName = mutableMapOf<CXFile, String>()
// The *name* of the header here is the path relative to the include path element., e.g. `curl/curl.h`.

override fun ppIncludedFile(info: CXIdxIncludedFileInfo) {
val includeLocation = clang_indexLoc_getCXSourceLocation(info.hashLoc.readValue())
val file = info.file!!

if (clang_Location_isFromMainFile(includeLocation) != 0) {
topLevelFiles.add(file)
}

val name = info.filename!!.toKString()
val headerName = if (info.isAngled != 0) {
// If the header is included with `#include <$name>`, then `name` is probably
// the path relative to the include path element.
name
} else {
// If it is included with `#include "$name"`, then `name` can also be the path relative to the includer.
val includerFile = memScoped {
val fileVar = alloc<CXFileVar>()
clang_getFileLocation(includeLocation, fileVar.ptr, null, null, null)
fileVar.value!!
}
val includerName = headerToName[includerFile] ?: ""
val includerPath = clang_getFileName(includerFile).convertAndDispose()

if (clang_getFile(translationUnit, Paths.get(includerPath).resolveSibling(name).toString()) == file) {
// included file is accessible from the includer by `name` used as relative path, so
// `name` seems to be relative to the includer:
Paths.get(includerName).resolveSibling(name).normalize().toString()
} else {
name
}
}

headerToName[file] = headerName
if (library.headerFilter(headerName)) {
result.add(file)
}
}
})

if (library.excludeDepdendentModules) {
ModulesMap(library, translationUnit).use { modulesMap ->
val topLevelModules = topLevelFiles.map { modulesMap.getModule(it) }.toSet()
result.removeAll {
val module = modulesMap.getModule(it)
module !in topLevelModules
}
// Note: if some of the top-level headers don't belong to modules,
// then all non-modular headers are included.
}
}

return result
}
Loading

0 comments on commit c5495e7

Please sign in to comment.