How to use general C/C++ code in KMP common source set? - kotlin

I'm building an app for Android and iOS and I want to reuse as much code as possible. I have some generic C code (an algorithm) that doesn't include any system library. Is it possible to expose it to my common Kotlin source set using cinterop or any other tool?
My build.gradle.kts:
plugins {
id("com.android.library")
kotlin("multiplatform")
kotlin("native.cocoapods")
}
android {
compileSdkVersion(29)
defaultConfig {
minSdkVersion(21)
targetSdkVersion(29)
}
sourceSets.all {
manifest.srcFile("src/androidMain/AndroidManifest.xml")
java.srcDirs("src/androidMain/java")
res.srcDirs("src/androidMain/res")
}
}
version = "1.0"
kotlin {
android()
ios()
cocoapods {
// Configure fields required by CocoaPods.
summary = "..."
homepage = "..."
// You can change the name of the produced framework.
// By default, it is the name of the Gradle project.
frameworkName = "SharedModule"
}
// Workaround for ios platform imports to work on Android Studio
// iosX64("ios")
sourceSets["commonMain"].dependencies {
implementation("org.jetbrains.kotlin:kotlin-stdlib-common")
}
sourceSets["androidMain"].dependencies {
implementation("org.jetbrains.kotlin:kotlin-stdlib")
}
sourceSets.all {
languageSettings.progressiveMode = true
}
}

Not automatically. On the JVM side you'll need to use JNI to talk to the C code, and on the native side you'll need to use Kotlin cinterop. JNI and Kotlin cinterop will have an interface to the C code that's very similar to each other, but not the same. To expose that to common code, you'll need to write a common API layer that delegates to JNI code on the JVM and Kotlin cinterop code on native.
Wrapping very similar platform-specific API's is pretty straightforward once you get used to it. Ideally, you could automatically wrap them, but right now you can't. I gave a talk that discusses some techniques for this: https://vimeo.com/371460823

Below follows a simple example of how to call c code within Kotlin:
cCaller.kt
class cCaller {
init {
System.loadLibrary("cCode")
} external fun callCFunction()
}
In your code:
fun main() {
cCaller().callCFunction()
}
cCode.c
#include <stdio.h>
#include "cCaller.h"JNIEXPORT void JNICALL Java_cCaller_callCFunction(JNIEnv *env, jobject obj) {
// YOUR CODE HERE
return;
}
Notice that the function callCFunction is prefixed with Java_ and cCaller_
Also notice the #include "cCaller.h" added to the second line of cCode.c - we need to create this file:
cCaller.h
#include <jni.h>#ifndef _Included_NativeSample
#define _Included_NativeSample
#ifdef __cplusplus
extern "C" {
#endifJNIEXPORT void JNICALL Java_cCaller_callCFunction(JNIEnv *, jobject);#ifdef __cplusplus
}
#endif
#endif
Compile
gcc cCode.c -o libcCode.so -shared -fPIC -I <jdk_path>/include -I <jdk_path>/include/linux

Related

Specific gradle options for target build platform

