diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index e4576fe7..77e37962 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -10,15 +10,17 @@ on: jobs: build: runs-on: ubuntu-latest + env: + JAVA_OPTS: -Xms2048M -Xmx2048M -Xss6M -XX:ReservedCodeCacheSize=256M -Dfile.encoding=UTF-8 steps: - uses: actions/checkout@v4 - - name: Caching dependencies - uses: actions/cache@v3 + - name: Setup JDK + uses: actions/setup-java@v4 with: - path: | - ~/.sbt - ~/.ivy2 - key: scala-build-deps + distribution: temurin + java-version: 17 + cache: sbt + - uses: sbt/setup-sbt@v1 - name: Building run: sbt assembly - uses: actions/upload-artifact@v4 @@ -29,11 +31,13 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - name: Caching dependencies - uses: actions/cache@v3 + - name: Setup JDK + uses: actions/setup-java@v4 with: - path: ~/.sbt - key: scala-fmt-deps + distribution: temurin + java-version: 17 + cache: sbt + - uses: sbt/setup-sbt@v1 - name: "Format check generator" run: sbt scalafmtCheck - name: "Format check integration test" @@ -43,13 +47,13 @@ jobs: needs: [build, formatCheck] steps: - uses: actions/checkout@v4 - - name: Caching dependencies - uses: actions/cache@v3 + - name: Setup JDK + uses: actions/setup-java@v4 with: - path: | - ~/.sbt - ~/.ivy2 - key: scala-build-deps + distribution: temurin + java-version: 17 + cache: sbt + - uses: sbt/setup-sbt@v1 - uses: actions/download-artifact@v4 with: name: djinni-generator @@ -61,15 +65,17 @@ jobs: buildWindows: runs-on: windows-latest + env: + JAVA_OPTS: -Xms2048M -Xmx2048M -Xss6M -XX:ReservedCodeCacheSize=256M -Dfile.encoding=UTF-8 steps: - uses: actions/checkout@v4 - - name: Caching dependencies - uses: actions/cache@v3 + - name: Setup JDK + uses: actions/setup-java@v4 with: - path: | - ~/.sbt - ~/.ivy2 - key: scala-build-deps-windows + distribution: temurin + java-version: 17 + cache: sbt + - uses: sbt/setup-sbt@v1 - name: Building run: sbt assembly - name: Testing diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index dff43650..99d33697 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -8,15 +8,17 @@ name: Upload Release Assets jobs: buildUnix: runs-on: ubuntu-latest + env: + JAVA_OPTS: -Xms2048M -Xmx2048M -Xss6M -XX:ReservedCodeCacheSize=256M -Dfile.encoding=UTF-8 steps: - uses: actions/checkout@v4 - - name: Caching dependencies - uses: actions/cache@v3 + - name: Setup JDK + uses: actions/setup-java@v4 with: - path: | - ~/.sbt - ~/.ivy2 - key: scala-build-deps + distribution: temurin + java-version: 17 + cache: sbt + - uses: sbt/setup-sbt@v1 - name: Building run: sbt assembly - uses: actions/upload-artifact@v4 diff --git a/src/main/scala/djinni/CppGenerator.scala b/src/main/scala/djinni/CppGenerator.scala index ced2b138..b9c3c324 100644 --- a/src/main/scala/djinni/CppGenerator.scala +++ b/src/main/scala/djinni/CppGenerator.scala @@ -15,6 +15,7 @@ package djinni +import djinni.ast.Interface.RequiresType import djinni.ast.Record.DerivingType import djinni.ast._ import djinni.generatorTools._ @@ -744,6 +745,38 @@ class CppGenerator(spec: Spec) extends Generator(spec) { .mkString("(", ", ", ")")}$constFlag = 0;") } } + // Requires + if (!i.requiresTypes.isEmpty) { + if (i.ext.cpp) { + w.wl + w.w("class Operators").bracedSemi { + w.wlOutdent("public:") + if (i.requiresTypes.contains(RequiresType.Eq)) { + w.wl( + s"static bool equals(const ${self}& left, const ${self}& right);" + ) + w.wl + w.wl(s"static int32_t hashCode(const ${self}& object);") + } + } + } + + // TODO: how to apply formating rules to equals/compareTo/hashCode? + + if (i.ext.java) { + if (i.requiresTypes.contains(RequiresType.Eq)) { + w.wl + w.wl(s"virtual bool equals(const ${self}& other) const = 0;") + w.wl + w.wl(s"virtual int hashCode() const = 0;") + } + + if (i.requiresTypes.contains(RequiresType.Ord)) { + w.wl + w.wl(s"virtual int compareTo(const ${self}& other) const = 0;") + } + } + } } } ) diff --git a/src/main/scala/djinni/JNIGenerator.scala b/src/main/scala/djinni/JNIGenerator.scala index 93d265b9..7d78ea65 100644 --- a/src/main/scala/djinni/JNIGenerator.scala +++ b/src/main/scala/djinni/JNIGenerator.scala @@ -15,6 +15,7 @@ package djinni +import djinni.ast.Interface.RequiresType import djinni.ast._ import djinni.generatorTools._ import djinni.meta._ @@ -591,6 +592,52 @@ class JNIGenerator(spec: Spec) extends Generator(spec) { } ) } + if (i.requiresTypes.contains(RequiresType.Eq)) { + val equalsName = "native_operator_equals" + val equalsMethodNameMunged = equalsName.replaceAllLiterally("_", "_1") + w.wl( + s"CJNIEXPORT jboolean JNICALL ${prefix}_00024CppProxy_$equalsMethodNameMunged(JNIEnv* jniEnv, jobject /*this*/, jlong nativeRef, jobject j_obj)" + ).braced { + w.w("try") + .bracedEnd( + s" JNI_TRANSLATE_EXCEPTIONS_RETURN(jniEnv, 0 /* value doesn't matter */)" + ) { + w.wl(s"DJINNI_FUNCTION_PROLOGUE1(jniEnv, nativeRef);") + w.wl( + s"const auto& ref = ::djinni::objectFromHandleAddress<$cppSelf>(nativeRef);" + ) + w.wl( + s"const auto& otherRef = ${withNs(Some(spec.jniNamespace), jniSelf)}::toCpp(jniEnv, j_obj);" + ) + w.wl(s"auto r = $cppSelf::Operators::equals(*ref, *otherRef);") + w.wl( + "return ::djinni::release(::djinni::Bool::fromCpp(jniEnv, r));" + ) + } + } + + val hashCodeName = "native_hash_code" + val hashCodeMethodNameMunged = + hashCodeName.replaceAllLiterally("_", "_1") + w.wl( + s"CJNIEXPORT jint JNICALL ${prefix}_00024CppProxy_$hashCodeMethodNameMunged(JNIEnv* jniEnv, jobject /*this*/, jlong nativeRef)" + ).braced { + w.w("try") + .bracedEnd( + s" JNI_TRANSLATE_EXCEPTIONS_RETURN(jniEnv, 0 /* value doesn't matter */)" + ) { + w.wl(s"DJINNI_FUNCTION_PROLOGUE1(jniEnv, nativeRef);") + w.wl( + s"const auto& ref = ::djinni::objectFromHandleAddress<$cppSelf>(nativeRef);" + ) + w.wl(s"auto r = $cppSelf::Operators::hashCode(*ref);") + w.wl( + "return ::djinni::release(::djinni::I32::fromCpp(jniEnv, r));" + ) + } + } + + } } } diff --git a/src/main/scala/djinni/JavaGenerator.scala b/src/main/scala/djinni/JavaGenerator.scala index 71557e7f..468c016b 100755 --- a/src/main/scala/djinni/JavaGenerator.scala +++ b/src/main/scala/djinni/JavaGenerator.scala @@ -15,6 +15,7 @@ package djinni +import djinni.ast.Interface.RequiresType import djinni.ast.Record.DerivingType import djinni.ast._ import djinni.generatorTools._ @@ -196,6 +197,13 @@ class JavaGenerator(spec: Spec) extends Generator(spec) { javaAnnotationHeader.foreach(w.wl) + val interfaces = scala.collection.mutable.ArrayBuffer[String]() + if (i.requiresTypes.contains(RequiresType.Ord)) + interfaces += s"Comparable<$javaClass>" + val implementsSection = + if (interfaces.isEmpty) "" + else " implements " + interfaces.mkString(", ") + // Generate an interface or an abstract class depending on whether the use // of Java interfaces was requested. val classPrefix = @@ -206,7 +214,7 @@ class JavaGenerator(spec: Spec) extends Generator(spec) { val innerClassAccessibility = if (spec.javaGenerateInterfaces) "" else "private " w.w( - s"${javaClassAccessModifierString}$classPrefix $javaClass$typeParamList" + s"${javaClassAccessModifierString}$classPrefix $javaClass$typeParamList$implementsSection" ).braced { val skipFirst = SkipFirst() generateJavaConstants(w, i.consts, spec.javaGenerateInterfaces) @@ -262,6 +270,24 @@ class JavaGenerator(spec: Spec) extends Generator(spec) { } } + if (i.ext.java) { + if (i.requiresTypes.contains(RequiresType.Eq)) { + w.wl + w.wl("@Override") + if (i.ext.java) { + w.wl("public abstract boolean equals(@Nullable Object obj);") + } + } + + if (i.requiresTypes.contains(RequiresType.Ord)) { + w.wl + w.wl("@Override") + if (i.ext.java) { + w.wl(s"public abstract int compareTo($javaClass other);") + } + } + } + if (i.ext.cpp) { w.wl javaAnnotationHeader.foreach(w.wl) @@ -305,7 +331,7 @@ class JavaGenerator(spec: Spec) extends Generator(spec) { m.params.map(p => idJava.local(p.ident)).mkString(", ") val meth = idJava.method(m.ident) w.wl - w.wl(s"@Override") + w.wl("@Override") w.wl(s"public $ret $meth($params)$throwException").braced { w.wl( "assert !this.destroyed.get() : \"trying to use a destroyed object\";" @@ -319,6 +345,46 @@ class JavaGenerator(spec: Spec) extends Generator(spec) { ) } + if (i.requiresTypes.contains(RequiresType.Eq)) { + // equals() override + w.wl + w.wl("@Override") + val nullableAnnotation = + javaNullableAnnotation.map(_ + " ").getOrElse("") + w.w(s"public boolean equals(${nullableAnnotation}Object obj)") + .braced { + w.wl( + "assert !this.destroyed.get() : \"trying to use a destroyed object\";" + ) + w.wl + w.w(s"if (!(obj instanceof $javaClass))").braced { + w.wl("return false;") + } + w.wl + w.wl( + s"return native_operator_equals(this.nativeRef, ($javaClass)obj);" + ) + } + w.wl( + s"private native boolean native_operator_equals(long _nativeRef, $javaClass other);" + ) + + // hashCode() override + w.wl + w.wl("@Override") + w.w("public int hashCode()").braced { + w.wl( + "assert !this.destroyed.get() : \"trying to use a destroyed object\";" + ) + w.wl( + s"return native_hash_code(this.nativeRef);" + ) + } + w.wl( + s"private native int native_hash_code(long _nativeRef);" + ) + } + // Declare a native method for each of the interface's static methods. for (m <- i.methods if m.static) { skipFirst { w.wl } diff --git a/src/main/scala/djinni/ast/ast.scala b/src/main/scala/djinni/ast/ast.scala index f5ae3c3e..d57637b9 100644 --- a/src/main/scala/djinni/ast/ast.scala +++ b/src/main/scala/djinni/ast/ast.scala @@ -16,6 +16,7 @@ package djinni.ast import djinni.ast.Record.DerivingType.DerivingType +import djinni.ast.Interface.RequiresType.RequiresType import djinni.meta.MExpr import djinni.syntax.Loc @@ -110,9 +111,14 @@ object Record { case class Interface( ext: Ext, methods: Seq[Interface.Method], - consts: Seq[Const] + consts: Seq[Const], + requiresTypes: Set[RequiresType] ) extends TypeDef object Interface { + object RequiresType extends Enumeration { + type RequiresType = Value + val Eq, Ord = Value + } case class Method( ident: Ident, params: Seq[Field], diff --git a/src/main/scala/djinni/parser.scala b/src/main/scala/djinni/parser.scala index d7fc5449..f1a2cac3 100644 --- a/src/main/scala/djinni/parser.scala +++ b/src/main/scala/djinni/parser.scala @@ -16,6 +16,7 @@ package djinni import djinni.ast.Interface.Method +import djinni.ast.Interface.RequiresType.RequiresType import djinni.ast.Record.DerivingType.DerivingType import djinni.ast._ import djinni.syntax._ @@ -189,11 +190,12 @@ case class Parser(includePaths: List[String]) { def interfaceHeader: Parser[Ext] = "interface" ~> extInterface def interface: Parser[Interface] = - interfaceHeader ~ bracesList(method | const) ^^ { - case ext ~ items => { + interfaceHeader ~ bracesList(method | const) ~ opt(requires) ^^ { + case ext ~ items ~ requires => { val methods = items collect { case m: Method => m } val consts = items collect { case c: Const => c } - Interface(ext, methods, consts) + val requiresTypes = requires.getOrElse(Set[RequiresType]()) + Interface(ext, methods, consts, requiresTypes) } } @@ -209,9 +211,10 @@ case class Parser(includePaths: List[String]) { case ext ~ deriving => Record(ext, List(), List(), deriving.getOrElse(Set[DerivingType]())) } - def externInterface: Parser[Interface] = interfaceHeader ^^ { case ext => - Interface(ext, List(), List()) - } + def externInterface: Parser[Interface] = + interfaceHeader ~ opt(requires) ^^ { case ext ~ requires => + Interface(ext, List(), List(), requires.getOrElse(Set[RequiresType]())) + } def staticLabel: Parser[Boolean] = ("static ".r | "".r) ^^ { case "static " => true @@ -230,6 +233,18 @@ case class Parser(includePaths: List[String]) { } def ret: Parser[TypeRef] = ":" ~> typeRef + def requires: Parser[Set[RequiresType]] = + "requires" ~> parens(rep1sepend(ident, ",")) ^^ { + _.map(ident => + ident.name match { + case "eq" => Interface.RequiresType.Eq + case "ord" => Interface.RequiresType.Ord + case _ => + return err(s"""Unrecognized requires type "${ident.name}"""") + } + ).toSet + } + def boolValue: Parser[Boolean] = "([Tt]rue)|([Ff]alse)".r ^^ { s: String => s.toBoolean }