diff --git a/kotlin-native/CPLUSPLUS.md b/kotlin-native/CPLUSPLUS.md new file mode 100644 index 00000000000..569fc35f2dc --- /dev/null +++ b/kotlin-native/CPLUSPLUS.md @@ -0,0 +1,43 @@ +Current status +============== + +Limited C++ interop provided via plain C wrappers and cinterop mechanism. + +Usage: +------ + +Add `Language = C++` to .def file or `compilerOpts = -x c++` to command line or def file. + +C++ features +------------ + +* C++ class: + + Virtual and non-virtual methods + * Static methods mapped as companion ones + * Fields + * Static fields as companion ones + * Constructors are mapped to `__init__(args)` companion methods + * Destructor is mapped to `__destroy__(this)` companion method +* LValueReference parameters and return value internally handled as pointers +* Namespaces provided as simple mangled class name. It works but awfully ugly. Shall be fixed by mapping to packages. +* Nested C++ classes: same as namespaces, simple mangling. Shall be fixed. +* Access modifiers: only public members exposed to Kotlin. Anonymous namespaces are silently ignored. + +Known issues +------------ + +* C++ object with nontrivial copy constructor may work incorrect when used as by value parameter or return typr. +"Nontrivial" in this context relates to objects which bahavior depends on memory location. Most of other non-POD objects may be handled well. +In fact, "by value" return type is mapped to CValue which is immutrable movable block. Particularly, non-const methods invoked with CValue receiver +via useContents mechanism run on temporary object and therefore does not modify the original copy. TBD. + +Limitations +----------- + +* Operators are not supported yet (silently ignored) +* LVReference is mapped to CPointer? which is incorrect (should be notNull). This may cause segmentation fault in case of null would be sent as a parameter. TBD +* const overload not supported and cause compilation error. That is, two class methods with the same signature (`const` and `non-const`) can't be compiled. The same for function parameters: if two functions differ only in `const*` modifier of parameter, this will cause "conflicting overloads" error. TBD. +* C++ lambda type is not supported yet. +* Member pointer, member reference, rvalue reference and some other types are not wupported. +* Inheritance is not implemented yet. C++-style callbacks (overriding virtual method in Kotlin) may be implemented via plain C bridge (this can be done by hand as a workaround). TBD. + diff --git a/kotlin-native/Interop/Indexer/src/main/kotlin/org/jetbrains/kotlin/native/interop/indexer/Indexer.kt b/kotlin-native/Interop/Indexer/src/main/kotlin/org/jetbrains/kotlin/native/interop/indexer/Indexer.kt index 6171596b049..5b576cf8384 100644 --- a/kotlin-native/Interop/Indexer/src/main/kotlin/org/jetbrains/kotlin/native/interop/indexer/Indexer.kt +++ b/kotlin-native/Interop/Indexer/src/main/kotlin/org/jetbrains/kotlin/native/interop/indexer/Indexer.kt @@ -32,6 +32,8 @@ private class StructDefImpl( size, align, decl ) { override val members = mutableListOf() + override val methods = mutableListOf() + override val staticFields = mutableListOf() } private class EnumDefImpl(spelling: String, type: Type, override val location: Location) : EnumDef(spelling, type) { @@ -74,6 +76,30 @@ private class ObjCCategoryImpl( override val properties = mutableListOf() } + +private fun getParentName(cursor: CValue, pkg: List = emptyList()) : String? { // }: List? { + // This doesn't work for anonymous C++ struct (such as typedef struct { void foo(); } TypeDefName) as well as anon namespace + // In contrast, clang_getTypeSpelling return fully qualified name for struct & class (incl. typedef anon struct), + // but does not help for anything elde such as template member, namespace etc + // So, TODO Use ultimately clang_getTypeSpelling for CXType_Record (no traversing needed) and traverse up the whole hierarchy for anythiong else + // Unfortunately, this won't work too for variable decl with anon type like that: ''struct { void foo(); } x;'' + // while function is accessible as x.foo() + + // skip this (zero) level: + + val parent = clang_getCursorSemanticParent(cursor) + if (clang_isDeclaration(parent.kind) == 0) + return if (pkg.isNotEmpty()) pkg.joinToString("::") else null + + val type = clang_getCursorType(parent) + if (type.kind == CXTypeKind.CXType_Record) + return clang_getTypeSpelling(type).convertAndDispose() + + val nextPkg = if (parent.kind == CXCursorKind.CXCursor_Namespace) listOf(parent.spelling) + pkg else pkg + return getParentName(parent, nextPkg) +} + + internal class NativeIndexImpl(val library: NativeLibrary, val verbose: Boolean = false) : NativeIndex() { private sealed class DeclarationID { @@ -136,10 +162,10 @@ internal class NativeIndexImpl(val library: NativeLibrary, val verbose: Boolean override val typedefs get() = typedefRegistry.included private val typedefRegistry = TypeDeclarationRegistry() - private val functionById = mutableMapOf() + private val functionById = mutableMapOf() override val functions: Collection - get() = functionById.values + get() = functionById.values.filterNotNull() override val macroConstants = mutableListOf() override val wrappedMacros = mutableListOf() @@ -191,6 +217,43 @@ internal class NativeIndexImpl(val library: NativeLibrary, val verbose: Boolean return StructDeclImpl(typeSpelling, getLocation(cursor)) } + private fun visitClass(cursor: CValue, clazz: StructDefImpl) { + + // TODO skip method (function) when encounter UnsupportedType in params or ret value. Otherwise all class methods will be lost due to exception (?) + visitChildren(cursor) { cursor, _ -> + if (cursor.isPublic) { + // TODO If a kotlin class is _conceptually_ derived from its c++ counterpart, then it shall be able to override virtual private and access protected + when (cursor.kind) { + CXCursorKind.CXCursor_CXXMethod -> { + val isOperatorFunction = (clang_getCursorSpelling(cursor).convertAndDispose().take(8) == "operator") + // operators are Not Implemented Yet + if (!isOperatorFunction) { + if (clang_isFunctionTypeVariadic(clang_getCursorType(cursor)) == 0) // FIXME why it doesn't work??? + getFunction(cursor, clazz.decl)?.let { clazz.methods.add(it) } + } + } + CXCursorKind.CXCursor_Constructor -> + getFunction(cursor, clazz.decl)?.let { clazz.methods.add(it) } + CXCursorKind.CXCursor_Destructor -> + getFunction(cursor, clazz.decl)?.let { clazz.methods.add(it) } + + CXCursorKind.CXCursor_VarDecl -> { + clazz.staticFields.add(GlobalDecl( + name =getCursorSpelling(cursor), + type = convertCursorType(cursor), + isConst = clang_isConstQualifiedType(clang_getCursorType(cursor)) != 0, + parentName = clazz.decl.spelling) + ) + } + + else -> { + } + } + } + CXChildVisitResult.CXChildVisit_Continue + } + } + private fun createStructDef(structDecl: StructDeclImpl, cursor: CValue) { val type = clang_getCursorType(cursor) @@ -205,17 +268,19 @@ internal class NativeIndexImpl(val library: NativeLibrary, val verbose: Boolean when (cursor.kind) { CXCursorKind.CXCursor_UnionDecl -> StructDef.Kind.UNION CXCursorKind.CXCursor_StructDecl -> StructDef.Kind.STRUCT + CXCursorKind.CXCursor_ClassDecl -> StructDef.Kind.CLASS else -> error(cursor.kind) } ) structDef.members += fields + visitClass(cursor, structDef) structDecl.def = structDef } private fun addDeclaredFields(result: MutableList, structType: CValue, containerType: CValue) { - getFields(containerType).forEach { fieldCursor -> + getFields(containerType).filter { it.isPublic }.forEach { fieldCursor -> val name = getCursorSpelling(fieldCursor) if (name.isNotEmpty()) { val fieldType = convertCursorType(fieldCursor) @@ -465,7 +530,7 @@ internal class NativeIndexImpl(val library: NativeLibrary, val verbose: Boolean convertType(clang_getCursorType(cursor), clang_getDeclTypeAttributes(cursor)) private inline fun objCType(supplier: () -> ObjCPointer) = when (library.language) { - Language.C -> UnsupportedType + Language.C, Language.CPP -> UnsupportedType Language.OBJECTIVE_C -> supplier() } @@ -582,7 +647,7 @@ internal class NativeIndexImpl(val library: NativeLibrary, val verbose: Boolean CXType_Record -> RecordType(getStructDeclAt(clang_getTypeDeclaration(type))) CXType_Enum -> EnumType(getEnumDefAt(clang_getTypeDeclaration(type))) - CXType_Pointer -> { + CXType_Pointer, CXType_LValueReference -> { val pointeeType = clang_getPointeeType(type) val pointeeIsConst = (clang_isConstQualifiedType(clang_getCanonicalType(pointeeType)) != 0) @@ -590,7 +655,9 @@ internal class NativeIndexImpl(val library: NativeLibrary, val verbose: Boolean val convertedPointeeType = convertType(pointeeType) PointerType( if (convertedPointeeType == UnsupportedType) VoidType else convertedPointeeType, - pointeeIsConst = pointeeIsConst + pointeeIsConst = pointeeIsConst, + isLVReference = (kind == CXType_LValueReference), + spelling = type.name ) } @@ -794,23 +861,43 @@ internal class NativeIndexImpl(val library: NativeLibrary, val verbose: Boolean return } + val namespace: Namespace? = + // semantic parent of any namespace member is always namespace itself (no type aliases etc) + if (info.semanticContainer!!.pointed.cursor.kind == CXCursorKind.CXCursor_Namespace) { + val parent = info.semanticContainer!!.pointed.cursor.readValue() + Namespace(getCursorSpelling(parent), getParentName(parent)) + } else null + + if (!cursor.isRecursivelyPublic()) { + // c++ : skip anon namespaces, static functions and variables and private inner classes + return + } + /** + * TODO It may be better to look at CXTypeKind instead of CXIdxEntity to distinguish C++ classes from templates + * C++ templates are also CXIdxEntity_CXXClass but CXCursor_ClassTemplate, + * while C++ class is CXCursor_ClassDecl + * The same for CXCursor_FunctionDecl vs CXCursor_FunctionTemplate + */ when (kind) { - CXIdxEntity_Struct, CXIdxEntity_Union -> { + CXIdxEntity_Struct, CXIdxEntity_Union, CXIdxEntity_CXXClass -> { if (entityName == null) { // Skip anonymous struct. // (It gets included anyway if used as a named field type). } else { - getStructDeclAt(cursor) + if (library.language != Language.CPP) { + getStructDeclAt(cursor) + } } } - CXIdxEntity_Typedef -> { + CXIdxEntity_Typedef, CXIdxEntity_CXXTypeAlias -> { val type = clang_getCursorType(cursor) getTypedef(type) } CXIdxEntity_Function -> { - if (isSuitableFunction(cursor)) { + if (isSuitableFunction(cursor) + && library.language != Language.CPP) { functionById.getOrPut(getDeclarationId(cursor)) { getFunction(cursor) } @@ -822,13 +909,15 @@ internal class NativeIndexImpl(val library: NativeLibrary, val verbose: Boolean } CXIdxEntity_Variable -> { - if (info.semanticContainer!!.pointed.cursor.kind == CXCursorKind.CXCursor_TranslationUnit) { - // Top-level variable. + val parentKind = info.semanticContainer!!.pointed.cursor.kind + if (parentKind == CXCursorKind.CXCursor_TranslationUnit || parentKind == CXCursorKind.CXCursor_Namespace) { + // Top-level or namespace member. Skip class static members - they are loaded by visitClass globalById.getOrPut(getDeclarationId(cursor)) { GlobalDecl( name = entityName!!, type = convertCursorType(cursor), - isConst = clang_isConstQualifiedType(clang_getCursorType(cursor)) != 0 + isConst = clang_isConstQualifiedType(clang_getCursorType(cursor)) != 0, + parentName = getParentName(cursor) ) } } @@ -880,6 +969,49 @@ internal class NativeIndexImpl(val library: NativeLibrary, val verbose: Boolean } } + fun indexDeclaration(cursor: CValue): Unit { + if (!library.includesDeclaration(cursor)) { + return + } + + if (cursor.isRecursivelyPublic()) { + when (cursor.kind) { + + CXCursorKind.CXCursor_ClassDecl, CXCursorKind.CXCursor_StructDecl, CXCursorKind.CXCursor_UnionDecl -> { + if (library.language == Language.CPP) { + if (cursor.spelling.isEmpty()) { + // Skip anonymous struct. + // (It gets included anyway if used as a named field type). + } else { + getStructDeclAt(cursor) + } + } + } + + CXCursorKind.CXCursor_FunctionDecl -> { + if (library.language == Language.CPP) { + indexCxxFunction(cursor) + } + } + + else -> { + } + } + } + } + + private fun indexCxxFunction(cursor: CValue) { + if (isSuitableFunction(cursor)) { + if (getCursorSpelling(cursor).take(8) == "operator") { + // not implemented yet + } else { + functionById.getOrPut(getDeclarationId(cursor)) { + getFunction(cursor) + } + } + } + } + fun indexObjCClass(cursor: CValue) { if (isAvailable(cursor)) { getObjCClassAt(cursor) @@ -892,14 +1024,19 @@ internal class NativeIndexImpl(val library: NativeLibrary, val verbose: Boolean } } - private fun getFunction(cursor: CValue): FunctionDecl { - val name = clang_getCursorSpelling(cursor).convertAndDispose() - val returnType = convertType(clang_getCursorResultType(cursor), clang_getCursorResultTypeAttributes(cursor)) + private fun getFunction(cursor: CValue, receiver: StructDecl? = null): FunctionDecl? { + if (!isFuncDeclEligible(cursor)) { + log("Skip function ${clang_getCursorSpelling(cursor).convertAndDispose()}") + return null + } + var name = clang_getCursorSpelling(cursor).convertAndDispose() + var returnType = convertType(clang_getCursorResultType(cursor), clang_getCursorResultTypeAttributes(cursor)) - val parameters = getFunctionParameters(cursor) + val parameters = mutableListOf() + parameters += getFunctionParameters(cursor) val binaryName = when (library.language) { - Language.C, Language.OBJECTIVE_C -> clang_Cursor_getMangling(cursor).convertAndDispose() + Language.C, Language.CPP, Language.OBJECTIVE_C -> clang_Cursor_getMangling(cursor).convertAndDispose() } val definitionCursor = clang_getCursorDefinition(cursor) @@ -907,7 +1044,40 @@ internal class NativeIndexImpl(val library: NativeLibrary, val verbose: Boolean val isVararg = clang_Cursor_isVariadic(cursor) != 0 - return FunctionDecl(name, parameters, returnType, binaryName, isDefined, isVararg) + // TODO Do the following if clang_getCursorLanguage(cursor) == CXLanguageKind.CXLanguage_CPlusPlus ... + val parentName = getParentName(cursor) + val cxxMethodInfo = receiver?.let { CxxMethodInfo( + PointerType(RecordType(receiver), + clang_CXXMethod_isConst(cursor) != 0), // CXCursor_ConversionFunction has constness too + when (cursor.kind) { + CXCursorKind.CXCursor_Constructor -> { + returnType = PointerType(RecordType(receiver)) + name = "__init__" // It is intended to init preallocated memory with placement new, so it is not "create" factory method. TODO One may want "create" method also. + // Parameter type for placement new is void*, but I want to emphasize that memory block ahall have proper size and alignment + parameters.add(0, Parameter("self", PointerType(RecordType(receiver)), false)) + CxxMethodKind.Constructor + } + CXCursorKind.CXCursor_Destructor -> { + name = "__destroy__" + parameters.add(0, Parameter("self", PointerType(RecordType(receiver)), false)) + CxxMethodKind.Destructor + } + // CXCursorKind.CXCursor_ConversionFunction -> ... + CXCursorKind.CXCursor_CXXMethod -> + if (clang_CXXMethod_isStatic(cursor) != 0) { + CxxMethodKind.StaticMethod + } else { + parameters.add(0, Parameter("self", + PointerType(RecordType(receiver), clang_CXXMethod_isConst(cursor) != 0), + false)) + CxxMethodKind.InstanceMethod + } + else -> CxxMethodKind.None // Not implemented. Not expected, OK to assert (?) + } + ) + } + + return FunctionDecl(name, parameters, returnType, binaryName, isDefined, isVararg, parentName, cxxMethodInfo) } private fun getObjCMethod(cursor: CValue): ObjCMethod? { @@ -957,6 +1127,22 @@ internal class NativeIndexImpl(val library: NativeLibrary, val verbose: Boolean CXAvailabilityKind.CXAvailability_NotAccessible -> false } + // Skip functions which parameter or return type is TemplateRef + private fun isFuncDeclEligible(cursor: CValue): Boolean { + + var ret = true + visitChildren(cursor) { cursor, _ -> + when (cursor.kind) { + CXCursorKind.CXCursor_TemplateRef -> { + ret = false + CXChildVisitResult.CXChildVisit_Break + } + else -> CXChildVisitResult.CXChildVisit_Recurse + } + } + return ret + } + private fun getFunctionParameters(cursor: CValue): List { val argNum = clang_Cursor_getNumArguments(cursor) val args = (0..argNum - 1).map { @@ -1024,6 +1210,13 @@ private fun indexDeclarations(nativeIndex: NativeIndexImpl): CompilationWithPCH } }) + visitChildren(clang_getTranslationUnitCursor(translationUnit)) { cursor, _ -> + if (getContainingFile(cursor) in headers) { + nativeIndex.indexDeclaration(cursor) + } + CXChildVisitResult.CXChildVisit_Recurse + } + visitChildren(clang_getTranslationUnitCursor(translationUnit)) { cursor, _ -> val file = getContainingFile(cursor) if (file in headers && nativeIndex.library.includesDeclaration(cursor)) { diff --git a/kotlin-native/Interop/Indexer/src/main/kotlin/org/jetbrains/kotlin/native/interop/indexer/MacroConstants.kt b/kotlin-native/Interop/Indexer/src/main/kotlin/org/jetbrains/kotlin/native/interop/indexer/MacroConstants.kt index 225e4b7b024..eed66ccf8b3 100644 --- a/kotlin-native/Interop/Indexer/src/main/kotlin/org/jetbrains/kotlin/native/interop/indexer/MacroConstants.kt +++ b/kotlin-native/Interop/Indexer/src/main/kotlin/org/jetbrains/kotlin/native/interop/indexer/MacroConstants.kt @@ -171,7 +171,7 @@ private fun reparseWithCodeSnippets(library: CompilationWithPCH, names.forEach { name -> val codeSnippetLines = when (library.language) { - Language.C, Language.OBJECTIVE_C -> + Language.C, Language.CPP, Language.OBJECTIVE_C -> listOf("void $CODE_SNIPPET_FUNCTION_NAME_PREFIX$name() {", " __auto_type KNI_INDEXER_VARIABLE_$name = $name;", "}") diff --git a/kotlin-native/Interop/Indexer/src/main/kotlin/org/jetbrains/kotlin/native/interop/indexer/NativeIndex.kt b/kotlin-native/Interop/Indexer/src/main/kotlin/org/jetbrains/kotlin/native/interop/indexer/NativeIndex.kt index a6eca10eeea..bcaef2be12c 100644 --- a/kotlin-native/Interop/Indexer/src/main/kotlin/org/jetbrains/kotlin/native/interop/indexer/NativeIndex.kt +++ b/kotlin-native/Interop/Indexer/src/main/kotlin/org/jetbrains/kotlin/native/interop/indexer/NativeIndex.kt @@ -18,6 +18,7 @@ package org.jetbrains.kotlin.native.interop.indexer enum class Language(val sourceFileExtension: String) { C("c"), + CPP("cpp"), OBJECTIVE_C("m") } @@ -156,10 +157,12 @@ abstract class StructDecl(val spelling: String) : TypeDeclaration { abstract class StructDef(val size: Long, val align: Int, val decl: StructDecl) { enum class Kind { - STRUCT, UNION + STRUCT, UNION, CLASS } + abstract val methods: List abstract val members: List + abstract val staticFields: List abstract val kind: Kind val fields: List get() = members.filterIsInstance() @@ -224,11 +227,53 @@ abstract class ObjCCategory(val name: String, val clazz: ObjCClass) : ObjCContai */ data class Parameter(val name: String?, val type: Type, val nsConsumed: Boolean) + +enum class CxxMethodKind { + None, // not supported yet? + Constructor, + Destructor, + StaticMethod, + InstanceMethod // virtual or non-virtual instance member method (non-static) + // do we need operators here? + // do we need to distinguish virtual and non-virtual? Static? Final? +} + +/** + * C++ class method, constructor or destructor details + */ +class CxxMethodInfo(val receiverType: PointerType, val kind: CxxMethodKind = CxxMethodKind.InstanceMethod) + +fun CxxMethodInfo.isConst() : Boolean = receiverType.pointeeIsConst + + /** * C function declaration. */ class FunctionDecl(val name: String, val parameters: List, val returnType: Type, val binaryName: String, - val isDefined: Boolean, val isVararg: Boolean) + val isDefined: Boolean, val isVararg: Boolean, + val parentName: String? = null, val cxxMethod: CxxMethodInfo? = null) { + + val fullName: String = parentName?.let { "$parentName::$name" } ?: name + + // C++ virtual or non-virtual instance member, i.e. has "this" receiver + val isCxxInstanceMethod: Boolean = cxxMethod != null && cxxMethod.kind == CxxMethodKind.InstanceMethod + + /** + * C++ class or instance member function, i.e. any function in the scope of class/struct: method, static, ctor, dtor, cast operator, etc + */ + val isCxxMethod: Boolean = cxxMethod != null + && this.cxxMethod.kind != CxxMethodKind.None + + val isCxxConstructor: Boolean = cxxMethod != null && this.cxxMethod.kind == CxxMethodKind.Constructor + val isCxxDestructor: Boolean = cxxMethod != null && this.cxxMethod.kind == CxxMethodKind.Destructor + val cxxReceiverType: PointerType? = cxxMethod?.receiverType + val cxxReceiverClass: StructDecl? = cxxMethod?. let { (this.cxxMethod.receiverType.pointeeType as RecordType).decl } +} + +class Namespace(val name: String, val parent: String? = null) { + val fullName: String = parent?.let { "$parent::$name" } ?: name +} + /** * C typedef definition. @@ -248,7 +293,9 @@ class StringConstantDef(name: String, type: Type, val value: String) : ConstantD class WrappedMacroDef(name: String, val type: Type) : MacroDef(name) -class GlobalDecl(val name: String, val type: Type, val isConst: Boolean) +class GlobalDecl(val name: String, val type: Type, val isConst: Boolean, val parentName: String? = null) { + val fullName: String = parentName?.let { "$it::$name" } ?: name +} /** * C type. @@ -279,7 +326,9 @@ data class RecordType(val decl: StructDecl) : Type data class EnumType(val def: EnumDef) : Type -data class PointerType(val pointeeType: Type, val pointeeIsConst: Boolean = false) : Type +// when pointer type is provided by clang we'll use ots correct spelling +data class PointerType(val pointeeType: Type, val pointeeIsConst: Boolean = false, + val isLVReference: Boolean = false, val spelling: String? = null) : Type // TODO: refactor type representation and support type modifiers more generally. data class FunctionType(val parameterTypes: List, val returnType: Type) : Type diff --git a/kotlin-native/Interop/Indexer/src/main/kotlin/org/jetbrains/kotlin/native/interop/indexer/Utils.kt b/kotlin-native/Interop/Indexer/src/main/kotlin/org/jetbrains/kotlin/native/interop/indexer/Utils.kt index 5ea12ca88d4..f6638e5344a 100644 --- a/kotlin-native/Interop/Indexer/src/main/kotlin/org/jetbrains/kotlin/native/interop/indexer/Utils.kt +++ b/kotlin-native/Interop/Indexer/src/main/kotlin/org/jetbrains/kotlin/native/interop/indexer/Utils.kt @@ -36,6 +36,46 @@ internal val CValue.name: String get() = clang_getTypeSpelling(this).con internal val CXTypeKind.spelling: String get() = clang_getTypeKindSpelling(this).convertAndDispose() internal val CXCursorKind.spelling: String get() = clang_getCursorKindSpelling(this).convertAndDispose() +internal val CValue.isPublic: Boolean get() { + val access = clang_getCXXAccessSpecifier(this) + return access != CX_CXXAccessSpecifier.CX_CXXProtected && access != CX_CXXAccessSpecifier.CX_CXXPrivate +} + + +/** + * TODO Accessibility needs better support + * Currently we provide binding (access) to static vars (= internal linkage) + * (i.e. following C policy, as the C header would be included into kotlin impl file + * Consistent approach to C++ would be: + * - Kotlin class inherits from C++ allowing overriding and protected access + * - namespace mapped to package + * - anon namespace members mapped to "internal" allowing access from the current translation unit + * To make this working we have to derive a complete C++ "proxy" class for each original one and declare C wrappers as friends + * BTW Such derived C++ proxy class is the only way to allow Kotlin to override the private virtual C++ methods (which is OK in C++) + * Without that C++ style callbacks via overriding would be limited or not supported + */ +internal fun CValue.isRecursivelyPublic(): Boolean { + when { + clang_isDeclaration(kind) == 0 -> + return true // got the topmost declaration already + !isPublic -> + return false + kind == CXCursorKind.CXCursor_Namespace && getCursorSpelling(this).isEmpty() -> + return false + + /* + * TODO FIXME In the current design we allow binding to static vars, but this won't work for anon namespaces and private members + * Need better (consistent( decision wrt accessibility. + */ + // clang_getCursorLinkage(this) == CXLinkageKind.CXLinkage_Internal -> + // return false; // check disabled for a while + + else -> + return clang_getCursorSemanticParent(this).isRecursivelyPublic() + } +} + + internal fun CValue.convertAndDispose(): String { try { return clang_getCString(this)!!.toKString() diff --git a/kotlin-native/Interop/StubGenerator/src/main/kotlin/org/jetbrains/kotlin/native/interop/gen/CodeUtils.kt b/kotlin-native/Interop/StubGenerator/src/main/kotlin/org/jetbrains/kotlin/native/interop/gen/CodeUtils.kt index 95e313e3013..91c334ec578 100644 --- a/kotlin-native/Interop/StubGenerator/src/main/kotlin/org/jetbrains/kotlin/native/interop/gen/CodeUtils.kt +++ b/kotlin-native/Interop/StubGenerator/src/main/kotlin/org/jetbrains/kotlin/native/interop/gen/CodeUtils.kt @@ -41,7 +41,7 @@ typealias KotlinExpression = String fun String.asSimpleName(): String = if (this in kotlinKeywords || this.contains("$")) { "`$this`" } else { - this + this.replace(':', '_') } /** diff --git a/kotlin-native/Interop/StubGenerator/src/main/kotlin/org/jetbrains/kotlin/native/interop/gen/MappingBridgeGeneratorImpl.kt b/kotlin-native/Interop/StubGenerator/src/main/kotlin/org/jetbrains/kotlin/native/interop/gen/MappingBridgeGeneratorImpl.kt index cfda59df17d..3632850eff1 100644 --- a/kotlin-native/Interop/StubGenerator/src/main/kotlin/org/jetbrains/kotlin/native/interop/gen/MappingBridgeGeneratorImpl.kt +++ b/kotlin-native/Interop/StubGenerator/src/main/kotlin/org/jetbrains/kotlin/native/interop/gen/MappingBridgeGeneratorImpl.kt @@ -16,10 +16,7 @@ package org.jetbrains.kotlin.native.interop.gen -import org.jetbrains.kotlin.native.interop.indexer.RecordType -import org.jetbrains.kotlin.native.interop.indexer.Type -import org.jetbrains.kotlin.native.interop.indexer.VoidType -import org.jetbrains.kotlin.native.interop.indexer.unwrapTypedefs +import org.jetbrains.kotlin.native.interop.indexer.* /** * The [MappingBridgeGenerator] implementation which uses [SimpleBridgeGenerator] as the backend and @@ -27,7 +24,8 @@ import org.jetbrains.kotlin.native.interop.indexer.unwrapTypedefs */ class MappingBridgeGeneratorImpl( val declarationMapper: DeclarationMapper, - val simpleBridgeGenerator: SimpleBridgeGenerator + val simpleBridgeGenerator: SimpleBridgeGenerator, + val language: Language ) : MappingBridgeGenerator { override fun kotlinToNative( @@ -40,7 +38,12 @@ class MappingBridgeGeneratorImpl( ): KotlinExpression { val bridgeArguments = mutableListOf() - kotlinValues.forEach { (type, value) -> + if (nativeBacked is FunctionStub && nativeBacked.isCxxInstanceMember()) { + bridgeArguments.add(BridgeTypedKotlinValue(BridgedType.NATIVE_PTR, "rawPtr")) + kotlinValues.drop(1) + } else { + kotlinValues + }.forEach { (type, value) -> if (type.unwrapTypedefs() is RecordType) { builder.pushMemScoped() val bridgeArgument = "$value.getPointer(memScope).rawValue" @@ -79,6 +82,25 @@ class MappingBridgeGeneratorImpl( val unwrappedType = type.unwrapTypedefs() if (unwrappedType is RecordType) { nativeValues.add("*(${unwrappedType.decl.spelling}*)${bridgeNativeValues[index]}") + } else if (language == Language.CPP) { + // C++ is more restrictive wrt type conversion + val cppRefTypePrefix = if (unwrappedType is PointerType && unwrappedType.isLVReference) "*" else "" + when { /// TODO Move this cludge to mirror() + type is Typedef -> + nativeValues.add("(${type.def.name})${bridgeNativeValues[index]}") + type is PointerType && type.spelling != null -> + nativeValues.add("(${type.spelling})$cppRefTypePrefix${bridgeNativeValues[index]}") + unwrappedType is EnumType -> + nativeValues.add("(${unwrappedType.def.spelling})${bridgeNativeValues[index]}") + unwrappedType is RecordType -> + nativeValues.add("*(${unwrappedType.decl.spelling}*)${bridgeNativeValues[index]}") + else -> + nativeValues.add(cppRefTypePrefix + + mirror(declarationMapper, type).info.cFromBridged( + bridgeNativeValues[index], scope, nativeBacked + ) + ) + } } else { nativeValues.add( mirror(declarationMapper, type).info.cFromBridged( @@ -90,18 +112,27 @@ class MappingBridgeGeneratorImpl( val nativeResult = block(nativeValues) - when (unwrappedReturnType) { - is VoidType -> { + when { + unwrappedReturnType is VoidType -> { out(nativeResult + ";") "" } - is RecordType -> { + unwrappedReturnType is RecordType -> { val kniStructResult = "kniStructResult" - out("${unwrappedReturnType.decl.spelling} $kniStructResult = $nativeResult;") - out("memcpy(${bridgeNativeValues.last()}, &$kniStructResult, sizeof($kniStructResult));") + if (language == Language.CPP) { + // use copy/move constructor to create object in place. + out("new(${bridgeNativeValues.last()}) ${unwrappedReturnType.decl.spelling}($nativeResult);") + } else { + out("${unwrappedReturnType.decl.spelling} $kniStructResult = $nativeResult;") + out("memcpy(${bridgeNativeValues.last()}, &$kniStructResult, sizeof($kniStructResult));") + // The following would be better, but won't work in case of const fields: C99 6.3.2.1p1 + // out("*(${unwrappedReturnType.decl.spelling}*) ${bridgeNativeValues.last()} = $nativeResult;") + } "" } + unwrappedReturnType is PointerType && unwrappedReturnType.isLVReference -> + "&$nativeResult" else -> { nativeResult } @@ -204,4 +235,4 @@ class MappingBridgeGeneratorImpl( return result } -} \ No newline at end of file +} diff --git a/kotlin-native/Interop/StubGenerator/src/main/kotlin/org/jetbrains/kotlin/native/interop/gen/SimpleBridgeGeneratorImpl.kt b/kotlin-native/Interop/StubGenerator/src/main/kotlin/org/jetbrains/kotlin/native/interop/gen/SimpleBridgeGeneratorImpl.kt index b6517d140bf..a378a3d5548 100644 --- a/kotlin-native/Interop/StubGenerator/src/main/kotlin/org/jetbrains/kotlin/native/interop/gen/SimpleBridgeGeneratorImpl.kt +++ b/kotlin-native/Interop/StubGenerator/src/main/kotlin/org/jetbrains/kotlin/native/interop/gen/SimpleBridgeGeneratorImpl.kt @@ -120,10 +120,11 @@ class SimpleBridgeGeneratorImpl( "JNIEXPORT $cReturnType JNICALL $functionName ($joinedCParameters)" } KotlinPlatform.NATIVE -> { + val externCPrefix = if (libraryForCStubs.language == Language.CPP) "extern \"C\" " else "" val functionName = pkgName.replace(INVALID_CLANG_IDENTIFIER_REGEX, "_") + "_$kotlinFunctionName" if (independent) kotlinLines.add("@" + topLevelKotlinScope.reference(KotlinTypes.independent)) kotlinLines.add("@SymbolName(${functionName.quoteAsKotlinLiteral()})") - "$cReturnType $functionName ($joinedCParameters)" + "$externCPrefix$cReturnType $functionName ($joinedCParameters)" } } nativeLines.add(cFunctionHeader + " {") diff --git a/kotlin-native/Interop/StubGenerator/src/main/kotlin/org/jetbrains/kotlin/native/interop/gen/StructRendering.kt b/kotlin-native/Interop/StubGenerator/src/main/kotlin/org/jetbrains/kotlin/native/interop/gen/StructRendering.kt index ae9f31f7c7e..4f440de86d1 100644 --- a/kotlin-native/Interop/StubGenerator/src/main/kotlin/org/jetbrains/kotlin/native/interop/gen/StructRendering.kt +++ b/kotlin-native/Interop/StubGenerator/src/main/kotlin/org/jetbrains/kotlin/native/interop/gen/StructRendering.kt @@ -5,6 +5,7 @@ import org.jetbrains.kotlin.native.interop.indexer.* fun tryRenderStructOrUnion(def: StructDef): String? = when (def.kind) { StructDef.Kind.STRUCT -> tryRenderStruct(def) StructDef.Kind.UNION -> tryRenderUnion(def) + StructDef.Kind.CLASS -> tryRenderStruct(def) } private fun tryRenderStruct(def: StructDef): String? { diff --git a/kotlin-native/Interop/StubGenerator/src/main/kotlin/org/jetbrains/kotlin/native/interop/gen/StubIrBridgeBuilder.kt b/kotlin-native/Interop/StubGenerator/src/main/kotlin/org/jetbrains/kotlin/native/interop/gen/StubIrBridgeBuilder.kt index e30ad7ad7a1..d6a602ef2d8 100644 --- a/kotlin-native/Interop/StubGenerator/src/main/kotlin/org/jetbrains/kotlin/native/interop/gen/StubIrBridgeBuilder.kt +++ b/kotlin-native/Interop/StubGenerator/src/main/kotlin/org/jetbrains/kotlin/native/interop/gen/StubIrBridgeBuilder.kt @@ -63,7 +63,7 @@ class StubIrBridgeBuilder( ) private val mappingBridgeGenerator: MappingBridgeGenerator = - MappingBridgeGeneratorImpl(declarationMapper, simpleBridgeGenerator) + MappingBridgeGeneratorImpl(declarationMapper, simpleBridgeGenerator, context.libraryForCStubs.language) private val propertyAccessorBridgeBodies = mutableMapOf() private val functionBridgeBodies = mutableMapOf>() @@ -260,6 +260,9 @@ class StubIrBridgeBuilder( isVararg = isVararg or parameter.isVararg val parameterName = parameter.name.asSimpleName() val bridgeArgument = when { + function.isCxxInstanceMember() && index == 0 -> { + "rawPtr" + } parameter in builderResult.bridgeGenerationComponents.cStringParameters -> { bodyGenerator.pushMemScoped() "$parameterName?.cstr?.getPointer(memScope)" @@ -289,7 +292,18 @@ class StubIrBridgeBuilder( bridgeArguments, independent = false ) { nativeValues -> - "${origin.function.name}(${nativeValues.joinToString()})" + with (origin.function) { + when { + isCxxInstanceMethod -> + "(${nativeValues[0]})->${name}(${nativeValues.drop(1).joinToString()})" + isCxxConstructor -> + "new(${nativeValues[0]}) ${cxxReceiverClass!!.spelling}(${nativeValues.drop(1).joinToString()})" + isCxxDestructor -> + "(${nativeValues[0]})->~${cxxReceiverClass!!.spelling?.substringAfterLast(':')}()" + else -> + "${fullName}(${nativeValues.joinToString()})" + } + } } bodyGenerator.returnResult(result) functionBridgeBodies[function] = bodyGenerator.build() diff --git a/kotlin-native/Interop/StubGenerator/src/main/kotlin/org/jetbrains/kotlin/native/interop/gen/StubIrDriver.kt b/kotlin-native/Interop/StubGenerator/src/main/kotlin/org/jetbrains/kotlin/native/interop/gen/StubIrDriver.kt index 4b30749529c..4aca5934c11 100644 --- a/kotlin-native/Interop/StubGenerator/src/main/kotlin/org/jetbrains/kotlin/native/interop/gen/StubIrDriver.kt +++ b/kotlin-native/Interop/StubGenerator/src/main/kotlin/org/jetbrains/kotlin/native/interop/gen/StubIrDriver.kt @@ -28,12 +28,15 @@ class StubIrContext( if (platform == KotlinPlatform.JVM) { add("jni.h") } + if (configuration.library.language == Language.CPP) { + add("new") + } addAll(configuration.library.includes) }, compilerArgs = configuration.library.compilerArgs, additionalPreambleLines = configuration.library.additionalPreambleLines + when (configuration.library.language) { - Language.C -> emptyList() + Language.C, Language.CPP -> emptyList() Language.OBJECTIVE_C -> listOf("void objc_terminate();") } ).precompileHeaders() diff --git a/kotlin-native/Interop/StubGenerator/src/main/kotlin/org/jetbrains/kotlin/native/interop/gen/StubIrElementBuilders.kt b/kotlin-native/Interop/StubGenerator/src/main/kotlin/org/jetbrains/kotlin/native/interop/gen/StubIrElementBuilders.kt index 84bacac7476..61ccc77da71 100644 --- a/kotlin-native/Interop/StubGenerator/src/main/kotlin/org/jetbrains/kotlin/native/interop/gen/StubIrElementBuilders.kt +++ b/kotlin-native/Interop/StubGenerator/src/main/kotlin/org/jetbrains/kotlin/native/interop/gen/StubIrElementBuilders.kt @@ -80,6 +80,17 @@ internal class StructStubBuilder( } val classifier = context.getKotlinClassForPointed(decl) + var methods: List = + def.methods + .filter { it.isCxxInstanceMethod } + .map { func -> + try { + (FunctionStubBuilder(context, func).build() as List).single() + } catch (e: Throwable) { + null + } + }.filterNotNull() + val fields: List = def.fields.map { field -> try { assert(field.name.isNotEmpty()) @@ -179,18 +190,33 @@ internal class StructStubBuilder( val annotation = AnnotationStub.CStruct.VarType(def.size, def.align).takeIf { context.generationMode == GenerationMode.METADATA } - val companion = ClassStub.Companion( - companionClassifier, - superClassInit = companionSuperInit, - annotations = listOfNotNull(annotation, AnnotationStub.Deprecated.deprecatedCVariableCompanion) - ) + var classMethods: List = + def.methods + .filter { !it.isCxxInstanceMethod } + .map { func -> + try { + (FunctionStubBuilder(context, func).build() as List).single() + } catch (e: Throwable) { + null + } + }.filterNotNull() + val classFields = def.staticFields + .map { field -> (GlobalStubBuilder(context, field).build() as List).single() } + + val companion = ClassStub.Companion( + companionClassifier, + superClassInit = companionSuperInit, + annotations = listOfNotNull(annotation, AnnotationStub.Deprecated.deprecatedCVariableCompanion), + properties = classFields, + methods = classMethods + ) return listOf(ClassStub.Simple( classifier, origin = origin, properties = fields.filterNotNull() + if (platform == KotlinPlatform.NATIVE) bitFields else emptyList(), constructors = listOf(primaryConstructor), - methods = emptyList(), + methods = methods, modality = ClassStubModality.NONE, annotations = listOfNotNull(structAnnotation), superClassInit = superClassInit, @@ -628,12 +654,12 @@ internal class GlobalStubBuilder( val getter = when (context.platform) { KotlinPlatform.JVM -> { PropertyAccessor.Getter.SimpleGetter().also { - val extra = BridgeGenerationInfo(global.name, mirror.info) + val extra = BridgeGenerationInfo(global.fullName, mirror.info) context.bridgeComponentsBuilder.arrayGetterBridgeInfo[it] = extra } } KotlinPlatform.NATIVE -> { - val cCallAnnotation = AnnotationStub.CCall.Symbol("${context.generateNextUniqueId("knifunptr_")}_${global.name}_getter") + val cCallAnnotation = AnnotationStub.CCall.Symbol("${context.generateNextUniqueId("knifunptr_")}_${global.fullName}_getter") PropertyAccessor.Getter.ExternalGetter(listOf(cCallAnnotation)).also { context.wrapperComponentsBuilder.getterToWrapperInfo[it] = WrapperGenerationInfo(global) } @@ -647,12 +673,12 @@ internal class GlobalStubBuilder( val getter = when (context.platform) { KotlinPlatform.JVM -> { PropertyAccessor.Getter.SimpleGetter().also { - val getterExtra = BridgeGenerationInfo(global.name, mirror.info) + val getterExtra = BridgeGenerationInfo(global.fullName, mirror.info) context.bridgeComponentsBuilder.getterToBridgeInfo[it] = getterExtra } } KotlinPlatform.NATIVE -> { - val cCallAnnotation = AnnotationStub.CCall.Symbol("${context.generateNextUniqueId("knifunptr_")}_${global.name}_getter") + val cCallAnnotation = AnnotationStub.CCall.Symbol("${context.generateNextUniqueId("knifunptr_")}_${global.fullName}_getter") PropertyAccessor.Getter.ExternalGetter(listOf(cCallAnnotation)).also { context.wrapperComponentsBuilder.getterToWrapperInfo[it] = WrapperGenerationInfo(global) } @@ -664,12 +690,12 @@ internal class GlobalStubBuilder( val setter = when (context.platform) { KotlinPlatform.JVM -> { PropertyAccessor.Setter.SimpleSetter().also { - val setterExtra = BridgeGenerationInfo(global.name, mirror.info) + val setterExtra = BridgeGenerationInfo(global.fullName, mirror.info) context.bridgeComponentsBuilder.setterToBridgeInfo[it] = setterExtra } } KotlinPlatform.NATIVE -> { - val cCallAnnotation = AnnotationStub.CCall.Symbol("${context.generateNextUniqueId("knifunptr_")}_${global.name}_setter") + val cCallAnnotation = AnnotationStub.CCall.Symbol("${context.generateNextUniqueId("knifunptr_")}_${global.fullName}_setter") PropertyAccessor.Setter.ExternalSetter(listOf(cCallAnnotation)).also { context.wrapperComponentsBuilder.setterToWrapperInfo[it] = WrapperGenerationInfo(global) } @@ -682,10 +708,10 @@ internal class GlobalStubBuilder( kotlinType = mirror.pointedType val getter = when (context.generationMode) { GenerationMode.SOURCE_CODE -> { - PropertyAccessor.Getter.InterpretPointed(global.name, kotlinType.toStubIrType()) + PropertyAccessor.Getter.InterpretPointed(global.fullName, kotlinType.toStubIrType()) } GenerationMode.METADATA -> { - val cCallAnnotation = AnnotationStub.CCall.Symbol("${context.generateNextUniqueId("knifunptr_")}_${global.name}_getter") + val cCallAnnotation = AnnotationStub.CCall.Symbol("${context.generateNextUniqueId("knifunptr_")}_${global.fullName}_getter") PropertyAccessor.Getter.ExternalGetter(listOf(cCallAnnotation)).also { context.wrapperComponentsBuilder.getterToWrapperInfo[it] = WrapperGenerationInfo(global, passViaPointer = true) } diff --git a/kotlin-native/Interop/StubGenerator/src/main/kotlin/org/jetbrains/kotlin/native/interop/gen/StubIrExtensions.kt b/kotlin-native/Interop/StubGenerator/src/main/kotlin/org/jetbrains/kotlin/native/interop/gen/StubIrExtensions.kt index d48a60046c7..282e36848b9 100644 --- a/kotlin-native/Interop/StubGenerator/src/main/kotlin/org/jetbrains/kotlin/native/interop/gen/StubIrExtensions.kt +++ b/kotlin-native/Interop/StubGenerator/src/main/kotlin/org/jetbrains/kotlin/native/interop/gen/StubIrExtensions.kt @@ -12,6 +12,17 @@ private val StubOrigin.ObjCMethod.isOptional: Boolean fun FunctionStub.isOptionalObjCMethod(): Boolean = this.origin is StubOrigin.ObjCMethod && this.origin.isOptional +fun FunctionStub.isCxxInstanceMember(): Boolean = this.origin is StubOrigin.Function && + this.origin.function.isCxxInstanceMethod + +fun FunctionStub.qualifiedName(): String = + if (this.origin is StubOrigin.Function && !this.origin.function.isCxxMethod) { + this.origin.function.fullName + } else { + name + } + + val StubContainer.isInterface: Boolean get() = if (this is ClassStub.Simple) { modality == ClassStubModality.INTERFACE @@ -97,4 +108,4 @@ val StubType.underlyingTypeFqName: String is AbbreviatedType -> underlyingType.underlyingTypeFqName is FunctionalType -> classifier.fqName is TypeParameterType -> name - } \ No newline at end of file + } diff --git a/kotlin-native/Interop/StubGenerator/src/main/kotlin/org/jetbrains/kotlin/native/interop/gen/StubIrTextEmitter.kt b/kotlin-native/Interop/StubGenerator/src/main/kotlin/org/jetbrains/kotlin/native/interop/gen/StubIrTextEmitter.kt index 42fc8e06397..dc572818a81 100644 --- a/kotlin-native/Interop/StubGenerator/src/main/kotlin/org/jetbrains/kotlin/native/interop/gen/StubIrTextEmitter.kt +++ b/kotlin-native/Interop/StubGenerator/src/main/kotlin/org/jetbrains/kotlin/native/interop/gen/StubIrTextEmitter.kt @@ -184,7 +184,8 @@ class StubIrTextEmitter( if (element in bridgeBuilderResult.excludedStubs) return val header = run { - val parameters = element.parameters.joinToString(prefix = "(", postfix = ")") { renderFunctionParameter(it) } + val parameters = (if (element.isCxxInstanceMember()) element.parameters.drop(1) else element.parameters). + joinToString(prefix = "(", postfix = ")") { renderFunctionParameter(it) } val receiver = element.receiver?.let { renderFunctionReceiver(it) + "." } ?: "" val typeParameters = renderTypeParameters(element.typeParameters) val override = if (element.isOverride) "override " else "" @@ -660,4 +661,4 @@ class StubIrTextEmitter( "$name : ${renderStubType(it)}" } ?: name } -} \ No newline at end of file +} diff --git a/kotlin-native/Interop/StubGenerator/src/main/kotlin/org/jetbrains/kotlin/native/interop/gen/jvm/main.kt b/kotlin-native/Interop/StubGenerator/src/main/kotlin/org/jetbrains/kotlin/native/interop/gen/jvm/main.kt index 50cb0a40af2..74f951bd5d7 100644 --- a/kotlin-native/Interop/StubGenerator/src/main/kotlin/org/jetbrains/kotlin/native/interop/gen/jvm/main.kt +++ b/kotlin-native/Interop/StubGenerator/src/main/kotlin/org/jetbrains/kotlin/native/interop/gen/jvm/main.kt @@ -121,16 +121,20 @@ private fun Properties.putAndRunOnReplace(key: Any, newValue: Any, beforeReplace this[key] = newValue } -private fun selectNativeLanguage(config: DefFile.DefFileConfig): Language { +private fun selectNativeLanguage(config: DefFile.DefFileConfig, hintIsCPP: Boolean = false): Language { val languages = mapOf( "C" to Language.C, + "C++" to Language.CPP, "Objective-C" to Language.OBJECTIVE_C ) - val language = config.language ?: return Language.C + val lang = config.language?.let { + languages[it] + ?: error("Unexpected language '${config.language}'. Possible values are: ${languages.keys.joinToString { "'$it'" }}") + } ?: Language.C + + return if (lang == Language.C && hintIsCPP) Language.CPP else lang - return languages[language] ?: - error("Unexpected language '$language'. Possible values are: ${languages.keys.joinToString { "'$it'" }}") } private fun parseImports(dependencies: List): ImportsImpl = @@ -220,8 +224,6 @@ private fun processCLib(flavor: KotlinPlatform, cinteropArguments: CInteropArgum cinteropArguments.linkerOptions.value.toTypedArray() val verbose = cinteropArguments.verbose - val language = selectNativeLanguage(def.config) - val entryPoint = def.config.entryPoints.atMostOne() val linkerOpts = def.config.linkerOpts.toTypedArray() + @@ -294,7 +296,7 @@ private fun processCLib(flavor: KotlinPlatform, cinteropArguments: CInteropArgum File(nativeLibsDir).mkdirs() - val outCFile = tempFiles.create(libName, ".${language.sourceFileExtension}") + val outCFile = tempFiles.create(libName, ".${library.language.sourceFileExtension}") val logger = if (verbose) { { message: String -> println(message) } @@ -451,6 +453,14 @@ internal fun prepareTool(target: String?, flavor: KotlinPlatform): ToolConfig { return tool } +private fun isCxxOptions(opts: List) : Boolean { + if (opts.size >= 2) opts.reduce args@{ prev, that -> + if (prev == "-x" && that == "c++") return@isCxxOptions true + return@args that + } + return false +} + internal fun buildNativeLibrary( tool: ToolConfig, def: DefFile, @@ -462,7 +472,8 @@ internal fun buildNativeLibrary( arguments.compilerOptions + arguments.compilerOption).toTypedArray() val headerFiles = def.config.headers + additionalHeaders - val language = selectNativeLanguage(def.config) + val cppOptions = isCxxOptions(def.config.compilerOpts + additionalCompilerOpts) + val language = selectNativeLanguage(def.config, cppOptions) val compilerOpts: List = mutableListOf().apply { addAll(def.config.compilerOpts) addAll(tool.defaultCompilerOpts) @@ -476,6 +487,7 @@ internal fun buildNativeLibrary( addAll(getCompilerFlagsForVfsOverlay(arguments.headerFilterPrefix.toTypedArray(), def)) addAll(when (language) { Language.C -> emptyList() + Language.CPP -> if (cppOptions) emptyList() else listOf("-x", "c++") Language.OBJECTIVE_C -> { // "Objective-C" within interop means "Objective-C with ARC": listOf("-fobjc-arc") diff --git a/kotlin-native/backend.native/tests/build.gradle b/kotlin-native/backend.native/tests/build.gradle index 9c59801f6eb..6f3ba365a96 100644 --- a/kotlin-native/backend.native/tests/build.gradle +++ b/kotlin-native/backend.native/tests/build.gradle @@ -3823,6 +3823,14 @@ createInterop("leakMemoryWithRunningThread") { it.extraOpts "-Xcompile-source", "$projectDir/interop/leakMemoryWithRunningThread/leakMemory.cpp" } +createInterop("cppClass") { + it.defFile 'interop/cpp/cppClass.def' +} + +createInterop("cppTypes") { + it.defFile 'interop/cpp/types.def' +} + if (PlatformInfo.isAppleTarget(project)) { createInterop("objcSmoke") { it.defFile 'interop/objc/objcSmoke.def' @@ -4182,6 +4190,18 @@ task interop_sourceCodeStruct(type: KonanLocalTest) { source = "codegen/intrinsics/interop_sourceCodeStruct.kt" } +interopTest("interop_cppClass") { + disabled = (project.testTarget == 'wasm32') // No interop for wasm yet. + source = "interop/cpp/cppClass.kt" + interop = 'cppClass' +} + +interopTest("interop_cppTypes") { + disabled = (project.testTarget == 'wasm32') // No interop for wasm yet. + source = "interop/cpp/types.kt" + interop = 'cppTypes' +} + /* TODO: This test isn't run automatically task interop_echo_server(type: RunInteropKonanTest) { diff --git a/kotlin-native/backend.native/tests/interop/cpp/cppClass.def b/kotlin-native/backend.native/tests/interop/cpp/cppClass.def new file mode 100644 index 00000000000..d3698dda5a6 --- /dev/null +++ b/kotlin-native/backend.native/tests/interop/cpp/cppClass.def @@ -0,0 +1,42 @@ +language = C++ +compilerOpts = -std=c++14 + + +--- +class CppTest { +public: + CppTest() { ++counter; } + + CppTest(const CppTest&) = default; + + explicit CppTest(int i, double j = 3.14) : iPub(i + int(j + 0.5)) {} + + ~CppTest() { --counter; } + + int iPub = 42; + + virtual int foo() { return iPub; } + + static int counter; + static int s_fun() { return counter; } + + class Nested { + public: + int nestedFoo() { return -2; } + } nested; + +// not supported yet + operator CppTest::Nested() const; + template void fooTmplMember() const; + +private: + CppTest* funPrivate() const; + static int s_funPrivate(); + int iPriv; +}; + +int CppTest::counter; + +CppTest retByValue(CppTest* s) { + return s ? *s : CppTest(); +} diff --git a/kotlin-native/backend.native/tests/interop/cpp/cppClass.kt b/kotlin-native/backend.native/tests/interop/cpp/cppClass.kt new file mode 100644 index 00000000000..4b7ff5bda41 --- /dev/null +++ b/kotlin-native/backend.native/tests/interop/cpp/cppClass.kt @@ -0,0 +1,133 @@ +import kotlinx.cinterop.* +import kotlin.test.* + +import cppClass.* + +class FeatureTest { + + @Test fun ctorDefault() { + memScoped { + val x = alloc() + CppTest.__init__(x.ptr) + assertEquals(42, x.iPub) + assertEquals(42, x.foo()) + // dtor is not called, leak is intentional for the purpose of UT + } + } + + @Test fun ctorWithParam() { + memScoped { + val x = alloc() + CppTest.__init__(x.ptr, 10, 3.8) + assertEquals(14, x.iPub) + } + } + + @Test fun copyCtor(y: CppTest) { + val x = nativeHeap.alloc() {} + CppTest.__init__(x.ptr, y.ptr) + + assertEquals(y.iPub, x.iPub) + nativeHeap.free(x) + } +/* + @Test fun reinitWithCtorAndDtor(y: CppTest) { + val count = CppTest.getCount() + val x = nativeHeap.alloc< CppTest>() {} + CppTest.__init__(x.ptr, y.ptr) + assertEquals( CppTest.getCount(), count + 1) + assertEquals(y.iPub, x.iPub) + + CppTest.__destroy__(x.ptr) + y.iPub = -11 + assertEquals(y.iPub, -11) + CppTest.__init__(x.ptr, y.ptr) + assertEquals(x.iPub, -11) + + CppTest.__destroy__(x.ptr) + assertEquals( CppTest.getCount(), count) + + nativeHeap.free(x) + } + + @Test fun publicField(x : CppTest) { + x.iPub = 21 + assertEquals(22, x.foo(x.ptr)) + } + + @Test fun lvRefParameter() { + memScoped { + val x = alloc() + var i = alloc() + i.value = 758 + assertEquals(x.noNameMember(i.ptr), 759) + assertEquals(i.value, 759) + } + + } + + @Test fun staticField() { + val save = CppTest.getCount() + assertEquals( CppTest.getCount(), CppTest.counter) + + CppTest.counter = 654 + assertEquals( CppTest.getCount(), 654) + assertEquals( CppTest.getCount(), CppTest.counter) + + CppTest.counter = save + assertEquals( CppTest.getCount(), save) + } +*/ +} + +fun main() { + + val testRun = FeatureTest() + testRun.ctorDefault() + testRun.ctorWithParam() + + val a0 = retByValue(null) + println("a0.useContents {iPub} = ${a0.useContents {iPub}}" ) + println("a0.useContents { foo() } = ${a0.useContents { foo() }}" ) + +// retByValue(null)!!.getValue().foo() +// val a1 = interpretPointed(retByValue(null).rawValue) +// println(a1.foo()) +/* + val a1 = interpretPointed< CppTest>(ns__create().rawValue) + testRun.publicField(a1) + + testRun.staticField() + + testRun.lvRefParameter() + + a1.iPub = 112 + testRun.copyCtor(a1) + testRun.reinitWithCtorAndDtor(a1) + */ +} +/* +fun testStatic() { + println(" CppTest.s_fun() returns ${ CppTest.s_fun()}") + println(" CppTest.s_fun() returns ${ CppTest.s_fun()}") + println(" CppTest.s_fun() returns ${ CppTest.s_fun()}") +} + +fun testCtor() { + println("testCtor") + val cxx = nativeHeap.alloc< CppTest>() { + memcpy(ptr, ns__create(), typeOf< CppTest>().size.convert()) // use placement new here + } + cxx.foo(null) + nativeHeap.free(cxx) +} + +fun test2() { + println("test2") + val x = ns__bar(null) +// val theS = interpretPointed< CppTest>(ns__bar(null).rawValue) +// theS.foo(null) + + println("x.useContents {iPub} = ${x.useContents {iPub}}" ) +} +*/ \ No newline at end of file diff --git a/kotlin-native/backend.native/tests/interop/cpp/types.def b/kotlin-native/backend.native/tests/interop/cpp/types.def new file mode 100644 index 00000000000..547245199c6 --- /dev/null +++ b/kotlin-native/backend.native/tests/interop/cpp/types.def @@ -0,0 +1,24 @@ +language = C++ +compilerOpts = -std=c++14 +package = cpptypes + +--- + +class CppTest { +public: + explicit CppTest(int m) : n(m) {} + int n = 11; + virtual int get() const { return n; } + virtual void set(int m) { n = m; } +}; + +CppTest retByValue(int k) { return CppTest(k); } +CppTest* retByPtr(int k) { return new CppTest(k); } // Leak is intentional +CppTest const* retByPtrConst(int k) { return new CppTest(k); } // Leak is intentional +CppTest& retByRef(int k) { return * new CppTest(k); } // Leak is intentional +CppTest const& retByRefConst(int k) { return * new CppTest(k); } // Leak is intentional +int paramByValue(CppTest x) { return x.get(); } +int paramByPtr(CppTest* x) { return x? x->get() : 0; } +int paramByPtrConst(CppTest const* x) { return x? x->get() : 0; } +int paramByRef(CppTest& x) { return x.get(); } +int paramByRefConst(CppTest const& x) { return x.get(); } diff --git a/kotlin-native/backend.native/tests/interop/cpp/types.kt b/kotlin-native/backend.native/tests/interop/cpp/types.kt new file mode 100644 index 00000000000..e846085f1d0 --- /dev/null +++ b/kotlin-native/backend.native/tests/interop/cpp/types.kt @@ -0,0 +1,93 @@ +import kotlinx.cinterop.* +import kotlin.test.* +import kotlin.random.* + +import cpptypes.* + +@Test +fun test_retByValue(k: Int) { + memScoped { + val x: CppTest = retByValue(k).getPointer(memScope).pointed + assertEquals(k, x.get()) + } +} + +@Test +fun test_retByPtr(k: Int) { + val x = interpretPointed(retByPtr(k).rawValue) + assertEquals(k, x.get()) +} + +@Test +fun test_retByPtrConst(k: Int) { + val x = interpretPointed(retByPtrConst(k).rawValue) + assertEquals(k, x.get()) +} + +@Test +fun test_retByRef(k: Int) { + val x = interpretPointed(retByRef(k).rawValue) + assertEquals(k, x.get()) +} + +@Test +fun test_retByRefConst(k: Int) { + val x = interpretPointed(retByRefConst(k).rawValue) + assertEquals(k, x.get()) +} + +@Test +fun test_paramByValue(k: Int) { + val x = nativeHeap.alloc() {} + CppTest.__init__(x.ptr, k) + assertEquals(k, paramByValue(x.readValue())) + nativeHeap.free(x) +} + +@Test +fun test_paramByPtr(k: Int) { + val x = nativeHeap.alloc() {} + CppTest.__init__(x.ptr, k) + assertEquals(k, paramByPtr(x.ptr)) + nativeHeap.free(x) +} + +@Test +fun test_paramByPtrConst(k: Int) { + val x = nativeHeap.alloc() {} + CppTest.__init__(x.ptr, k) + assertEquals(k, paramByPtrConst(x.ptr)) + nativeHeap.free(x) +} + +@Test +fun test_paramByRef(k: Int) { + val x = nativeHeap.alloc() {} + CppTest.__init__(x.ptr, k) + assertEquals(k, paramByRef(x.ptr)) + nativeHeap.free(x) +} + +@Test +fun test_paramByRefConst(k: Int) { + val x = nativeHeap.alloc() {} + CppTest.__init__(x.ptr, k) + assertEquals(k, paramByRefConst(x.ptr)) + nativeHeap.free(x) +} + +fun main() { + val seed = Random.nextInt() + val r = Random(seed) + + test_retByValue(r.nextInt()) + test_retByPtr(r.nextInt()) + test_retByPtrConst(r.nextInt()) + test_retByRef(r.nextInt()) + test_retByRefConst(r.nextInt()) + test_paramByValue(r.nextInt()) + test_paramByPtr(r.nextInt()) + test_paramByPtrConst(r.nextInt()) + test_paramByRef(r.nextInt()) + test_paramByRefConst(r.nextInt()) +} diff --git a/kotlin-native/samples/cpptest/NOTES-ru.md b/kotlin-native/samples/cpptest/NOTES-ru.md new file mode 100644 index 00000000000..e49d09e3308 --- /dev/null +++ b/kotlin-native/samples/cpptest/NOTES-ru.md @@ -0,0 +1,127 @@ +Что я хотел, и что из этого вышло +-------------------------------------- + +Я думал, что быстро прикостыляю базовый С++ к cinterop, используя plain C обёртки над CXXMethod и передавая адрес объекта (receiver) дополнительным (скрытым) параметром. Так сделан C++ runtime и так сделана куча биндингов в C++ из разных языков. Так что - должно получиться. + +Вышло сплошное разочаровние. Если обобщить все встреченные беды - вывод такой: существующий cinterop не годится для этого от слова "совсем". Что обидно: если бы я не тратил время на борьбу с cinterop - я бы, наверное, за это время написал прототип с нуля. Делать глубокий рефакторинг в рамках тестового задания я счёл неправильным - я не имею возможности даже обсудить грабли, тем более - протестить то что получится. (на самом деле, бОльшую часть кода и инфраструктуры cinterop можно переиспользовать, но рефаторинг коснётся всех сценариев - так что надо всё тестить) + +Что получилось в итоге. Ну я всё же прикостылял базовый C++, не всё и с разной степенью проработки (C++ бесконечен, see below for details). Можно запустить тесты. Можно попытаться затащить библиотеку или, например, свои собственные legacy наработки. Я пытаюсь игнорировать неподдержанные (пока) типы и конструкции, но не все учтены, и на реальном примере придется подхачить C++, чтобы пройти компиляцию. Но это не проблема: дописать поддержку недостающего - вопрос времени/эффортов, принципиальных проблем нет. (В сторону: интероп с собственным legacy кодом я вижу как, потенциально, основной business case. У меня много знакомых плюсеров, которые сидят на легаси огромного объёма и были бы счастливы патчить бизнес-логику на более динамичном и функциональном языке). + +Но пока результат разочаровывает, и вот почему: + +* Этим неудобно пользоваться из-за разницы между CValue и сгенерённым котлиновским CxxClass. (Никто и не пользуется: даже Indexer.kt всюду использует CValue, а сгенерённые классы остались без дела) + +* cinterop всё время пытается делать свои копии нативных структур: readValue, useContents, placeTo etc. Это не работает для нетривиальных объектов. Хуже того: это абсолютный тупик для move семантики, которая в современном C++ является основным трендом. Пример: сейчас модно писать API, в котором все std::string передаются by value, в расчете на move. При попытке оттащить string в какую-то "другую" память (даже с использованием правильных move/copy конструкторов) я получаю маленький объект, из которого торчит указатель в C++ heap, причём указатель будет всё время меняться, immutability ему противопоказана. Или std::shared_ptr ... в общем, понятно. И. наконец, эти копирования не добавляют ничего полезного в случае kotlin-native (или я не понял). + +* Похоже, cinterop был заточен на то, что основной workflow такой: аллоцировать в котлине memory block, инициализировать поля вручную, дальше побитово копировать туда где нужно. У объектного языка другой flow: get handler (using create or new), keep it and use, destroy. Я прикостылял placement new, чтобы сконструировать объект там где его аллоцировал котлин, но смысла в этом ноль. И это не работает для Factory, у объекта может не быть публичного конструктора. + +* Я не стал фиксить by value параметры и return, это будет глючить на нетривиальных объектах (в тестах таких нет). Фикс простой, но концептуальный и задевает весь существующий cinterop. + +Что я предлагаю сделать: + +* Отказаться от попыток менеджерить нативные объекты. Это невозможно для объектов и не имеет смысла для kotlin-native. Используем handler с нативным указателем внутри. Котлиновский класс сейчас так и генерится - отлично. Никаких CValue. Параметр by value - компилятор подставит звёздочку, *rawPtr. Возврат by value: я использую placement new и copy ctor, чтобы заставить C++ вернуть значение не на стеке, а в хипе. Собственно, это сделано. Но дальше надо в котлин отдать handler с указателем в хип, а не readValue. Это тривиально и это я тоже сделал, но это ломает совместимость с существующим кодом. +* Упростить сгенерённый для котлина API, в смысле типов. Никаких CValue, ptr и прочего. Все параметры и возвращаемые типы - только котлиновский прокси класс, handler. Компилятор подставит нужное преобразование в значение, указатель или ссылку. Где-то здесь придётся покувыркаться с move и с temporary, но это feasible. Манглингом или типами показать разницу между const-nonconst параметрами (in/out). Разницы между by value и const ref вообще нет. В случае неоднозначности (конфликт имён из-за того, что не показываем в котлине разницу между by value и by ptr), и только в этом случае - дополнительно манглинг, или программист подскажет компилятору как-то, например напишет ptr. +* Вместо бесполезного для деструкторов mamscope сделать cxxscope, чтобы автоматически вызывать зарегистрированные деструкторы. Я попробовал сделать, используя std::unique_ptr - но не закончил. + + +TODO +---- + +0. Design style. Костыли. Во многих местах в реальной продуктовой задаче я бы предпочёл сначала сделать подготовительный рефакторинг, цикл review-commit-tests, потом поверх этого сделать фичу. Т.е. разделить коммит на два: изменение (без добавления) и добавление (без изменений). Для тестового задания я в основном выбирал вариант минимальных изменений, чтобы проще ревьюить за один раз. В результате - костыли и рост технического долга, но это осознанный выбор. + +0. Сейчас могут сгенериться имена с конфликтами в двух популярных случаях: 1) const и non-const метод с одинаковой сигнатуройг 2) функции с похожей сигнатурой, одна получает параметр по указателю, другая по ссылке (оба биндятся как указатель). В первом случае: добавим mangling, во втором надо мапить LVReference в not nullable, и всё развяжется. Руки не дошли. + +0. NativeIndex имеет плоскую структуру создаваемых сущностей. Хотелось иметь дерево, так проще поддержать вложенные классы и неймспейсы. Не успел. В результате вложенные структуры я сделал простым манглингом (quick and dirty). Это работает, но выглядит ужасно, надо переделать. В качестве варианта я рассматривал идею: оставить indexer плоским, но пусть он содержит только один уровень семантической вложенности (один уровень semantic parent в терминологии clang), и я буду строить дерево из множества маленьких одноуровневых индексеров. + +0. Хотелось замапить namespace в package. Вроде просто, но уткнулся в тяжелый рефакторинг в генераторах: там единственность выходного файла и package прибита гвоздями. Один translation unit = один выходной файл. А тут translation unit должен создать целое дерево с перекрестными импортами. + +0. Многие типы и конструкции не поддержаны, но каких-то принципиальных проблем в реализации я не вижу. Темплейты - почти мапятся в темплейты, а инстанцированный темплейт - это обычный класс. Exceptions надо просто ретранслировать. RValueRef кажется должен работать легко. Операторы по разному... их много и это надолго. Можно мапить в обычные функции. Но например operator-> я бы предложил не мудрить, а вместо этого написать удобный аналог unique_ptr и shared_ptr вручную, и тогда их почти не останется. Как мапить лямбду в блок и обратно, похоже без поддержки рантайма не обойдется. И т.д., фичи бесконечны... + +0. Override С++ методов в котлине (callbacks) Можно сделать через обертки: генерим C++ класс-наследник, у которого каждый метод перевызывает соответствующий котлинвский метод как plain C. Это сработает, но сгенерим и скомпилируем много лишнего. Хорошо бы генерить эти обертки только on demand, т.е. когда компилятор видит, что котлин хотел бы перегрузить соответствующий метод. Но замечу, что в современном C++ для колбэка скорее будут использовать лямбду и std::function, чем наследование. + +0. Надо замапить самые важные std типы: string, containers, smart pointers + +0. Сейчас работа с C/C++ из котлина выглядит неестественно, многословная. Interop должен быть seamless. Без лишних interpretPointed, rawValue etc. Но это требует определенного пересмотра концепции (type matching). Уже сказал об этом выше. + +0. Костыль visitChildren, или франкенштейн из двух параллельных indexDeclaration. +Проблема в том, что IndexDeclaration(CXIdxDeclInfo) не отличает класс от темплейта (и функцию от темплейтной функции). Курсор внутри дерева indexDeclaration для темплейтов имеет kind = CXCursor_ClassDecl (или StructDecl) вместо ожидаемого CXCursor_ClassTemplate, и я не нашел способа вытащить "правильный" курсор. Но если разобрать дерево через visitChildren - то курсор правильный. Поэтому, если язык C++, то я разбираю классы через visitChildren (и отбрасываю темплейты, пока они не поддержаны). Я временно оставил такого франкенштейна, когда часть сущностей парсится через CXIdxDeclInfo (из clang_indexTranslationUnit), а часть через CXCursor (из visitChildren). Я думаю, что надо всё перенести в visitChildren, но у меня не было возможности протестить все сценарии в plain C и ObjC. Такой рефакторинг - не для тестового задания. Кстати, такой костыль уже был до меня, для парсинга ObjC, но я не стал втыкать свой код в него, а написал отдельный - потому что мне нужен был CXChildVisit_Recurse, а там был CXChildVisit_Continue и я опять боялся сломать ObjC, который не умею как следует протестить. + + +Conceptual +---------- + +0. Internal linkage. +Сейчас сущности с internal linkage (static & anon namespace) не фильтруются, биндинг на них создаётся (все функции в .def в тестах написаны со словом static). Фактически, строим так, как будто C включён в котлиновский исходник. В таком случае, эти сущности должны биндиться как internal, причем каждый С-ник надо компилировать, как отдельный translation unit, иначе будут конфликты. + - Это нормальный подход, если моё намерение - расширить (extend) нативную имплементацию на котлине. + - Если моё намерение - затащить нативную библиотеку, то я компилирую все публичные хедеры как один translation unit, при этом лучше зафильтровать static. + - Проблема некритична, потому что в публичных хедерах static и anon namespace не встречается. Зато часто в c/cpp имплементации. Лучше быть последовательным в этом вопросе. + +0. Что делать с protected и private? Я пока выключил. Технически, сейчас котлиновский класс соответствует C++ ссылке на объект, а не derived class, т.е. должен иметь доступ только к public. IMHO: это неправильно, в такой логике невозможно реализовать C++ callbacks (оверрайдить виртуальные методы, в т.ч. приватные). See below. + +0. Derive vs Use. Хотелось бы имитироватиь наследование от C++ класса. Это можно сделать в концепции kotlin object is a handler, через делегирование, но надо решить вопрос автоматического вызова деструктора на созданном объекте. + +0. Конструкторы-деструкторы надо доработать. Вопрос концептуальный, требует обсуждения. И это довольно большая работа. Моё предложение: надо различать 3 сущности: + - Явно аллоцированный объект с владением: alloc + init (т.е. placement new) и парный к нему destroy (это this->~MyClass()), деаллокация автоматическая или ручная + - Holder, владеющий указатель на объект, созданный в нативном хипе по new MyClass() и парный к нему delete. Аналог std::unique_ptr. Семантически не отличается от предыдущего. + - Non-owning holder без передачи ownership, т.е. невладеюший указатель. Освобождать не надо. Аналог C raw pointer. + - Можно замапить std::shared_ptr на котлин тип с acquire/release парой. + + +Design issues +------------- + +0. В контейнере members может лежать IncompleteField. Это имело бы смысл, если Incomplete - такой lazy тип, который позже можно, например, зарезолвить. Но это не предусмотрено - так что зачем добавлять dummy, надо было просто пропустить. C FunctionDecl ещё хуже: в registry складываем неподдерживаемые функции, их отличие только в том, что у них может быть UnsupportedType у параметра. В результате exception в stub builder'е. Я заменил такие невалидные объекты на null, их легче потом фильтровать. Но вообще-то их добавлять не следовало. Проблема в том, как сделано добавление: через getOrPut. Если я только в середине построения объекта понял, что валидный объект создать не получается - он всё равно уже добавлен в map, я уже внутри initializer'а. И это не эксепшн концептуально, потому что это нормальная часть контракта, не исключение. Здесь лучше подошла бы логика factory: пытаюсь создать объект и добавляю только если создался успешно. + +0. Часто приватный метод класса является, по сути, чистой функцией и не работает со стейтом объекта - только с параметром. Такие функции надо вытаскивать из класса и делать чистой функцией, или экстеншном на типе параметра. Пример:. getArrayLength(type: ArrayType) сделана приватным методом StructStubBuilder'а, но никакого отношения к этому классу не имеет. Таких примеров много. + +0. Спорное решение про throw Error("Native interop types constructors must not be called directly"). В чем смысл? Мой тип - handler, и это его конструктор. Точно так работает `std::make_unique` и `make_shared`, при этом я могу и конструктором воспользоваться: `auto x = unique_ptr(new T())`. Другого способа сделать биндинг на класс, у которого только factory method - вообще нет. interpretPointed по сути то же самое, но непонятно, и зачем плодить сущности? Наконец, это runtime exception, а не синтаксическая ошибка, которую мог бы подсветить IDE - и это неудобно. +В порядке эксперимента я отключил этот error, в результате и клиентский код, и генеренный стал проще и читабельнее. От чего защищаемся, ведь я все равно могу написать такой wrapper вручную, хранить как long то, что вернул фактори метод, а потом вызывать методы класса на нём? Пусть лучше это сделает генератор. + +0. Parameter by value. + На входе - C declaration: + ``` + struct CTest {}; + int paramByValue(struct CTest x); + ``` + На выходе + ``` + fun paramByValue(x: CValue): Int { + memScoped { + return kniBridge0(x.getPointer(memScope).rawValue) + } + } + + int32_t c1_kniBridge0 (void* p0) { + return (int32_t)paramByValue(*(struct CTest*)p0); + } + ``` + Здесь копия сделана дважды: сначала CValue.getPointer, а потом при вызове paramByValue. Причем во втором случае C++ компилятор корректно использует конструктор копирования, а getPointer - увы... + Нет, это еще не всё. Поскольку функция принимает CValue, а у меня в руках есть только `class CTest`, который `CStructVar`, то придется сделать третье копирование уже в клиентском коде: readValue. + При возврате то же самое: + ``` + void c1_kniBridge1 (void* p0) { + struct CTest kniStructResult = retByValue(); + memcpy(p0, &kniStructResult, sizeof(kniStructResult)); + } + ``` + Здесь уже два копирования. Достаточно было бы + ``` + void c1_kniBridge1 (void* p0) { + *(struct CTest*) p0 = retByValue(); + } + ``` + (Я знаю, что это не сработает при наличии const fields, но это значит, что автор явно запретил копирование. Такую структуру на C невозможно создать в хипе, только на стеке - такой "тёмный угол" стандарта C. На C++ это решается с помощью placement new.) + Но есть ещё и третье копирование в котлиновской обёртке: + ``` + return kniRetVal.readValue() + ``` + и это ещё не конец: kniRetVal это CVariable, так что readValue делает копию дважды. Четыре копирования на каждый вызов retByValue - только чтобы получить экземпляр структуры. + +0. Я попытался постичь дзен этого пассажа: + ``` + * [CValuesRef] is designed to be used as Kotlin representation of pointer-typed parameters of C functions. + * When passing [CPointer] as [CValuesRef] to the Kotlin binding method, the C function receives exactly this pointer. + * Passing [CValues] has nearly the same semantics as passing by value: the C function receives + * the pointer to the temporary copy of these values, and the caller can't observe the modifications to this copy. + ``` + Первые две фразы - ok. Третью я не смог расшифровать. `Passing [CValues]` - куда passing? Если C API принимает MyStruct* то это мутатор с input параметром и отдавать туда нужно указатель на исходный объект. Иначе в чём смысл - вызывать мутатор на временном объекте? Если параметр MyStruct const* то компилятор уже позаботился о нас (трюки типа mutable или const_cast не в счёт - тот кто делает это в публичном API - сам берет на себя ответственность). Если параметр по значению, то C компилятор и так сделает копию. diff --git a/kotlin-native/samples/cpptest/ReadMe.md b/kotlin-native/samples/cpptest/ReadMe.md new file mode 100644 index 00000000000..00db8aad954 --- /dev/null +++ b/kotlin-native/samples/cpptest/ReadMe.md @@ -0,0 +1,8 @@ +Build native: + + g++ -std=c++14 -o src/native/features.o -c src/native/features.cpp + +c++ interop: + + ../../dist/bin/cinterop -def features.def -compiler-options "-I." -o features + diff --git a/kotlin-native/samples/cpptest/features.def b/kotlin-native/samples/cpptest/features.def new file mode 100644 index 00000000000..3ebfc6b8aff --- /dev/null +++ b/kotlin-native/samples/cpptest/features.def @@ -0,0 +1,6 @@ +headers = src/native/features.h +headerFilter = src/native/features.h +package = test +language = C++ +#compilerOpts = -std=c++14 + diff --git a/kotlin-native/samples/cpptest/src/kotlin/FeatureTest/main.kt b/kotlin-native/samples/cpptest/src/kotlin/FeatureTest/main.kt new file mode 100644 index 00000000000..dab46b864f9 --- /dev/null +++ b/kotlin-native/samples/cpptest/src/kotlin/FeatureTest/main.kt @@ -0,0 +1,176 @@ +//package test.cpp + + +import kotlinx.cinterop.* +import platform.posix.* +import platform.posix.memcpy +import test.* +import kotlin.test.* + +class FeatureTest { + + @Test fun ctorDefault() { + memScoped { + val x = alloc() + ns__CppTest.__init__(x.ptr) + assertEquals(42, x.iPub) + assertEquals(42, x.foo(null)) + assertEquals(43, x.foo(x.ptr)) + // dtor is not called, leak is intentional for the purpose of UT + } + } + + @Test fun ctorWithParam() { + memScoped { + val x = alloc() + ns__CppTest.__init__(x.ptr, 1001, 0.0) + assertEquals(1001, x.iPub) + assertEquals(1001, x.foo(null)) + assertEquals(1002, x.foo(x.ptr)) + } + } + + @Test fun copyCtor(y: ns__CppTest) { + val x = nativeHeap.alloc() {} + ns__CppTest.__init__(x.ptr, y.ptr) + + assertEquals(y.iPub, x.iPub) + nativeHeap.free(x) + } + + @Test fun reinitWithCtorAndDtor(y: ns__CppTest) { + val count = ns__CppTest.getCount() + val x = nativeHeap.alloc() {} + ns__CppTest.__init__(x.ptr, y.ptr) + assertEquals(ns__CppTest.getCount(), count + 1) + assertEquals(y.iPub, x.iPub) + + ns__CppTest.__destroy__(x.ptr) + y.iPub = -11 + assertEquals(y.iPub, -11) + ns__CppTest.__init__(x.ptr, y.ptr) + assertEquals(x.iPub, -11) + + ns__CppTest.__destroy__(x.ptr) + assertEquals(ns__CppTest.getCount(), count) + + nativeHeap.free(x) + } + + @Test fun publicField(x : ns__CppTest) { + x.iPub = 21 + assertEquals(22, x.foo(x.ptr)) + } + + @Test fun lvRefParameter() { + memScoped { + val x = alloc() + var i = alloc() + i.value = 758 + assertEquals(x.noNameMember(i.ptr), 759) + assertEquals(i.value, 759) + } + + } + + @Test fun staticField() { + val save = ns__CppTest.getCount() + assertEquals(ns__CppTest.getCount(), ns__CppTest.counter) + + ns__CppTest.counter = 654 + assertEquals(ns__CppTest.getCount(), 654) + assertEquals(ns__CppTest.getCount(), ns__CppTest.counter) + + ns__CppTest.counter = save + assertEquals(ns__CppTest.getCount(), save) + } + +} + +fun main() { + + val testRun = FeatureTest() + testRun.ctorDefault() + testRun.ctorWithParam() + + val a1 = interpretPointed(ns__create().rawValue) + testRun.publicField(a1) + + testRun.staticField() + + testRun.lvRefParameter() + + a1.iPub = 112 + testRun.copyCtor(a1) + testRun.reinitWithCtorAndDtor(a1) + + +//****************************** + println("*** UT passed ***") +//****************************** + + testStatic() + + testCtor() +// testCtor1() + +// testCtor2() +// testCtor3() + test2() +} + +fun testStatic() { + println("ns__CppTest.s_fun() returns ${ns__CppTest.s_fun()}") + println("ns__CppTest.s_fun() returns ${ns__CppTest.s_fun()}") + println("ns__CppTest.s_fun() returns ${ns__CppTest.s_fun()}") +} + +fun testCtor() { + println("testCtor") + val cxx = nativeHeap.alloc() { + memcpy(ptr, ns__create(), typeOf().size.convert()) // use placement new here + } + cxx.foo(null) + nativeHeap.free(cxx) +} +/* +fun testCtor1() { + println("testCtor1: interpretPointed(ns__CppTest.__create__().rawValue)") + val theStruct = interpretPointed(ns__CppTest.__create__().rawValue) + theStruct.iPub = 33 + theStruct.foo(theStruct.ptr) + + println("testCtor1: ns__CppTest(ns__CppTest.__create__().rawValue)") + val xs = ns__CppTest(ns__CppTest.__create__().rawValue) + xs.foo(null) + xs.foo(xs.ptr) + + println("testCtor1: ns__CppTest(ns__CppTest.__create__(1001).rawValue)") + val x2 = ns__CppTest(ns__CppTest.__create__(1001, 2.718).rawValue) + x2.foo(null) + x2.foo(x2.ptr) + +} + +fun testCtor2() { + println("testCtor2: ns__CppTest(ns__create().rawValue)") + val xs = ns__CppTest(ns__create().rawValue) + xs.foo(null) + xs.foo(xs.ptr) +} + +fun testCtor3() { + println("testCtor3: MyStruct()") + val xs = MyStruct() + xs.foo() +} +*/ + +fun test2() { + println("test2") + val x = ns__bar(null) +// val theS = interpretPointed(ns__bar(null).rawValue) +// theS.foo(null) + + println("x.useContents {iPub} = ${x.useContents {iPub}}" ) +} diff --git a/kotlin-native/samples/cpptest/src/native/features.cpp b/kotlin-native/samples/cpptest/src/native/features.cpp new file mode 100644 index 00000000000..10d58acd528 --- /dev/null +++ b/kotlin-native/samples/cpptest/src/native/features.cpp @@ -0,0 +1,73 @@ +#include "features.h" + +#include + +using namespace std; + +namespace ns { + +int NoName::noNameMember(int& iRef) { + cout << __PRETTY_FUNCTION__ << " invoked" << endl; + return ++iRef; +} + +int CppTest::counter; + +int CppTest::s_fun() { + static int counter = 777; + cout << __PRETTY_FUNCTION__ << " invoked" << endl; + return counter++; +} + +int CppTest::foo(const CppTest* x) { + int res = x == this; + cout << "This is CppTest::foo: result is iPub + (int)(param == this): " << iPub + res << endl; + return iPub + res; +} + +CppTest::CppTest() { + cout << ++counter << "\t" << __PRETTY_FUNCTION__ << endl; +} + +CppTest::CppTest(const CppTest& c) { + *this = c; + cout << ++counter << "\t" << __PRETTY_FUNCTION__ << endl; +} + +CppTest::CppTest(int i, double j) : iPub(i) { + cout << ++counter << "\t" << __PRETTY_FUNCTION__ << endl; +} + + +CppTest::~CppTest() { + cout << --counter << "\t" << __PRETTY_FUNCTION__ << endl; +} + + +CppTest bar(CppTest* s) { + if (s) + return *s; + else + return * new CppTest(); +} + +CppTest* create() { + return new CppTest(); +} + +} // ns + +CppTest* create() { + cout << __PRETTY_FUNCTION__ << " declared in global ns" << endl; + return nullptr; +} + +::CppTest* ns2::create() { + cout << __PRETTY_FUNCTION__ << " declared in ns2" << endl; + return nullptr; +} + +void test() { + char buf[sizeof(ns::CppTest)]; + ns::CppTest* x = new((ns::CppTest*)buf) ns::CppTest(); +} \ No newline at end of file diff --git a/kotlin-native/samples/cpptest/src/native/features.h b/kotlin-native/samples/cpptest/src/native/features.h new file mode 100644 index 00000000000..da8f8df5fe4 --- /dev/null +++ b/kotlin-native/samples/cpptest/src/native/features.h @@ -0,0 +1,211 @@ + +class MyClass { +}; + +MyClass retByValue(); + + + +struct OuterCStruct { + const int plainCField; + struct { + int innerCField; + }; + + union { + int innerUnionF1; + double innerUnionF2; + }; + + struct Inner { + }; + struct Inner innerImpl; + +#ifdef __cplusplus +// Illegal C. ok in C++ + typedef struct { + } NestedStructInnerT; + NestedStructInnerT nestedStructT; +#endif +}; + +const int g_IntVal = 0; + + +typedef int (*funT)(int); +void fooFuncParam( funT ); +void barFuncParam( int (*)(int) ); + + +template struct TmplStruct { +public: + void baz() const {} +}; + +void funTmplParam(TmplStruct& t); + +template void tmplFunction(); +template void tmplFunction(X x); + +//int funVariadic(const char* format, ...); + +//#include +//void appendVAList(const char format[], va_list); // doesn't work + + +int plainCFreeFunction(); +static inline void plainCInternalFunction(); // [Conceptual] should be mapped as internal fun or not eligible for binding at all + +int g_Var; + +struct PlainCStruct { + int plainCField; + struct { + int innerCField; + }; +}; + +typedef struct PlainCStruct PlainCStructT; + +using PlainCStructAlias = PlainCStruct; + +void wrappingFun() { + class NestedInFunction { + int varInLocalClass; + } var; // local types shall be ignored +} + +struct UnknownT; +UnknownT* cFunUnknownParams(const UnknownT*); + + +struct ForwardT; // declaratuin +ForwardT* cFunForwardParams(const ForwardT*); // decl + +struct ForwardT {}; // definition +ForwardT* cFunForwardParams(const ForwardT*) { // def + return new ForwardT(); +} + +namespace ns0 { +class clDeclaredInNS; +extern int varWithDefinition; +int varWithDefFirst = 99; +} + + +namespace ns0_alias = ns0; +class ns0_alias::clDeclaredInNS{}; +extern int ns0_alias::varWithDefFirst; + +using clDeclaredInNSAlias = ns0_alias::clDeclaredInNS; + +// int ns0_alias::varInNSAlias; // illegal declaration +int ns0_alias::varWithDefinition = 21; + +namespace ns { + +int g_varInNS; + +namespace { + void funInInnerAnonNS(); + namespace nsInAnonNS { + int memberOf_nsInAnonNS; + } +} + +namespace NestedNS { + int funInNestedNS(); + int g_varInNestedNS; +} + +namespace { + void funInInnerAnonNS(); +} + +typedef class { +public: + int noNameMember(int& iRef); +// int noNameMember(const int& iRef); need mangling + static int noNameStaticFun(); // won't work + int fieldInAnonClass; + // static int s_fieldInAnonClass; not legal C++ +} NoName; + +enum EnumInNS {one, two, three}; + +enum class EnumClass : char { one, two, three}; + +class CppTest { +public: + static int s_fun(); + + CppTest(); + + CppTest(const CppTest&); + + explicit CppTest(int i, double j = 3.14); + + ~CppTest(); + + operator NoName() const; + + int iPub = 42; + + virtual int foo(const CppTest*); +// virtual int foo(const CppTest*) const; + + static int counter; + + static NoName compStaticField; + NoName compField; + + static int getCount() { return counter; } + template void fooTmplMember() const; + + class Nested { + public: + int nestedFoo(); + }; + + enum NestedEnum {one, two, three}; + void funEnumParam(NestedEnum e); + +private: + CppTest* funPrivate() const; + static int s_funPrivate(); + +private: + int iPriv; +}; + +CppTest funRetByValue(CppTest* s); + +CppTest* create(); + +} // ns + +namespace { +ns::CppTest* fooInAnonNamespace(); +} + +class CppTest; +CppTest* create(); + +namespace ns2 { + ::CppTest* create(); + +} // ns2 +/* +template class TmplClass { +public: + void baz() const {} +}; + +typedef class TmplStruct IntTmplStruct; +IntTmplStruct intTmplStruct; + +struct Smth { + IntTmplStruct intTmplStruct; +}; +*/ diff --git a/kotlin-native/samples/skia/README.md b/kotlin-native/samples/skia/README.md new file mode 100644 index 00000000000..f645c154629 --- /dev/null +++ b/kotlin-native/samples/skia/README.md @@ -0,0 +1,6 @@ + + g++ -std=c++14 main.cpp -I$HOME/work/cpptools/skia /Users/vdi/work/cpptools/skia/out/Static/libskia.a -framework CoreServices -framework CoreText -framework CoreGraphics + + ../../dist/bin/cinterop -def Skia.def -o Skia -compiler-options "-I/Volumes/vdi/work/cpptools/skia" + + ../../dist/bin/kotlinc src/skiaMain/kotlin/test_SkString.kt -o test_SkString -l Skia -linker-options "$HOME/work/cpptools/skia/out/Static/libskia.a" -linker-options "-framework CoreServices" -linker-options "-framework CoreText" -linker-options "-framework CoreGraphics" diff --git a/kotlin-native/samples/skia/Skia.def b/kotlin-native/samples/skia/Skia.def new file mode 100644 index 00000000000..328418b951b --- /dev/null +++ b/kotlin-native/samples/skia/Skia.def @@ -0,0 +1,5 @@ +headers = include/core/SkString.h include/core/SkTime.h +headerFilter = include/core/SkTime.h include/core/SkString.h +package = skia +compilerOpts = -std=c++14 +compilerOpts = -x c++ diff --git a/kotlin-native/samples/skia/main.cpp b/kotlin-native/samples/skia/main.cpp new file mode 100644 index 00000000000..2b1be6b3427 --- /dev/null +++ b/kotlin-native/samples/skia/main.cpp @@ -0,0 +1,11 @@ +#include "include/core/SkTime.h" +#include + +using namespace std; + +int main() +{ + auto nsec = SkTime::GetNSecs(); + cout << nsec << endl; + return 0; +} \ No newline at end of file diff --git a/kotlin-native/samples/skia/src/skiaMain/kotlin/sktimeSample.kt b/kotlin-native/samples/skia/src/skiaMain/kotlin/sktimeSample.kt new file mode 100644 index 00000000000..3bf8b683cc8 --- /dev/null +++ b/kotlin-native/samples/skia/src/skiaMain/kotlin/sktimeSample.kt @@ -0,0 +1,19 @@ +import kotlinx.cinterop.* +import SkTime.* + +fun main() { + println("Skia sample") + println("${SkTime.GetNSecs()}") + println("${SkTime.GetNSecs()}") + println("${SkTime.GetNSecs()}") + test_GetDateTime() +} + +fun test_GetDateTime() { + memScoped { + val dateTime = alloc() + SkTime.GetDateTime(dateTime.ptr) + println("${dateTime.fYear}-${dateTime.fMonth}-${dateTime.fDay} ${dateTime.fHour}:${dateTime.fMinute}.${dateTime.fSecond}" ) + } + +} \ No newline at end of file diff --git a/kotlin-native/samples/skia/src/skiaMain/kotlin/test_SkString.kt b/kotlin-native/samples/skia/src/skiaMain/kotlin/test_SkString.kt new file mode 100644 index 00000000000..9c3dfed0ba9 --- /dev/null +++ b/kotlin-native/samples/skia/src/skiaMain/kotlin/test_SkString.kt @@ -0,0 +1,80 @@ +import kotlinx.cinterop.* +import kotlin.test.* + +import skia.* + +fun hello() { + memScoped { + val cString = "Hello Skia".cstr.getPointer(memScope) + val s = alloc() { + SkString.__init__(ptr, cString) + } + println(s.c_str()?.toKString()) + } +} + +fun SkString_create(from: String): SkString { + val res = nativeHeap.alloc() {} + memScoped { + val cString = from.cstr.getPointer(memScope) + SkString.__init__(res.ptr, cString) + } + return res +} + +fun SkString.clone(): SkString { + return nativeHeap.alloc() { + SkString.__init__(ptr, this@clone.ptr) + } +} + +fun SkString.delete(): Unit { + SkString.__destroy__(this.ptr) + nativeHeap.free(this) +} + +fun SkString.toKString() = this.c_str()!!.toKString() + + +fun main() { + + hello() + + /* + with (CppContext) { + val go = SkString("Let's go fishing!") + val goback = SkString(go) + val pos = goback.find("fish") + println(go) + if (pos >= 0) { + goback.insert(pos, "back!") + goback.resize((pos + "back!".length) + } + println(goback) + oback.swap(go) + goback.resize(goback.size() - 1U) + goback.append(" again!") + println(goback) + } + */ + val go = SkString_create("Let's go fishing!") + println(go.toKString()) + + val goback = go.clone() + memScoped { + val pos = goback.find("fish".cstr.getPointer(memScope)) + if (pos >= 0) { + goback.insert(pos.convert(), "back!".cstr.getPointer(memScope)) + goback.resize((pos + "back!".length).convert()) + } + println(goback.toKString()) + + goback.swap(go.ptr) + + goback.resize(goback.size() - 1U) + goback.append(" again!".cstr.getPointer(memScope)) + println(goback.toKString()) + } + go.delete() + goback.delete() +} \ No newline at end of file