I have a Kotlin project (not Android) that uses the LWJGL library. Under macOS, I need to add the following options to build.gradle:
project.ext.lwjglNatives = "natives-macos"
applicationDefaultJvmArgs = ["-XstartOnFirstThread"]
dependencies {
implementation platform('org.jetbrains.kotlin:kotlin-bom')
implementation platform("org.lwjgl:lwjgl-bom:3.2.3")
implementation "org.lwjgl:lwjgl"
implementation "org.lwjgl:lwjgl-openal"
runtimeOnly "org.lwjgl:lwjgl::$lwjglNatives"
runtimeOnly "org.lwjgl:lwjgl-openal::$lwjglNatives"
}
On Windows, however, I need to drop applicationDefaultJvmArgs, and set lwjglNatives to:
project.ext.lwjglNatives = "natives-windows"
How can I tell gradle to do this? Basically I need some kind of target platform check.
Moreover, I need to know the target platform in Kotlin was well. How can I tell the build platform from Kotlin code?
I figured it out. The operating system can be tested like this:
import org.apache.tools.ant.taskdefs.condition.Os
if (Os.isFamily(Os.FAMILY_WINDOWS)) {
project.ext.lwjglNatives = "natives-windows"
} else {
project.ext.lwjglNatives = "natives-macos"
applicationDefaultJvmArgs = ["-XstartOnFirstThread"]
}
To access this from Kotlin, a BuildConfig.java needs to be generated ad hoc:
tasks.register('generateSources') {
ext.outputDir = "$buildDir/generated/java"
outputs.dir outputDir
doFirst {
mkdir "$outputDir/ch/digorydoo/ksoundrender"
file("$outputDir/ch/digorydoo/ksoundrender/BuildConfig.java").text =
"""|package ch.digorydoo.ksoundrender;
|public class BuildConfig {
| public static Boolean isWindows() {
| return ${if (Os.isFamily(Os.FAMILY_WINDOWS)) "true" else "false"};
|}
|}""".stripMargin()
}
}
compileKotlin.dependsOn generateSources
sourceSets.main.java.srcDir generateSources.outputDir
Now I can just import BuildConfig from Kotlin.

How to configure a plugin with custom sourceSet extension in Kotlin DSL

