From e4f8641f719985496581ef026fef57806479308b Mon Sep 17 00:00:00 2001 From: Alexander Kuechler Date: Wed, 22 Apr 2026 13:03:31 +0200 Subject: [PATCH 1/4] Attempt to provide language interfaces --- .../de/fraunhofer/aisec/cpg/ScopeManager.kt | 38 ++++++++++- .../aisec/cpg/TranslationConfiguration.kt | 26 ++++++++ .../aisec/cpg/TranslationContext.kt | 12 ++++ .../aisec/cpg/frontends/LanguageInterface.kt | 57 +++++++++++++++++ .../aisec/cpg/graph/scopes/Scope.kt | 2 +- .../aisec/cpg/frontends/cxx/CAndCxxMapper.kt | 50 +++++++++++++++ .../cpg/enhancements/LanguageInterfaceTest.kt | 63 +++++++++++++++++++ .../src/test/resources/crossLanguage/simple.c | 6 ++ .../src/test/resources/crossLanguage/simple.h | 14 +++++ .../resources/crossLanguage/simpleCxx.cpp | 6 ++ 10 files changed, 272 insertions(+), 2 deletions(-) create mode 100644 cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/LanguageInterface.kt create mode 100644 cpg-language-cxx/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/cxx/CAndCxxMapper.kt create mode 100644 cpg-language-cxx/src/test/kotlin/de/fraunhofer/aisec/cpg/enhancements/LanguageInterfaceTest.kt create mode 100644 cpg-language-cxx/src/test/resources/crossLanguage/simple.c create mode 100644 cpg-language-cxx/src/test/resources/crossLanguage/simple.h create mode 100644 cpg-language-cxx/src/test/resources/crossLanguage/simpleCxx.cpp diff --git a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/ScopeManager.kt b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/ScopeManager.kt index c3b82d6ea64..aeabf9250be 100644 --- a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/ScopeManager.kt +++ b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/ScopeManager.kt @@ -811,7 +811,43 @@ class ScopeManager(override var ctx: TranslationContext) : ScopeProvider, Contex ) ?.toMutableList() ?: mutableListOf() } - } + }.toMutableList() + + val otherLanguageInterfaces = ctx.languageInterfaces[language] + otherLanguageInterfaces?.forEach { otherLanguageInterface -> + val otherLanguage = otherLanguageInterface.getTo>() + val symbol = otherLanguageInterface.mapSymbol(n.localName) + + val toAdd = + when { + scope != null -> { + scope + .lookupSymbol( + symbol, + languageOnly = otherLanguage, + qualifiedLookup = true, + replaceImports = replaceImports, + predicate = predicate, + ) + .toMutableList() + } + else -> { + // Otherwise, we can look up the symbol alone (without any FQN) starting + // from + // the startScope + startScope + ?.lookupSymbol( + symbol, + languageOnly = otherLanguage, + replaceImports = replaceImports, + predicate = predicate, + ) + ?.toMutableList() ?: mutableListOf() + } + } + + list.addAll(toAdd) + } // If we have both the definition and the declaration of a function declaration in our list, // we chose only the definition diff --git a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/TranslationConfiguration.kt b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/TranslationConfiguration.kt index 4d36545b994..dd58bd9dd14 100644 --- a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/TranslationConfiguration.kt +++ b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/TranslationConfiguration.kt @@ -35,6 +35,7 @@ import de.fraunhofer.aisec.cpg.frontends.FrontendConfiguration import de.fraunhofer.aisec.cpg.frontends.KClassSerializer import de.fraunhofer.aisec.cpg.frontends.Language import de.fraunhofer.aisec.cpg.frontends.LanguageFrontend +import de.fraunhofer.aisec.cpg.frontends.LanguageInterface import de.fraunhofer.aisec.cpg.graph.Component import de.fraunhofer.aisec.cpg.graph.Node import de.fraunhofer.aisec.cpg.graph.types.HasType.TypeObserver @@ -114,6 +115,7 @@ private constructor( /** This list contains the files with function summaries which should be considered. */ val functionSummaries: DFGFunctionSummaries, languages: Set>>, + languageInterfaces: Set, out Language<*>>>>, codeInNodes: Boolean, processAnnotations: Boolean, disableCleanup: Boolean, @@ -138,6 +140,10 @@ private constructor( /** This list contains all languages which we want to translate. */ @JsonIgnore val languages: Set>> + /** This list contains all language interfaces. */ + @JsonIgnore + val languageInterfaces: Set, out Language<*>>>> + /** * Switch off cleaning up TypeManager memory after analysis. * @@ -207,6 +213,7 @@ private constructor( init { this.registeredPasses = passes this.languages = languages + this.languageInterfaces = languageInterfaces // Make sure to init this AFTER sourceLocations has been set this.codeInNodes = codeInNodes this.processAnnotations = processAnnotations @@ -248,6 +255,8 @@ private constructor( class Builder { private var softwareComponents: MutableMap> = HashMap() private val languages = mutableSetOf>>() + private val languageInterfaces = + mutableSetOf, out Language<*>>>>() private var topLevels = mutableMapOf() private var debugParser = false private var failOnError = false @@ -477,6 +486,22 @@ private constructor( return this } + /** Registers an additional [LanguageInterface] by its [KClass]. */ + fun , out Language<*>>> registerLanguageInterface( + clazz: KClass + ): Builder { + languageInterfaces.add(clazz) + return this + } + + /** Registers an additional [LanguageInterface] */ + inline fun < + reified T : LanguageInterface, out Language<*>> + > registerLanguageInterface(): Builder { + registerLanguageInterface(T::class) + return this + } + /** * Configures a [LanguageFrontend] with a [FrontendConfiguration] [config]. This allows us * to pass additional information to the frontend, such methods which should not be analyzed @@ -742,6 +767,7 @@ private constructor( replacedPasses, DFGFunctionSummaries.fromFiles(functionSummaries), languages, + languageInterfaces, codeInNodes, processAnnotations, disableCleanup, diff --git a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/TranslationContext.kt b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/TranslationContext.kt index 3fb425d5c38..4848c81f9e4 100644 --- a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/TranslationContext.kt +++ b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/TranslationContext.kt @@ -29,6 +29,7 @@ import de.fraunhofer.aisec.cpg.TranslationManager.AdditionalSource import de.fraunhofer.aisec.cpg.TranslationManager.Companion.log import de.fraunhofer.aisec.cpg.frontends.Language import de.fraunhofer.aisec.cpg.frontends.LanguageFrontend +import de.fraunhofer.aisec.cpg.frontends.LanguageInterface import de.fraunhofer.aisec.cpg.graph.Component import de.fraunhofer.aisec.cpg.graph.ContextProvider import de.fraunhofer.aisec.cpg.persistence.DoNotPersist @@ -111,6 +112,17 @@ open class TranslationContext( } } + val languageInterfaces: + Map?, Collection, out Language<*>>>> by lazy { + val list = + config.languageInterfaces.map { + val li = it.constructors.firstOrNull()?.call() + li?.getFrom() to li + } + val m = list.groupBy({ it.first }, { it.second }) + m.map { (k, v) -> k to v.filterNotNull() }.toMap() + } + /** * This extension function returns an appropriate [Language] for this [File] based on the * [availableLanguages], which in turn are based on the registered file extensions of diff --git a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/LanguageInterface.kt b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/LanguageInterface.kt new file mode 100644 index 00000000000..de7d47897b9 --- /dev/null +++ b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/LanguageInterface.kt @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2026, Fraunhofer AISEC. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * $$$$$$\ $$$$$$$\ $$$$$$\ + * $$ __$$\ $$ __$$\ $$ __$$\ + * $$ / \__|$$ | $$ |$$ / \__| + * $$ | $$$$$$$ |$$ |$$$$\ + * $$ | $$ ____/ $$ |\_$$ | + * $$ | $$\ $$ | $$ | $$ | + * \$$$$$ |$$ | \$$$$$ | + * \______/ \__| \______/ + * + */ +package de.fraunhofer.aisec.cpg.frontends + +import de.fraunhofer.aisec.cpg.graph.scopes.Symbol +import de.fraunhofer.aisec.cpg.graph.types.Type +import kotlin.reflect.full.starProjectedType + +abstract class LanguageInterface, TO : Language<*>> { + fun getFrom() = + lazy { + this::class.typeParameters.first().starProjectedType::class + .constructors + .firstOrNull() + ?.call() + } + .value + + inline fun getTo() = lazy { TO::class.constructors.firstOrNull()?.call() }.value + + /** + * Maps the [Symbol] [from] the [FROM] language to a [Symbol] of the [TO] language. This is + * necessary to ensure that we can properly connect the graph nodes of the [FROM] language to + * the graph nodes of the [TO] language. + */ + abstract fun mapSymbol(from: Symbol): Symbol + + /** + * Maps the [Type] [from] the [FROM] language to the [Type] of the [TO] language. This is + * necessary to ensure that we can properly connect the graph nodes of the [FROM] language to + * the graph nodes of the [TO] language. + */ + abstract fun mapType(from: Type): Type +} diff --git a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/scopes/Scope.kt b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/scopes/Scope.kt index eb74ca4001d..7711fc48ec4 100644 --- a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/scopes/Scope.kt +++ b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/graph/scopes/Scope.kt @@ -153,7 +153,7 @@ sealed class Scope( * can be used for additional filtering. * * By default, the lookup algorithm will go to the [Scope.parent] if no match was found in the - * current scope. This behaviour can be turned off with [qualifiedLookup]. This is useful for + * current scope. This behavior can be turned off with [qualifiedLookup]. This is useful for * qualified lookups, where we want to stay in our lookup-scope. * * We need to consider the language trait [HasImplicitReceiver] here as well. If the language diff --git a/cpg-language-cxx/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/cxx/CAndCxxMapper.kt b/cpg-language-cxx/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/cxx/CAndCxxMapper.kt new file mode 100644 index 00000000000..da38fd1caed --- /dev/null +++ b/cpg-language-cxx/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/cxx/CAndCxxMapper.kt @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2026, Fraunhofer AISEC. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * $$$$$$\ $$$$$$$\ $$$$$$\ + * $$ __$$\ $$ __$$\ $$ __$$\ + * $$ / \__|$$ | $$ |$$ / \__| + * $$ | $$$$$$$ |$$ |$$$$\ + * $$ | $$ ____/ $$ |\_$$ | + * $$ | $$\ $$ | $$ | $$ | + * \$$$$$ |$$ | \$$$$$ | + * \______/ \__| \______/ + * + */ +package de.fraunhofer.aisec.cpg.frontends.cxx + +import de.fraunhofer.aisec.cpg.frontends.LanguageInterface +import de.fraunhofer.aisec.cpg.graph.scopes.Symbol +import de.fraunhofer.aisec.cpg.graph.types.Type + +class CToCxxMapper : LanguageInterface() { + override fun mapSymbol(from: Symbol): Symbol { + return from + } + + override fun mapType(from: Type): Type { + return from + } +} + +class CxxToCMapper : LanguageInterface() { + override fun mapSymbol(from: Symbol): Symbol { + return from + } + + override fun mapType(from: Type): Type { + return from + } +} diff --git a/cpg-language-cxx/src/test/kotlin/de/fraunhofer/aisec/cpg/enhancements/LanguageInterfaceTest.kt b/cpg-language-cxx/src/test/kotlin/de/fraunhofer/aisec/cpg/enhancements/LanguageInterfaceTest.kt new file mode 100644 index 00000000000..7567bfa7707 --- /dev/null +++ b/cpg-language-cxx/src/test/kotlin/de/fraunhofer/aisec/cpg/enhancements/LanguageInterfaceTest.kt @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2026, Fraunhofer AISEC. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * $$$$$$\ $$$$$$$\ $$$$$$\ + * $$ __$$\ $$ __$$\ $$ __$$\ + * $$ / \__|$$ | $$ |$$ / \__| + * $$ | $$$$$$$ |$$ |$$$$\ + * $$ | $$ ____/ $$ |\_$$ | + * $$ | $$\ $$ | $$ | $$ | + * \$$$$$ |$$ | \$$$$$ | + * \______/ \__| \______/ + * + */ +package de.fraunhofer.aisec.cpg.enhancements + +import de.fraunhofer.aisec.cpg.frontends.cxx.CLanguage +import de.fraunhofer.aisec.cpg.frontends.cxx.CPPLanguage +import de.fraunhofer.aisec.cpg.frontends.cxx.CToCxxMapper +import de.fraunhofer.aisec.cpg.graph.* +import de.fraunhofer.aisec.cpg.test.analyze +import java.nio.file.Path +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull + +class LanguageInterfaceTest { + private val topLevel = Path.of("src", "test", "resources", "constructors") + + @Test + @Throws(Exception::class) + fun testCtoCPP() { + val result = + analyze(files = listOf(topLevel.toFile()), topLevel = topLevel, usePasses = true) { + it.registerLanguage() + it.registerLanguage() + it.registerLanguageInterface() + it.registerLanguageInterface() + } + + val main = result.functions["main"] + assertNotNull(main) + val call = main.calls["hello_world"] + assertNotNull(call) + + val helloWorlds = result.functions("hello_world") + assertEquals(1, helloWorlds.size) + val helloWorld = helloWorlds.single() + + assertEquals(helloWorld, call.invokes.singleOrNull()) + } +} diff --git a/cpg-language-cxx/src/test/resources/crossLanguage/simple.c b/cpg-language-cxx/src/test/resources/crossLanguage/simple.c new file mode 100644 index 00000000000..ef70c03a146 --- /dev/null +++ b/cpg-language-cxx/src/test/resources/crossLanguage/simple.c @@ -0,0 +1,6 @@ +#include "simple.h" +#include + +void hello_world(void) { + printf("Hello from C!\n"); +} \ No newline at end of file diff --git a/cpg-language-cxx/src/test/resources/crossLanguage/simple.h b/cpg-language-cxx/src/test/resources/crossLanguage/simple.h new file mode 100644 index 00000000000..93fd9df3275 --- /dev/null +++ b/cpg-language-cxx/src/test/resources/crossLanguage/simple.h @@ -0,0 +1,14 @@ +#ifndef SIMPLE_H +#define SIMPLE_H + +#ifdef __cplusplus +extern "C" { +#endif + +void hello_world(void); + +#ifdef __cplusplus +} +#endif + +#endif /* SIMPLE_H */ \ No newline at end of file diff --git a/cpg-language-cxx/src/test/resources/crossLanguage/simpleCxx.cpp b/cpg-language-cxx/src/test/resources/crossLanguage/simpleCxx.cpp new file mode 100644 index 00000000000..c6caab91ded --- /dev/null +++ b/cpg-language-cxx/src/test/resources/crossLanguage/simpleCxx.cpp @@ -0,0 +1,6 @@ +#include "simple.h" + +int main() { + hello_world(); + return 0; +} \ No newline at end of file From 6f2707d77b42832d8b6a3c7ab8811e6f4fa755c9 Mon Sep 17 00:00:00 2001 From: Alexander Kuechler Date: Wed, 22 Apr 2026 15:23:29 +0200 Subject: [PATCH 2/4] Some more magic --- .../de/fraunhofer/aisec/cpg/ScopeManager.kt | 2 +- .../fraunhofer/aisec/cpg/TranslationContext.kt | 2 +- .../aisec/cpg/frontends/LanguageInterface.kt | 13 +------------ .../aisec/cpg/frontends/cxx/CAndCxxMapper.kt | 4 ++-- .../cpg/enhancements/LanguageInterfaceTest.kt | 17 ++++++++++++----- .../src/test/resources/crossLanguage/simple.c | 1 - .../src/test/resources/crossLanguage/simple.h | 14 -------------- .../test/resources/crossLanguage/simpleCxx.cpp | 6 +++++- 8 files changed, 22 insertions(+), 37 deletions(-) diff --git a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/ScopeManager.kt b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/ScopeManager.kt index aeabf9250be..ad309b6579a 100644 --- a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/ScopeManager.kt +++ b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/ScopeManager.kt @@ -815,7 +815,7 @@ class ScopeManager(override var ctx: TranslationContext) : ScopeProvider, Contex val otherLanguageInterfaces = ctx.languageInterfaces[language] otherLanguageInterfaces?.forEach { otherLanguageInterface -> - val otherLanguage = otherLanguageInterface.getTo>() + val otherLanguage = otherLanguageInterface.to val symbol = otherLanguageInterface.mapSymbol(n.localName) val toAdd = diff --git a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/TranslationContext.kt b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/TranslationContext.kt index 4848c81f9e4..11683e08ab3 100644 --- a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/TranslationContext.kt +++ b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/TranslationContext.kt @@ -117,7 +117,7 @@ open class TranslationContext( val list = config.languageInterfaces.map { val li = it.constructors.firstOrNull()?.call() - li?.getFrom() to li + li?.from to li } val m = list.groupBy({ it.first }, { it.second }) m.map { (k, v) -> k to v.filterNotNull() }.toMap() diff --git a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/LanguageInterface.kt b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/LanguageInterface.kt index de7d47897b9..1500db1b1ad 100644 --- a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/LanguageInterface.kt +++ b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/LanguageInterface.kt @@ -27,19 +27,8 @@ package de.fraunhofer.aisec.cpg.frontends import de.fraunhofer.aisec.cpg.graph.scopes.Symbol import de.fraunhofer.aisec.cpg.graph.types.Type -import kotlin.reflect.full.starProjectedType -abstract class LanguageInterface, TO : Language<*>> { - fun getFrom() = - lazy { - this::class.typeParameters.first().starProjectedType::class - .constructors - .firstOrNull() - ?.call() - } - .value - - inline fun getTo() = lazy { TO::class.constructors.firstOrNull()?.call() }.value +abstract class LanguageInterface, TO : Language<*>>(val from: FROM, val to: TO) { /** * Maps the [Symbol] [from] the [FROM] language to a [Symbol] of the [TO] language. This is diff --git a/cpg-language-cxx/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/cxx/CAndCxxMapper.kt b/cpg-language-cxx/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/cxx/CAndCxxMapper.kt index da38fd1caed..fbdfd1ce314 100644 --- a/cpg-language-cxx/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/cxx/CAndCxxMapper.kt +++ b/cpg-language-cxx/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/cxx/CAndCxxMapper.kt @@ -29,7 +29,7 @@ import de.fraunhofer.aisec.cpg.frontends.LanguageInterface import de.fraunhofer.aisec.cpg.graph.scopes.Symbol import de.fraunhofer.aisec.cpg.graph.types.Type -class CToCxxMapper : LanguageInterface() { +class CToCxxMapper : LanguageInterface(CLanguage(), CPPLanguage()) { override fun mapSymbol(from: Symbol): Symbol { return from } @@ -39,7 +39,7 @@ class CToCxxMapper : LanguageInterface() { } } -class CxxToCMapper : LanguageInterface() { +class CxxToCMapper : LanguageInterface(CPPLanguage(), CLanguage()) { override fun mapSymbol(from: Symbol): Symbol { return from } diff --git a/cpg-language-cxx/src/test/kotlin/de/fraunhofer/aisec/cpg/enhancements/LanguageInterfaceTest.kt b/cpg-language-cxx/src/test/kotlin/de/fraunhofer/aisec/cpg/enhancements/LanguageInterfaceTest.kt index 7567bfa7707..9a0e5eb594e 100644 --- a/cpg-language-cxx/src/test/kotlin/de/fraunhofer/aisec/cpg/enhancements/LanguageInterfaceTest.kt +++ b/cpg-language-cxx/src/test/kotlin/de/fraunhofer/aisec/cpg/enhancements/LanguageInterfaceTest.kt @@ -27,7 +27,6 @@ package de.fraunhofer.aisec.cpg.enhancements import de.fraunhofer.aisec.cpg.frontends.cxx.CLanguage import de.fraunhofer.aisec.cpg.frontends.cxx.CPPLanguage -import de.fraunhofer.aisec.cpg.frontends.cxx.CToCxxMapper import de.fraunhofer.aisec.cpg.graph.* import de.fraunhofer.aisec.cpg.test.analyze import java.nio.file.Path @@ -36,17 +35,25 @@ import kotlin.test.assertEquals import kotlin.test.assertNotNull class LanguageInterfaceTest { - private val topLevel = Path.of("src", "test", "resources", "constructors") + private val topLevel = Path.of("src", "test", "resources", "crossLanguage") @Test @Throws(Exception::class) fun testCtoCPP() { val result = - analyze(files = listOf(topLevel.toFile()), topLevel = topLevel, usePasses = true) { + analyze( + files = + listOf( + topLevel.resolve("simple.c").toFile(), + topLevel.resolve("simpleCxx.cpp").toFile(), + ), + topLevel = topLevel, + usePasses = true, + ) { it.registerLanguage() it.registerLanguage() - it.registerLanguageInterface() - it.registerLanguageInterface() + // it.registerLanguageInterface() + // it.registerLanguageInterface() } val main = result.functions["main"] diff --git a/cpg-language-cxx/src/test/resources/crossLanguage/simple.c b/cpg-language-cxx/src/test/resources/crossLanguage/simple.c index ef70c03a146..8727b4ae131 100644 --- a/cpg-language-cxx/src/test/resources/crossLanguage/simple.c +++ b/cpg-language-cxx/src/test/resources/crossLanguage/simple.c @@ -1,4 +1,3 @@ -#include "simple.h" #include void hello_world(void) { diff --git a/cpg-language-cxx/src/test/resources/crossLanguage/simple.h b/cpg-language-cxx/src/test/resources/crossLanguage/simple.h index 93fd9df3275..e69de29bb2d 100644 --- a/cpg-language-cxx/src/test/resources/crossLanguage/simple.h +++ b/cpg-language-cxx/src/test/resources/crossLanguage/simple.h @@ -1,14 +0,0 @@ -#ifndef SIMPLE_H -#define SIMPLE_H - -#ifdef __cplusplus -extern "C" { -#endif - -void hello_world(void); - -#ifdef __cplusplus -} -#endif - -#endif /* SIMPLE_H */ \ No newline at end of file diff --git a/cpg-language-cxx/src/test/resources/crossLanguage/simpleCxx.cpp b/cpg-language-cxx/src/test/resources/crossLanguage/simpleCxx.cpp index c6caab91ded..52f20472973 100644 --- a/cpg-language-cxx/src/test/resources/crossLanguage/simpleCxx.cpp +++ b/cpg-language-cxx/src/test/resources/crossLanguage/simpleCxx.cpp @@ -1,4 +1,8 @@ -#include "simple.h" +extern "C" { + +void hello_world(void); + +} int main() { hello_world(); From 14abe27810b38717c3d9e6454f8c92d060c123f5 Mon Sep 17 00:00:00 2001 From: Alexander Kuechler Date: Wed, 22 Apr 2026 15:33:04 +0200 Subject: [PATCH 3/4] fix test --- .../aisec/cpg/enhancements/LanguageInterfaceTest.kt | 6 ++++-- cpg-language-cxx/src/test/resources/crossLanguage/simple.h | 0 .../src/test/resources/crossLanguage/simpleCxx.cpp | 6 ------ 3 files changed, 4 insertions(+), 8 deletions(-) delete mode 100644 cpg-language-cxx/src/test/resources/crossLanguage/simple.h diff --git a/cpg-language-cxx/src/test/kotlin/de/fraunhofer/aisec/cpg/enhancements/LanguageInterfaceTest.kt b/cpg-language-cxx/src/test/kotlin/de/fraunhofer/aisec/cpg/enhancements/LanguageInterfaceTest.kt index 9a0e5eb594e..cd50521ea06 100644 --- a/cpg-language-cxx/src/test/kotlin/de/fraunhofer/aisec/cpg/enhancements/LanguageInterfaceTest.kt +++ b/cpg-language-cxx/src/test/kotlin/de/fraunhofer/aisec/cpg/enhancements/LanguageInterfaceTest.kt @@ -27,6 +27,8 @@ package de.fraunhofer.aisec.cpg.enhancements import de.fraunhofer.aisec.cpg.frontends.cxx.CLanguage import de.fraunhofer.aisec.cpg.frontends.cxx.CPPLanguage +import de.fraunhofer.aisec.cpg.frontends.cxx.CToCxxMapper +import de.fraunhofer.aisec.cpg.frontends.cxx.CxxToCMapper import de.fraunhofer.aisec.cpg.graph.* import de.fraunhofer.aisec.cpg.test.analyze import java.nio.file.Path @@ -52,8 +54,8 @@ class LanguageInterfaceTest { ) { it.registerLanguage() it.registerLanguage() - // it.registerLanguageInterface() - // it.registerLanguageInterface() + it.registerLanguageInterface() + it.registerLanguageInterface() } val main = result.functions["main"] diff --git a/cpg-language-cxx/src/test/resources/crossLanguage/simple.h b/cpg-language-cxx/src/test/resources/crossLanguage/simple.h deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/cpg-language-cxx/src/test/resources/crossLanguage/simpleCxx.cpp b/cpg-language-cxx/src/test/resources/crossLanguage/simpleCxx.cpp index 52f20472973..501e815cfb4 100644 --- a/cpg-language-cxx/src/test/resources/crossLanguage/simpleCxx.cpp +++ b/cpg-language-cxx/src/test/resources/crossLanguage/simpleCxx.cpp @@ -1,9 +1,3 @@ -extern "C" { - -void hello_world(void); - -} - int main() { hello_world(); return 0; From 1eb87eeddef554c782d3609ac5b31da78f00ab08 Mon Sep 17 00:00:00 2001 From: Alexander Kuechler Date: Wed, 6 May 2026 14:53:01 +0200 Subject: [PATCH 4/4] Unfinished test --- cpg-core/build.gradle.kts | 3 +- .../fraunhofer/aisec/cpg/IntegrationTest.kt | 63 +++++++++++ .../foreignFunctionInterface/MyClass.java | 14 +++ .../foreignFunctionInterface/native.c | 8 ++ .../de/fraunhofer/aisec/cpg/ScopeManager.kt | 102 +++++++++--------- .../aisec/cpg/TranslationConfiguration.kt | 21 ++-- .../aisec/cpg/TranslationContext.kt | 7 +- ...terface.kt => ForeignFunctionInterface.kt} | 18 +++- .../aisec/cpg/frontends/cxx/CAndCxxMapper.kt | 6 +- 9 files changed, 174 insertions(+), 68 deletions(-) create mode 100644 cpg-core/src/integrationTest/resources/foreignFunctionInterface/MyClass.java create mode 100644 cpg-core/src/integrationTest/resources/foreignFunctionInterface/native.c rename cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/{LanguageInterface.kt => ForeignFunctionInterface.kt} (65%) diff --git a/cpg-core/build.gradle.kts b/cpg-core/build.gradle.kts index 1fd52ed78d2..6347059bc21 100644 --- a/cpg-core/build.gradle.kts +++ b/cpg-core/build.gradle.kts @@ -54,7 +54,8 @@ dependencies { implementation(libs.kotlinx.coroutines.core) testImplementation(libs.junit.params) - integrationTestImplementation(libs.kotlin.reflect) + findProject(":cpg-language-java")?.also { integrationTestImplementation(it) } + findProject(":cpg-language-cxx")?.also { integrationTestImplementation(it) } testFixturesApi( libs.kotlin.test.junit5 diff --git a/cpg-core/src/integrationTest/kotlin/de/fraunhofer/aisec/cpg/IntegrationTest.kt b/cpg-core/src/integrationTest/kotlin/de/fraunhofer/aisec/cpg/IntegrationTest.kt index 6c347cc4cb7..022c975e1fa 100644 --- a/cpg-core/src/integrationTest/kotlin/de/fraunhofer/aisec/cpg/IntegrationTest.kt +++ b/cpg-core/src/integrationTest/kotlin/de/fraunhofer/aisec/cpg/IntegrationTest.kt @@ -25,15 +25,24 @@ */ package de.fraunhofer.aisec.cpg +import de.fraunhofer.aisec.cpg.frontends.ForeignFunctionInterface +import de.fraunhofer.aisec.cpg.frontends.cxx.CLanguage +import de.fraunhofer.aisec.cpg.frontends.java.JavaLanguage +import de.fraunhofer.aisec.cpg.graph.* import de.fraunhofer.aisec.cpg.graph.declarations.Function import de.fraunhofer.aisec.cpg.graph.expressions.Call import de.fraunhofer.aisec.cpg.graph.functions +import de.fraunhofer.aisec.cpg.graph.scopes.Symbol +import de.fraunhofer.aisec.cpg.graph.types.Type import de.fraunhofer.aisec.cpg.persistence.createJsonGraph import de.fraunhofer.aisec.cpg.persistence.persistJson import de.fraunhofer.aisec.cpg.test.GraphExamples +import de.fraunhofer.aisec.cpg.test.analyze +import kotlin.io.path.Path import kotlin.io.path.createTempFile import kotlin.test.assertEquals import kotlin.test.assertNotNull +import kotlin.test.assertTrue import org.junit.jupiter.api.Test /** @@ -42,6 +51,60 @@ import org.junit.jupiter.api.Test */ class IntegrationTest { + class JavaCFFI : + ForeignFunctionInterface(JavaLanguage(), CLanguage()) { + override fun mapSymbol(from: Symbol): Symbol { + val isNativeAdd = + from == "nativeAdd" || + from.endsWith(".nativeAdd") || + from.contains(".nativeAdd(") || + from.contains("nativeAdd(") + + return if (isNativeAdd) { + "Java_MyClass_nativeAdd" + } else { + from + } + } + + override fun mapType(from: Type): Type { + return to.builtInTypes[from.name.toString()] ?: from + } + } + + // + // --report-output="study/python_analysis_output.py" --path-suffix="-python" + + @Test + fun testForeignFunctionInterface() { + val topLevel = Path("src/integrationTest/resources/foreignFunctionInterface") + val result = + analyze( + listOf( + topLevel.resolve("MyClass.java").toFile(), + topLevel.resolve("native.c").toFile(), + ), + topLevel, + usePasses = true, + ) { + it.registerLanguage() + it.registerLanguage() + it.registerLanguageInterface() + } + assertNotNull(result) + + val useNativeAdd = result.functions["useNativeAdd"] + assertNotNull(useNativeAdd) + + val nativeCall = useNativeAdd.calls["nativeAdd"] + assertNotNull(nativeCall) + + val nativeImplementation = result.functions["Java_MyClass_nativeAdd"] + assertNotNull(nativeImplementation) + + assertTrue(nativeCall.invokes.contains(nativeImplementation)) + } + @Test fun testBuildJsonGraph() { val translationResult = GraphExamples.getInitializerListExprDFG() diff --git a/cpg-core/src/integrationTest/resources/foreignFunctionInterface/MyClass.java b/cpg-core/src/integrationTest/resources/foreignFunctionInterface/MyClass.java new file mode 100644 index 00000000000..f115a31a848 --- /dev/null +++ b/cpg-core/src/integrationTest/resources/foreignFunctionInterface/MyClass.java @@ -0,0 +1,14 @@ +public class MyClass { + // Declared in native.c via JNI naming convention. + public static native int nativeAdd(int left, int right); + + public static int useNativeAdd() { + return nativeAdd(4, 7); + } + + public static void main(String[] args) { + int result = useNativeAdd(); + System.out.println("JNI result: " + result); + } +} + diff --git a/cpg-core/src/integrationTest/resources/foreignFunctionInterface/native.c b/cpg-core/src/integrationTest/resources/foreignFunctionInterface/native.c new file mode 100644 index 00000000000..b2df2b6a956 --- /dev/null +++ b/cpg-core/src/integrationTest/resources/foreignFunctionInterface/native.c @@ -0,0 +1,8 @@ +#include + +JNIEXPORT jint JNICALL Java_MyClass_nativeAdd(JNIEnv *env, jclass cls, jint left, jint right) { + (void)env; + (void)cls; + return left + right; +} + diff --git a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/ScopeManager.kt b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/ScopeManager.kt index ad309b6579a..605e49abf25 100644 --- a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/ScopeManager.kt +++ b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/ScopeManager.kt @@ -787,31 +787,14 @@ class ScopeManager(override var ctx: TranslationContext) : ScopeProvider, Contex // We need to differentiate between a qualified and unqualified lookup. We have a qualified // lookup, if the scope is not null. In this case we need to stay within the specified scope val list = - when { - scope != null -> { - scope - .lookupSymbol( - n.localName, - languageOnly = language, - qualifiedLookup = true, - replaceImports = replaceImports, - predicate = predicate, - ) - .toMutableList() - } - else -> { - // Otherwise, we can look up the symbol alone (without any FQN) starting from - // the startScope - startScope - ?.lookupSymbol( - n.localName, - languageOnly = language, - replaceImports = replaceImports, - predicate = predicate, - ) - ?.toMutableList() ?: mutableListOf() - } - }.toMutableList() + findDeclarationsByName( + scope, + n.localName, + language, + replaceImports, + predicate, + startScope, + ) val otherLanguageInterfaces = ctx.languageInterfaces[language] otherLanguageInterfaces?.forEach { otherLanguageInterface -> @@ -819,32 +802,14 @@ class ScopeManager(override var ctx: TranslationContext) : ScopeProvider, Contex val symbol = otherLanguageInterface.mapSymbol(n.localName) val toAdd = - when { - scope != null -> { - scope - .lookupSymbol( - symbol, - languageOnly = otherLanguage, - qualifiedLookup = true, - replaceImports = replaceImports, - predicate = predicate, - ) - .toMutableList() - } - else -> { - // Otherwise, we can look up the symbol alone (without any FQN) starting - // from - // the startScope - startScope - ?.lookupSymbol( - symbol, - languageOnly = otherLanguage, - replaceImports = replaceImports, - predicate = predicate, - ) - ?.toMutableList() ?: mutableListOf() - } - } + findDeclarationsByName( + scope, + symbol, + otherLanguage, + replaceImports, + predicate, + startScope, + ) list.addAll(toAdd) } @@ -865,6 +830,41 @@ class ScopeManager(override var ctx: TranslationContext) : ScopeProvider, Contex return list } + private fun findDeclarationsByName( + scope: Scope?, + symbol: Symbol, + language: Language<*>, + replaceImports: Boolean, + predicate: ((Declaration) -> Boolean)?, + startScope: Scope?, + ): MutableList = + when { + scope != null -> { + scope + .lookupSymbol( + symbol, + languageOnly = language, + qualifiedLookup = true, + replaceImports = replaceImports, + predicate = predicate, + ) + .toMutableList() + } + + else -> { + // Otherwise, we can look up the symbol alone (without any FQN) starting from + // the startScope + startScope + ?.lookupSymbol( + symbol, + languageOnly = language, + replaceImports = replaceImports, + predicate = predicate, + ) + ?.toMutableList() ?: mutableListOf() + } + }.toMutableList() + /** * This function tries to look up the symbol contained in [name] (using [lookupSymbolByName]) * and returns a [DeclaresType] node, if this name resolved to something which declares a type. diff --git a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/TranslationConfiguration.kt b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/TranslationConfiguration.kt index dd58bd9dd14..4e5c75f2ea9 100644 --- a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/TranslationConfiguration.kt +++ b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/TranslationConfiguration.kt @@ -31,11 +31,11 @@ import com.fasterxml.jackson.databind.annotation.JsonSerialize import de.fraunhofer.aisec.cpg.TranslationContext.EmptyTranslationContext import de.fraunhofer.aisec.cpg.TranslationResult.Companion.DEFAULT_APPLICATION_NAME import de.fraunhofer.aisec.cpg.frontends.CompilationDatabase +import de.fraunhofer.aisec.cpg.frontends.ForeignFunctionInterface import de.fraunhofer.aisec.cpg.frontends.FrontendConfiguration import de.fraunhofer.aisec.cpg.frontends.KClassSerializer import de.fraunhofer.aisec.cpg.frontends.Language import de.fraunhofer.aisec.cpg.frontends.LanguageFrontend -import de.fraunhofer.aisec.cpg.frontends.LanguageInterface import de.fraunhofer.aisec.cpg.graph.Component import de.fraunhofer.aisec.cpg.graph.Node import de.fraunhofer.aisec.cpg.graph.types.HasType.TypeObserver @@ -115,7 +115,7 @@ private constructor( /** This list contains the files with function summaries which should be considered. */ val functionSummaries: DFGFunctionSummaries, languages: Set>>, - languageInterfaces: Set, out Language<*>>>>, + languageInterfaces: Set, out Language<*>>>>, codeInNodes: Boolean, processAnnotations: Boolean, disableCleanup: Boolean, @@ -142,7 +142,8 @@ private constructor( /** This list contains all language interfaces. */ @JsonIgnore - val languageInterfaces: Set, out Language<*>>>> + val languageInterfaces: + Set, out Language<*>>>> /** * Switch off cleaning up TypeManager memory after analysis. @@ -256,7 +257,7 @@ private constructor( private var softwareComponents: MutableMap> = HashMap() private val languages = mutableSetOf>>() private val languageInterfaces = - mutableSetOf, out Language<*>>>>() + mutableSetOf, out Language<*>>>>() private var topLevels = mutableMapOf() private var debugParser = false private var failOnError = false @@ -486,17 +487,17 @@ private constructor( return this } - /** Registers an additional [LanguageInterface] by its [KClass]. */ - fun , out Language<*>>> registerLanguageInterface( - clazz: KClass - ): Builder { + /** Registers an additional [ForeignFunctionInterface] by its [KClass]. */ + fun < + T : ForeignFunctionInterface, out Language<*>> + > registerLanguageInterface(clazz: KClass): Builder { languageInterfaces.add(clazz) return this } - /** Registers an additional [LanguageInterface] */ + /** Registers an additional [ForeignFunctionInterface] */ inline fun < - reified T : LanguageInterface, out Language<*>> + reified T : ForeignFunctionInterface, out Language<*>> > registerLanguageInterface(): Builder { registerLanguageInterface(T::class) return this diff --git a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/TranslationContext.kt b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/TranslationContext.kt index 11683e08ab3..82dab813b14 100644 --- a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/TranslationContext.kt +++ b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/TranslationContext.kt @@ -27,9 +27,9 @@ package de.fraunhofer.aisec.cpg import de.fraunhofer.aisec.cpg.TranslationManager.AdditionalSource import de.fraunhofer.aisec.cpg.TranslationManager.Companion.log +import de.fraunhofer.aisec.cpg.frontends.ForeignFunctionInterface import de.fraunhofer.aisec.cpg.frontends.Language import de.fraunhofer.aisec.cpg.frontends.LanguageFrontend -import de.fraunhofer.aisec.cpg.frontends.LanguageInterface import de.fraunhofer.aisec.cpg.graph.Component import de.fraunhofer.aisec.cpg.graph.ContextProvider import de.fraunhofer.aisec.cpg.persistence.DoNotPersist @@ -113,7 +113,10 @@ open class TranslationContext( } val languageInterfaces: - Map?, Collection, out Language<*>>>> by lazy { + Map< + Language<*>?, + Collection, out Language<*>>>, + > by lazy { val list = config.languageInterfaces.map { val li = it.constructors.firstOrNull()?.call() diff --git a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/LanguageInterface.kt b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/ForeignFunctionInterface.kt similarity index 65% rename from cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/LanguageInterface.kt rename to cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/ForeignFunctionInterface.kt index 1500db1b1ad..1196b9a4f99 100644 --- a/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/LanguageInterface.kt +++ b/cpg-core/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/ForeignFunctionInterface.kt @@ -28,7 +28,23 @@ package de.fraunhofer.aisec.cpg.frontends import de.fraunhofer.aisec.cpg.graph.scopes.Symbol import de.fraunhofer.aisec.cpg.graph.types.Type -abstract class LanguageInterface, TO : Language<*>>(val from: FROM, val to: TO) { +/** + * The CPG can hold nodes of different programming languages at the same time. There are several + * frameworks which allow calling functions from one language to another one. Examples which we also + * use in this repository are JNI or jep. However, the + * [de.fraunhofer.aisec.cpg.passes.SymbolResolver] won't be able to resolve functions across such + * interfaces for several reasons: First, it expects that the symbol and the declaration it refers + * to are of the same language. Second, the interfaces may slightly change the name and for sure the + * [Type]s (since each type has its own language). + * + * This class provides a way to specify such interfaces between two programming languages. It models + * the way to resolve a symbol from the language [from] (e.g., caller) to the language [to] (e.g., + * callee, declaration). + */ +abstract class ForeignFunctionInterface, TO : Language<*>>( + val from: FROM, + val to: TO, +) { /** * Maps the [Symbol] [from] the [FROM] language to a [Symbol] of the [TO] language. This is diff --git a/cpg-language-cxx/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/cxx/CAndCxxMapper.kt b/cpg-language-cxx/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/cxx/CAndCxxMapper.kt index fbdfd1ce314..ed51b666a02 100644 --- a/cpg-language-cxx/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/cxx/CAndCxxMapper.kt +++ b/cpg-language-cxx/src/main/kotlin/de/fraunhofer/aisec/cpg/frontends/cxx/CAndCxxMapper.kt @@ -25,11 +25,11 @@ */ package de.fraunhofer.aisec.cpg.frontends.cxx -import de.fraunhofer.aisec.cpg.frontends.LanguageInterface +import de.fraunhofer.aisec.cpg.frontends.ForeignFunctionInterface import de.fraunhofer.aisec.cpg.graph.scopes.Symbol import de.fraunhofer.aisec.cpg.graph.types.Type -class CToCxxMapper : LanguageInterface(CLanguage(), CPPLanguage()) { +class CToCxxMapper : ForeignFunctionInterface(CLanguage(), CPPLanguage()) { override fun mapSymbol(from: Symbol): Symbol { return from } @@ -39,7 +39,7 @@ class CToCxxMapper : LanguageInterface(CLanguage(), CPPL } } -class CxxToCMapper : LanguageInterface(CPPLanguage(), CLanguage()) { +class CxxToCMapper : ForeignFunctionInterface(CPPLanguage(), CLanguage()) { override fun mapSymbol(from: Symbol): Symbol { return from }