I could use a helping hand converting victor trellos simple sourceSet extension to Kotlin DSL, I seem a bit lost.
Victor Trello has an extension on SourceSet named svg
project.extensions.create('victor', VictorPluginExtension)
// Add 'svg' as a source set extension
project.android.sourceSets.all { sourceSet ->
SourceDirectorySet sds = project.objects.sourceDirectorySet(sourceSet.name, "${sourceSet.name} svgs")
sourceSet.extensions.add('svg', sds)
}
This is how it is configured in Groovy:
// build.gradle:
android {
// Variant 1 with individual setup
sourceSets {
main {
svg.srcDir 'src/main/svg'
}
}
// Variant 2 handling all at once
sourceSets.all { sourceSet ->
svg.srcDir "src/${sourceSet.name}/svg"
}
}
// Plugin configuration of class com.trello.victor.VictorPluginExtension
victor {
svgDpi = 72
generateVectorDrawables = true
}
What would this look like in Kotlin DSL?
Here's the Victor Trello Gradle Plugin source
I notice that the Victor plugin extends SourceSet, and project.sourceSet has type SourceSet (from gradle) which contains an extensions, whereas AndroidSourceSet does not contain the .extensions. Also I can seem to get a gradle SourceSet from the AndroidSourceSet
This might be the right way to do the plugin configuration, though I cannot test it without the srcDir (I'll remove this from the question if its wrong, to avoid confusion for future readers).
// build.gradle.kts:
configure<com.trello.victor.VictorPluginExtension> {
svgDpi = 72
generateVectorDrawables = true
}
I found the solution with the help of several people (Google + Gradle forums). Hopefully this can help others with other plugins.
The solution had three parts to solve,
Configure a plugin with the configure()
Access Groovy extensions
Iterate sourceSets.all in kotlin (which is obvious if your not unlucky enough to start with sourceSets.all { sourceSet -> })
Here's the source
val Any.extensions get() = (this as org.gradle.api.plugins.ExtensionAware).extensions
android {
sourceSets {
// Variant 1 with individual setup
named("main") {
val a: com.android.build.api.dsl.AndroidSourceSet = this
java.srcDir("src/sharedTest/java")
}
}
// Variant 2 handling all at once
sourceSets.all {
val svgSourceSet = this.extensions["svg"] as SourceDirectorySet
svgSourceSet.srcDir("src/${name}/svg")
}
}
configure<com.trello.victor.VictorPluginExtension> {
// Any assets defined in relative terms needs a base DPI specified
svgDpi = 72
// Do not generate these densities for SVG assets
excludeDensities = listOf("ldpi", "xxxhdpi")
// Set this to "true" if you want to generate Android vectors instead of PNGs
generateVectorDrawables = false
}

Using a Custom SourceSet in Gradle

According to this from the official docs and this SO answer, defining a new source set should be as simple as (in kotlin):
sourceSets {
create("demo") {
compileClasspath += sourceSets.main.get().output
}
}
The second reference also explicitly claims this can now be used in building a jar. However, while the above does not throw an error, actually trying to use the new source set anywhere does. Minimal example (generated with gradle init -> "kotlin application" and project name "foobar"):
plugins {
id("org.jetbrains.kotlin.jvm").version("1.3.21")
application
}
repositories {
jcenter()
}
dependencies {
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
}
application {
mainClassName = "foobar.AppKt"
}
sourceSets {
create("demo") {
compileClasspath += sourceSets.main.get().output
}
}
I've removed configuration of the test source set since it isn't relevant here. The src directory looks like this (which is a bit non standard, but I think fine for the discussion):
src
├── demo
│   └── Demo.kt
└── main
└── foobar
└── App.kt
Ideally, I'd like to have completely separate source trees (but this question is not specifically about that). What this builds makes sense in relation to the configuration above, but it is not really the goal. To do that, I want to add a custom jar or two.1
Unfortunately though, the demo source set can't be referenced in the gradle script.
val jar by tasks.getting(Jar::class) {
manifest { attributes["Main-Class"] = "DemoKt" }
// HERE IS THE PROBLEM:
from(sourceSets["demo"].get().output)
dependsOn(configurations.runtimeClasspath)
from({
configurations.runtimeClasspath.get().filter { it.name.endsWith("jar") }.map { zipTree(it) }
})
}
This fat jar task definition is taken from the gradle docs and I've used much before.
As is, gradle chokes:
Script compilation errors:
Line 28: from(sourceSets["demo"].get().output)
^ Unresolved reference.
If I change that line to:
from(sourceSets.demo.get().output)
Which is the syntax used with sourceSets.main, then the error becomes:
Line 28: from(sourceSets.demo.get().output)
^ Unresolved reference: demo
How am I supposed to work with the custom source set?
I am aware of the gradle subproject pattern and have used it to do much the same thing, but I'd prefer the simple separate source sets solution, which does I think meet all three criteria for such mentioned in that doc.
The sourceSets property (an extension property for Project) returns a SourceSetContainer which extends NamedDomainObjectContainer<SourceSet>. The latter interface defines get(String) (an operator extension function) which is what allows you to use the ["..."] syntax. That method returns T directly, meaning you should just be using:
from(sourceSets["demo"].output)
The reason you have to use get() when using sourceSets.main is because you're getting a NamedDomainObjectProvider<SourceSet> which wraps the source set.
You can also get the custom source set via delegation
When creating it:
val demo by sourceSets.creating { /* configure */ }
Or some time after creating it:
val demo by sourceSets

Adds an implementation dependency only to the "free" product flavor in Kotlin DSL

I am in process of migrating our Groovy based scripts to Kotlin. I have been able to get most of done except not sure how to add a dependency for a particular flavour.
This is how looks like in Kotlin DSL so far but not sure why the freeImplementation("bar:2.2.8")
productFlavors {
create("free") {
...
...
}
create("paid") {
...
...
}
}
dependencies {
implementation("foo:1.2.0")
// This is not working when migrated to Kotlin DSL
freeImplementation("bar:2.2.8")
//Below was the code in Groovy which was working fine earlier
//freeImplementation "bar:2.2.8"
}
Below is the solution for it.
val freeImplementation by configurations
dependencies {
freeImplementation("bar:2.2.8")
}
Alternatively, a string literal can be used to denote a dynamic configuration:
dependencies {
"freeImplementation"("bar:2.2.8")
}

Aspectj doesn't work with kotlin

i want to use aspectj aop in kotlin,here is my code:
my annotation in annotation.lazy_list:
Kotlin:
package anotation
#Retention(AnnotationRetention.RUNTIME)
#Target(AnnotationTarget.FUNCTION)
annotation class lazy_list
my aspectj aop class:
#Aspect
class ActiveListAop{
#Pointcut("execution(#annotation.lazy_list * *(..))")
fun profile() {
}
#Before("profile()")
fun testModeOnly(joinPoint: JoinPoint) {
println("123")
}
}
my usage:
#lazy_list
fun all():List<T>{
return lazy_obj?.all() as List<T>
}
when i call all() function , no error,but wont't print "123", why?
EDIT 9-2021 - there is a nice updated plugin for android that works well as an alternate to my original 2018 answer below: https://github.com/Ibotta/gradle-aspectj-pipeline-plugin
For what it's worth, we needed aspectJ weaving in our android project but really wanted to move to kotlin so we had to solve this problem. So the solutions in this thread using spring or maven didn't work for us. This is the solution for android gradle projects however, this WILL break incremental compilation and therefor slow down your build times and/or break something eventually. This gets us by until I can re-think our architecture and phase out aspectJ or (hopefully) android starts supporting it.
There is confusion in some of the answers and comments to the OP that kapt solves this, but kapt lets you do compile time annotation processing, not weaving. That is, annotation processors let you generate code based on annotations but do not let you inject logic into existing code.
This builds on top of this blog on adding aspectJ to android: https://fernandocejas.com/2014/08/03/aspect-oriented-programming-in-android
Your kotlin classes get compiled into byte code, just into a different directory. So this solution using the same process to weave the java classes but runs it again on the kotlin class files
at the top of your App/build.gradle add:
buildscript {
ext.aspectjVersion = '1.9.1'
dependencies {
classpath "org.aspectj:aspectjtools:$aspectjVersion"
}
}
At the bottom of your App/build.gradle add:
android.applicationVariants.all { variant ->
// add the versionName & versionCode to the apk file name
variant.outputs.all { output ->
def newPath = outputFileName.replace(".apk", "-${variant.versionName}.${variant.versionCode}.apk")
outputFileName = new File(outputFileName, newPath)
def fullName = ""
output.name.tokenize('-').eachWithIndex { token, index ->
fullName = fullName + (index == 0 ? token : token.capitalize())
}
JavaCompile javaCompile = variant.javaCompiler
MessageHandler handler = new MessageHandler(true)
javaCompile.doLast {
String[] javaArgs = ["-showWeaveInfo",
"-1.8",
"-inpath", javaCompile.destinationDir.toString(),
"-aspectpath", javaCompile.classpath.asPath,
"-d", javaCompile.destinationDir.toString(),
"-classpath", javaCompile.classpath.asPath,
"-bootclasspath", project.android.bootClasspath.join(
File.pathSeparator)]
String[] kotlinArgs = ["-showWeaveInfo",
"-1.8",
"-inpath", project.buildDir.path + "/tmp/kotlin-classes/" + fullName,
"-aspectpath", javaCompile.classpath.asPath,
"-d", project.buildDir.path + "/tmp/kotlin-classes/" + fullName,
"-classpath", javaCompile.classpath.asPath,
"-bootclasspath", project.android.bootClasspath.join(
File.pathSeparator)]
new Main().run(javaArgs, handler)
new Main().run(kotlinArgs, handler)
def log = project.logger
for (IMessage message : handler.getMessages(null, true)) {
switch (message.getKind()) {
case IMessage.ABORT:
case IMessage.ERROR:
case IMessage.FAIL:
log.error message.message, message.thrown
break
case IMessage.WARNING:
case IMessage.INFO:
log.info message.message, message.thrown
break
case IMessage.DEBUG:
log.debug message.message, message.thrown
break
}
}
}
}
spring + kotlin + AOP work nice, just go to http://start.spring.io/ and generate a project with AOP support, you can see a piece of build.gradle here...
buildscript {
ext {
kotlinVersion = '1.2.30'
springBootVersion = '2.0.0.RELEASE'
}
repositories {
mavenCentral()
}
dependencies {
classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:${kotlinVersion}")
classpath("org.jetbrains.kotlin:kotlin-allopen:${kotlinVersion}")
}
}
apply plugin: 'kotlin'
apply plugin: 'kotlin-spring'
apply plugin: 'org.springframework.boot'
...
dependencies {
compile('org.springframework.boot:spring-boot-starter-aop')
...
}
plugin kotlin-spring makes all classes open to allow AOP
Then, just declare your aspect as follows
#Aspect
#Component
class MyAspect {
...
Important: annotate your aspect class with #Aspect and #Component annotations
Piece of cake! :)
For annotation process in Kotlin, you must enable and use KAPT. Without this being added via Gradle or Maven plugin, nothing is going to work for annotation processing in Kotlin code.
The Kotlin plugin supports annotation processors like Dagger or DBFlow. In order for them to work with Kotlin classes, apply the kotlin-kapt plugin.
See also:
Pushing the limits of Kotlin annotation processing
kapt: Annotation Processing for Kotlin
Better Annotation Processing: Supporting Stubs in kapt
You can use freefair gradle plugin
buildscript {
repositories {
maven {
url "https://plugins.gradle.org/m2/"
}
}
dependencies {
classpath "io.freefair.gradle:aspectj-plugin:5.2.1"
}
}
apply plugin: "io.freefair.aspectj.post-compile-weaving"
So I think I've got a good (but wordy) solution for Android. At the time of writing I'm using Gradle 6.7, Android plugin 4.1.0, and AspectJ tools 1.9.6.
The gist of the problem is that:
Java is compiled by task compileDebugJavaWithJavac
Kotlin is compiled by task compileDebugKotlin
Gradle can run either one of these tasks, or both of them, or none
compileDebugJavaWithJavac depends on compileDebugKotlin
weaving Kotlin usually requires Java classes.
If you look at these points closely, you'll see that you can't do weaving as part of compiling Kotlin, as Java classes can be missing at this point. If you do that, you'll get warnings such as:
WARN: incorrect classpath: C:\Users\user\StudioProjects\myapp\app\build\intermediates\javac\debug\classes
and errors such as
ERROR: can’t determine modifiers of missing type myapp.Foo.Bar
So the better approach would be to postpone weaving until Java classes are compiled. But as you would be modifying files not as a part of compilation task, you lose incremental builds... Besides, this postponed weaving is super hard to get right—remember, none of the compile tasks might be actually scheduled for running!
The real solution is to wrap weaving in a Transform, which will produce a Gradle task with its own inputs and outputs. This means that you will not be polluting the files of compile tasks, and those tasks, as well as this task, will be, so to say, UP-TO-DATE-able. This requires quite a bit of code, but it's rather sensible!
First, put this in your project build.gradle.kts:
buildscript {
dependencies {
classpath("org.aspectj:aspectjtools:1.9.6")
}
}
This is needed to run weaving from inside “inside” the buildscript. If you want to run weaving in a separate process, which is a good idea on Windows, you will need the path of this jar, which you can get by adding the following to your app build.gradle.kts:
val weaving: Configuration by configurations.creating
dependencies {
weaving("org.aspectj:aspectjtools:1.9.6")
}
Finally, put AspectJ runtime on the classpath (app build.gradle.kts, note that I only need weaving in debug builds):
dependencies {
debugImplementation("org.aspectj:aspectjrt:1.9.6")
}
Now, here's my setup. I have a local logging library, :cats, which containts aspects that I want to weave. Logging statements are only inside my project, and not anywhere else. Also, I only want to run these in debug builds. So here's the transformation that “weaves cats” into the app (app's build.gradle.kts):
class TransformCats : Transform() {
override fun getName(): String = TransformCats::class.simpleName!!
override fun getInputTypes() = setOf(QualifiedContent.DefaultContentType.CLASSES)
// only look for annotations in app classes
// transformation will consume these and put woven classes in the output dir
override fun getScopes() = mutableSetOf(QualifiedContent.Scope.PROJECT)
// ...but also have the rest on our class path
// these will not be touched by the transformation
override fun getReferencedScopes() = mutableSetOf(QualifiedContent.Scope.SUB_PROJECTS,
QualifiedContent.Scope.EXTERNAL_LIBRARIES)
override fun isIncremental() = false
// only run on debug builds
override fun applyToVariant(variant: VariantInfo) = variant.isDebuggable
override fun transform(invocation: TransformInvocation) {
if (!invocation.isIncremental) {
invocation.outputProvider.deleteAll()
}
val output = invocation.outputProvider.getContentLocation(name, outputTypes,
scopes, Format.DIRECTORY)
if (output.isDirectory) FileUtils.deleteDirectoryContents(output)
FileUtils.mkdirs(output)
val input = mutableListOf<File>()
val classPath = mutableListOf<File>()
val aspectPath = mutableListOf<File>()
invocation.inputs.forEach { source ->
source.directoryInputs.forEach { dir ->
input.add(dir.file)
classPath.add(dir.file)
}
source.jarInputs.forEach { jar ->
input.add(jar.file)
classPath.add(jar.file)
}
}
invocation.referencedInputs.forEach { source ->
source.directoryInputs.forEach { dir ->
classPath.add(dir.file)
}
source.jarInputs.forEach { jar ->
classPath.add(jar.file)
if (jar.name == ":cats") aspectPath.add(jar.file)
}
}
weave(classPath, aspectPath, input, output)
}
}
android.registerTransform(TransformCats())
And here's the weaving code mentioned above:
// ajc gets hold of some files such as R.jar, and on Windows it leads to errors such as:
// The process cannot access the file because it is being used by another process
// to avoid these, weave in a process, which `javaexec` will helpfully launch for us.
fun weave(classPath: Iterable<File>, aspectPath: Iterable<File>, input: Iterable<File>, output: File) {
val runInAProcess = OperatingSystem.current().isWindows
val bootClassPath = android.bootClasspath
println(if (runInAProcess) ":: weaving in a process..." else ":: weaving...")
println(":: boot class path: $bootClassPath")
println(":: class path: $classPath")
println(":: aspect path: $aspectPath")
println(":: input: $input")
println(":: output: $output")
val arguments = listOf("-showWeaveInfo",
"-1.8",
"-bootclasspath", bootClassPath.asArgument,
"-classpath", classPath.asArgument,
"-aspectpath", aspectPath.asArgument,
"-inpath", input.asArgument,
"-d", output.absolutePath)
if (runInAProcess) {
javaexec {
classpath = weaving
main = "org.aspectj.tools.ajc.Main"
args = arguments
}
} else {
val handler = MessageHandler(true)
Main().run(arguments.toTypedArray(), handler)
val log = project.logger
for (message in handler.getMessages(null, true)) {
when (message.kind) {
IMessage.DEBUG -> log.debug("DEBUG " + message.message, message.thrown)
IMessage.INFO -> log.info("INFO: " + message.message, message.thrown)
IMessage.WARNING -> log.warn("WARN: " + message.message, message.thrown)
IMessage.FAIL,
IMessage.ERROR,
IMessage.ABORT -> log.error("ERROR: " + message.message, message.thrown)
}
}
}
}
val Iterable<File>.asArgument get() = joinToString(File.pathSeparator)
(The Windows part is using weaving configuration; you may not want either part of the if)
This is it!
Edit: As of AGP 4.2.0, jar.name doesn't return anything useful. For the time being, I used this fragile workaround:
if (jar.file.directoriesInsideRootProject().contains("cats")) {
aspectPath.add(jar.file)
}
fun File.directoriesInsideRootProject() = sequence {
var file = this#directoriesInsideRootProject
while (true) {
yield(file.name)
file = file.parentFile ?: break
if (file == rootProject.projectDir) break
}
}