diff --git a/build.sbt b/build.sbt index d34bf02..96c5011 100644 --- a/build.sbt +++ b/build.sbt @@ -3,6 +3,7 @@ import sbtassembly.AssemblyPlugin.defaultUniversalScript ThisBuild / scalaVersion := "2.13.6" ThisBuild / organization := "com.github.cross-language-cpp" +val binExt = if (System.getProperty("os.name").startsWith("Windows")) ".bat" else "" lazy val djinni = (project in file(".")) .configs(IntegrationTest) .settings( @@ -14,7 +15,7 @@ lazy val djinni = (project in file(".")) libraryDependencies += "org.yaml" % "snakeyaml" % "1.29", libraryDependencies += "com.github.scopt" %% "scopt" % "4.0.1", libraryDependencies += "commons-io" % "commons-io" % "2.11.0", - assembly / assemblyOutputPath := { file("target/bin") / (assembly / assemblyJarName).value }, + assembly / assemblyOutputPath := { file("target/bin") / s"${(assembly / assemblyJarName).value}${binExt}" }, assembly / assemblyJarName := s"${name.value}", assembly / assemblyOption := (assembly / assemblyOption).value.copy(prependShellScript = Some(defaultUniversalScript(shebang = false))), assembly / test := {} diff --git a/docs/developer-guide.md b/docs/developer-guide.md index 0a2f829..72d3cd1 100644 --- a/docs/developer-guide.md +++ b/docs/developer-guide.md @@ -71,8 +71,6 @@ You can run the jar like this: ./djinni --help ``` -On Windows the file must be renamed to `djinni.bat` to make it executable. - !!! attention The resulting binary still requires Java to be able to run! [Details on how the self-executing jar works](https://github.com/sbt/sbt-assembly#prepending-a-launch-script). diff --git a/src/it/resources/cppcli_circular_dependent_interface.djinni b/src/it/resources/cppcli_circular_dependent_interface.djinni new file mode 100644 index 0000000..75bc92b --- /dev/null +++ b/src/it/resources/cppcli_circular_dependent_interface.djinni @@ -0,0 +1,13 @@ +one_interface = interface +c { + method_taking_another_interface(dep: another_interface); + method_taking_optional_another_interface(dep: optional); + method_returning_another_interface(): another_interface; + method_returning_optional_another_interface(): optional; +} + +another_interface = interface +c { + method_taking_one_interface(dep: one_interface); + method_taking_optional_one_interface(dep: optional); + method_returning_one_interface(): one_interface; + method_returning_optional_one_interface(): optional; +} diff --git a/src/it/resources/cppcli_extern_dependent_interface.djinni b/src/it/resources/cppcli_extern_dependent_interface.djinni new file mode 100644 index 0000000..06ac267 --- /dev/null +++ b/src/it/resources/cppcli_extern_dependent_interface.djinni @@ -0,0 +1,8 @@ +@extern "cpp_interface_dependency.yml" + +dependent_interface = interface +c { + method_taking_interface_dependency(dep: interface_dependency); + method_taking_optional_interface_dependency(dep: optional); + method_returning_interface_dependency(): interface_dependency; + method_returning_optional_interface_dependency(): optional; +} diff --git a/src/it/resources/cppcli_interface_nonnull.djinni b/src/it/resources/cppcli_interface_nonnull.djinni new file mode 100644 index 0000000..6b1d31f --- /dev/null +++ b/src/it/resources/cppcli_interface_nonnull.djinni @@ -0,0 +1,11 @@ +# interface comment +my_cpp_interface = interface +c { + # method comment + method_returning_nothing(value: i32); + method_returning_some_type(key: string): i32; + const method_changing_nothing(): i32; + static get_version(): i32; + + # Interfaces can also have constants + const version: i32 = 1; +} diff --git a/src/it/resources/expected/cppcli_circular_dependent_interface/cppcli/AnotherInterface.cpp b/src/it/resources/expected/cppcli_circular_dependent_interface/cppcli/AnotherInterface.cpp new file mode 100644 index 0000000..d241b68 --- /dev/null +++ b/src/it/resources/expected/cppcli_circular_dependent_interface/cppcli/AnotherInterface.cpp @@ -0,0 +1,66 @@ +// AUTOGENERATED FILE - DO NOT MODIFY! +// This file was generated by Djinni from cppcli_circular_dependent_interface.djinni + +#include "AnotherInterface.hpp" // my header +#include "djinni/cppcli/Error.hpp" +#include "djinni/cppcli/Marshal.hpp" +#include "djinni/cppcli/WrapperCache.hpp" +#include "one_interface.hpp" + +ref class AnotherInterfaceCppProxy : public AnotherInterface { + using CppType = std::shared_ptr<::AnotherInterface>; + using HandleType = ::djinni::CppProxyCache::Handle; +public: + AnotherInterfaceCppProxy(const CppType& cppRef) : _cppRefHandle(new HandleType(cppRef)) {} + + void MethodTakingOneInterface(OneInterface^ dep) override { + try { + _cppRefHandle->get()->method_taking_one_interface(::OneInterface::ToCpp(dep)); + } DJINNI_TRANSLATE_EXCEPTIONS() + } + + void MethodTakingOptionalOneInterface(OneInterface^ dep) override { + try { + _cppRefHandle->get()->method_taking_optional_one_interface(::djinni::Optional::ToCpp(dep)); + } DJINNI_TRANSLATE_EXCEPTIONS() + } + + OneInterface^ MethodReturningOneInterface() override { + try { + auto cs_result = _cppRefHandle->get()->method_returning_one_interface(); + return ::OneInterface::FromCpp(cs_result); + } DJINNI_TRANSLATE_EXCEPTIONS() + return nullptr; // Unreachable! (Silencing compiler warnings.) + } + + OneInterface^ MethodReturningOptionalOneInterface() override { + try { + auto cs_result = _cppRefHandle->get()->method_returning_optional_one_interface(); + return ::djinni::Optional::FromCpp(cs_result); + } DJINNI_TRANSLATE_EXCEPTIONS() + return nullptr; // Unreachable! (Silencing compiler warnings.) + } + + CppType djinni_private_get_proxied_cpp_object() { + return _cppRefHandle->get(); + } + +private: + AutoPtr _cppRefHandle; +}; + +AnotherInterface::CppType AnotherInterface::ToCpp(AnotherInterface::CsType cs) +{ + if (!cs) { + return nullptr; + } + return dynamic_cast(cs)->djinni_private_get_proxied_cpp_object(); +} + +AnotherInterface::CsType AnotherInterface::FromCppOpt(const AnotherInterface::CppOptType& cpp) +{ + if (!cpp) { + return nullptr; + } + return ::djinni::get_cpp_proxy(cpp); +} diff --git a/src/it/resources/expected/cppcli_circular_dependent_interface/cppcli/AnotherInterface.hpp b/src/it/resources/expected/cppcli_circular_dependent_interface/cppcli/AnotherInterface.hpp new file mode 100644 index 0000000..25607ef --- /dev/null +++ b/src/it/resources/expected/cppcli_circular_dependent_interface/cppcli/AnotherInterface.hpp @@ -0,0 +1,30 @@ +// AUTOGENERATED FILE - DO NOT MODIFY! +// This file was generated by Djinni from cppcli_circular_dependent_interface.djinni + +#pragma once + +#include "../cpp-headers/another_interface.hpp" +#include "OneInterface.hpp" +#include + +ref class OneInterface; + +public ref class AnotherInterface abstract { +public: + virtual void MethodTakingOneInterface(OneInterface^ dep) abstract; + + virtual void MethodTakingOptionalOneInterface(OneInterface^ dep) abstract; + + virtual OneInterface^ MethodReturningOneInterface() abstract; + + virtual OneInterface^ MethodReturningOptionalOneInterface() abstract; + +internal: + using CppType = std::shared_ptr<::AnotherInterface>; + using CppOptType = std::shared_ptr<::AnotherInterface>; + using CsType = AnotherInterface^; + + static CppType ToCpp(CsType cs); + static CsType FromCppOpt(const CppOptType& cpp); + static CsType FromCpp(const CppType& cpp) { return FromCppOpt(cpp); } +}; diff --git a/src/it/resources/expected/cppcli_circular_dependent_interface/cppcli/OneInterface.cpp b/src/it/resources/expected/cppcli_circular_dependent_interface/cppcli/OneInterface.cpp new file mode 100644 index 0000000..b3c3d53 --- /dev/null +++ b/src/it/resources/expected/cppcli_circular_dependent_interface/cppcli/OneInterface.cpp @@ -0,0 +1,66 @@ +// AUTOGENERATED FILE - DO NOT MODIFY! +// This file was generated by Djinni from cppcli_circular_dependent_interface.djinni + +#include "OneInterface.hpp" // my header +#include "another_interface.hpp" +#include "djinni/cppcli/Error.hpp" +#include "djinni/cppcli/Marshal.hpp" +#include "djinni/cppcli/WrapperCache.hpp" + +ref class OneInterfaceCppProxy : public OneInterface { + using CppType = std::shared_ptr<::OneInterface>; + using HandleType = ::djinni::CppProxyCache::Handle; +public: + OneInterfaceCppProxy(const CppType& cppRef) : _cppRefHandle(new HandleType(cppRef)) {} + + void MethodTakingAnotherInterface(AnotherInterface^ dep) override { + try { + _cppRefHandle->get()->method_taking_another_interface(::AnotherInterface::ToCpp(dep)); + } DJINNI_TRANSLATE_EXCEPTIONS() + } + + void MethodTakingOptionalAnotherInterface(AnotherInterface^ dep) override { + try { + _cppRefHandle->get()->method_taking_optional_another_interface(::djinni::Optional::ToCpp(dep)); + } DJINNI_TRANSLATE_EXCEPTIONS() + } + + AnotherInterface^ MethodReturningAnotherInterface() override { + try { + auto cs_result = _cppRefHandle->get()->method_returning_another_interface(); + return ::AnotherInterface::FromCpp(cs_result); + } DJINNI_TRANSLATE_EXCEPTIONS() + return nullptr; // Unreachable! (Silencing compiler warnings.) + } + + AnotherInterface^ MethodReturningOptionalAnotherInterface() override { + try { + auto cs_result = _cppRefHandle->get()->method_returning_optional_another_interface(); + return ::djinni::Optional::FromCpp(cs_result); + } DJINNI_TRANSLATE_EXCEPTIONS() + return nullptr; // Unreachable! (Silencing compiler warnings.) + } + + CppType djinni_private_get_proxied_cpp_object() { + return _cppRefHandle->get(); + } + +private: + AutoPtr _cppRefHandle; +}; + +OneInterface::CppType OneInterface::ToCpp(OneInterface::CsType cs) +{ + if (!cs) { + return nullptr; + } + return dynamic_cast(cs)->djinni_private_get_proxied_cpp_object(); +} + +OneInterface::CsType OneInterface::FromCppOpt(const OneInterface::CppOptType& cpp) +{ + if (!cpp) { + return nullptr; + } + return ::djinni::get_cpp_proxy(cpp); +} diff --git a/src/it/resources/expected/cppcli_circular_dependent_interface/cppcli/OneInterface.hpp b/src/it/resources/expected/cppcli_circular_dependent_interface/cppcli/OneInterface.hpp new file mode 100644 index 0000000..a8b62bd --- /dev/null +++ b/src/it/resources/expected/cppcli_circular_dependent_interface/cppcli/OneInterface.hpp @@ -0,0 +1,30 @@ +// AUTOGENERATED FILE - DO NOT MODIFY! +// This file was generated by Djinni from cppcli_circular_dependent_interface.djinni + +#pragma once + +#include "../cpp-headers/one_interface.hpp" +#include "AnotherInterface.hpp" +#include + +ref class AnotherInterface; + +public ref class OneInterface abstract { +public: + virtual void MethodTakingAnotherInterface(AnotherInterface^ dep) abstract; + + virtual void MethodTakingOptionalAnotherInterface(AnotherInterface^ dep) abstract; + + virtual AnotherInterface^ MethodReturningAnotherInterface() abstract; + + virtual AnotherInterface^ MethodReturningOptionalAnotherInterface() abstract; + +internal: + using CppType = std::shared_ptr<::OneInterface>; + using CppOptType = std::shared_ptr<::OneInterface>; + using CsType = OneInterface^; + + static CppType ToCpp(CsType cs); + static CsType FromCppOpt(const CppOptType& cpp); + static CsType FromCpp(const CppType& cpp) { return FromCppOpt(cpp); } +}; diff --git a/src/it/resources/expected/cppcli_circular_dependent_interface/generated-files.txt b/src/it/resources/expected/cppcli_circular_dependent_interface/generated-files.txt new file mode 100644 index 0000000..55b5ce2 --- /dev/null +++ b/src/it/resources/expected/cppcli_circular_dependent_interface/generated-files.txt @@ -0,0 +1,4 @@ +src/it/resources/result/cppcli_circular_dependent_interface/cppcli/OneInterface.hpp +src/it/resources/result/cppcli_circular_dependent_interface/cppcli/OneInterface.cpp +src/it/resources/result/cppcli_circular_dependent_interface/cppcli/AnotherInterface.hpp +src/it/resources/result/cppcli_circular_dependent_interface/cppcli/AnotherInterface.cpp diff --git a/src/it/resources/expected/cppcli_extern_dependent_interface/cppcli/DependentInterface.cpp b/src/it/resources/expected/cppcli_extern_dependent_interface/cppcli/DependentInterface.cpp new file mode 100644 index 0000000..ca3c50b --- /dev/null +++ b/src/it/resources/expected/cppcli_extern_dependent_interface/cppcli/DependentInterface.cpp @@ -0,0 +1,65 @@ +// AUTOGENERATED FILE - DO NOT MODIFY! +// This file was generated by Djinni from cppcli_extern_dependent_interface.djinni + +#include "DependentInterface.hpp" // my header +#include "djinni/cppcli/Error.hpp" +#include "djinni/cppcli/Marshal.hpp" +#include "djinni/cppcli/WrapperCache.hpp" + +ref class DependentInterfaceCppProxy : public DependentInterface { + using CppType = std::shared_ptr<::DependentInterface>; + using HandleType = ::djinni::CppProxyCache::Handle; +public: + DependentInterfaceCppProxy(const CppType& cppRef) : _cppRefHandle(new HandleType(cppRef)) {} + + void MethodTakingInterfaceDependency(InterfaceDependency^ dep) override { + try { + _cppRefHandle->get()->method_taking_interface_dependency(::InterfaceDependency::ToCpp(dep)); + } DJINNI_TRANSLATE_EXCEPTIONS() + } + + void MethodTakingOptionalInterfaceDependency(InterfaceDependency^ dep) override { + try { + _cppRefHandle->get()->method_taking_optional_interface_dependency(::djinni::Optional::ToCpp(dep)); + } DJINNI_TRANSLATE_EXCEPTIONS() + } + + InterfaceDependency^ MethodReturningInterfaceDependency() override { + try { + auto cs_result = _cppRefHandle->get()->method_returning_interface_dependency(); + return ::InterfaceDependency::FromCpp(cs_result); + } DJINNI_TRANSLATE_EXCEPTIONS() + return nullptr; // Unreachable! (Silencing compiler warnings.) + } + + InterfaceDependency^ MethodReturningOptionalInterfaceDependency() override { + try { + auto cs_result = _cppRefHandle->get()->method_returning_optional_interface_dependency(); + return ::djinni::Optional::FromCpp(cs_result); + } DJINNI_TRANSLATE_EXCEPTIONS() + return nullptr; // Unreachable! (Silencing compiler warnings.) + } + + CppType djinni_private_get_proxied_cpp_object() { + return _cppRefHandle->get(); + } + +private: + AutoPtr _cppRefHandle; +}; + +DependentInterface::CppType DependentInterface::ToCpp(DependentInterface::CsType cs) +{ + if (!cs) { + return nullptr; + } + return dynamic_cast(cs)->djinni_private_get_proxied_cpp_object(); +} + +DependentInterface::CsType DependentInterface::FromCppOpt(const DependentInterface::CppOptType& cpp) +{ + if (!cpp) { + return nullptr; + } + return ::djinni::get_cpp_proxy(cpp); +} diff --git a/src/it/resources/expected/cppcli_extern_dependent_interface/cppcli/DependentInterface.hpp b/src/it/resources/expected/cppcli_extern_dependent_interface/cppcli/DependentInterface.hpp new file mode 100644 index 0000000..163f844 --- /dev/null +++ b/src/it/resources/expected/cppcli_extern_dependent_interface/cppcli/DependentInterface.hpp @@ -0,0 +1,28 @@ +// AUTOGENERATED FILE - DO NOT MODIFY! +// This file was generated by Djinni from cppcli_extern_dependent_interface.djinni + +#pragma once + +#include "../cpp-headers/dependent_interface.hpp" +#include "InterfaceDependency.hpp" +#include + +public ref class DependentInterface abstract { +public: + virtual void MethodTakingInterfaceDependency(InterfaceDependency^ dep) abstract; + + virtual void MethodTakingOptionalInterfaceDependency(InterfaceDependency^ dep) abstract; + + virtual InterfaceDependency^ MethodReturningInterfaceDependency() abstract; + + virtual InterfaceDependency^ MethodReturningOptionalInterfaceDependency() abstract; + +internal: + using CppType = std::shared_ptr<::DependentInterface>; + using CppOptType = std::shared_ptr<::DependentInterface>; + using CsType = DependentInterface^; + + static CppType ToCpp(CsType cs); + static CsType FromCppOpt(const CppOptType& cpp); + static CsType FromCpp(const CppType& cpp) { return FromCppOpt(cpp); } +}; diff --git a/src/it/resources/expected/cppcli_extern_dependent_interface/generated-files.txt b/src/it/resources/expected/cppcli_extern_dependent_interface/generated-files.txt new file mode 100644 index 0000000..28db92c --- /dev/null +++ b/src/it/resources/expected/cppcli_extern_dependent_interface/generated-files.txt @@ -0,0 +1,2 @@ +src/it/resources/result/cppcli_extern_dependent_interface/cppcli/DependentInterface.hpp +src/it/resources/result/cppcli_extern_dependent_interface/cppcli/DependentInterface.cpp diff --git a/src/it/resources/expected/cppcli_interface_nonnull/cppcli/MyCppInterface.cpp b/src/it/resources/expected/cppcli_interface_nonnull/cppcli/MyCppInterface.cpp new file mode 100644 index 0000000..877ab62 --- /dev/null +++ b/src/it/resources/expected/cppcli_interface_nonnull/cppcli/MyCppInterface.cpp @@ -0,0 +1,67 @@ +// AUTOGENERATED FILE - DO NOT MODIFY! +// This file was generated by Djinni from cppcli_interface_nonnull.djinni + +#include "MyCppInterface.hpp" // my header +#include "djinni/cppcli/Error.hpp" +#include "djinni/cppcli/Marshal.hpp" +#include "djinni/cppcli/WrapperCache.hpp" + +int MyCppInterface::GetVersion() { + try { + auto cs_result = ::MyCppInterface::get_version(); + return ::djinni::I32::FromCpp(cs_result); + } DJINNI_TRANSLATE_EXCEPTIONS() + return 0; // Unreachable! (Silencing compiler warnings.) +} + +ref class MyCppInterfaceCppProxy : public MyCppInterface { + using CppType = std::shared_ptr<::MyCppInterface>; + using HandleType = ::djinni::CppProxyCache::Handle; +public: + MyCppInterfaceCppProxy(const CppType& cppRef) : _cppRefHandle(new HandleType(cppRef)) {} + + void MethodReturningNothing(int value) override { + try { + _cppRefHandle->get()->method_returning_nothing(::djinni::I32::ToCpp(value)); + } DJINNI_TRANSLATE_EXCEPTIONS() + } + + int MethodReturningSomeType(System::String^ key) override { + try { + auto cs_result = _cppRefHandle->get()->method_returning_some_type(::djinni::String::ToCpp(key)); + return ::djinni::I32::FromCpp(cs_result); + } DJINNI_TRANSLATE_EXCEPTIONS() + return 0; // Unreachable! (Silencing compiler warnings.) + } + + int MethodChangingNothing() override { + try { + auto cs_result = _cppRefHandle->get()->method_changing_nothing(); + return ::djinni::I32::FromCpp(cs_result); + } DJINNI_TRANSLATE_EXCEPTIONS() + return 0; // Unreachable! (Silencing compiler warnings.) + } + + CppType djinni_private_get_proxied_cpp_object() { + return _cppRefHandle->get(); + } + +private: + AutoPtr _cppRefHandle; +}; + +MyCppInterface::CppType MyCppInterface::ToCpp(MyCppInterface::CsType cs) +{ + if (!cs) { + throw gcnew System::Exception("MyCppInterface::ToCpp requires non-nil object."); + } + return NN_CHECK_ASSERT(dynamic_cast(cs)->djinni_private_get_proxied_cpp_object()); +} + +MyCppInterface::CsType MyCppInterface::FromCppOpt(const MyCppInterface::CppOptType& cpp) +{ + if (!cpp) { + return nullptr; + } + return ::djinni::get_cpp_proxy(cpp); +} diff --git a/src/it/resources/expected/cppcli_interface_nonnull/cppcli/MyCppInterface.hpp b/src/it/resources/expected/cppcli_interface_nonnull/cppcli/MyCppInterface.hpp new file mode 100644 index 0000000..da17721 --- /dev/null +++ b/src/it/resources/expected/cppcli_interface_nonnull/cppcli/MyCppInterface.hpp @@ -0,0 +1,30 @@ +// AUTOGENERATED FILE - DO NOT MODIFY! +// This file was generated by Djinni from cppcli_interface_nonnull.djinni + +#pragma once + +#include "../cpp-headers/my_cpp_interface.hpp" +#include "nn.hpp" +#include + +/** interface comment */ +public ref class MyCppInterface abstract { +public: + /** method comment */ + virtual void MethodReturningNothing(int value) abstract; + + virtual int MethodReturningSomeType(System::String^ key) abstract; + + virtual int MethodChangingNothing() abstract; + + static int GetVersion(); + +internal: + using CppType = dropbox::oxygen::nn_shared_ptr<::MyCppInterface>; + using CppOptType = std::shared_ptr<::MyCppInterface>; + using CsType = MyCppInterface^; + + static CppType ToCpp(CsType cs); + static CsType FromCppOpt(const CppOptType& cpp); + static CsType FromCpp(const CppType& cpp) { return FromCppOpt(cpp); } +}; diff --git a/src/it/resources/expected/cppcli_interface_nonnull/generated-files.txt b/src/it/resources/expected/cppcli_interface_nonnull/generated-files.txt new file mode 100644 index 0000000..cf02726 --- /dev/null +++ b/src/it/resources/expected/cppcli_interface_nonnull/generated-files.txt @@ -0,0 +1,2 @@ +src/it/resources/result/cppcli_interface_nonnull/cppcli/MyCppInterface.hpp +src/it/resources/result/cppcli_interface_nonnull/cppcli/MyCppInterface.cpp diff --git a/src/it/scala/djinni/GeneratorIntegrationTest.scala b/src/it/scala/djinni/GeneratorIntegrationTest.scala index c5b37e7..db08b05 100644 --- a/src/it/scala/djinni/GeneratorIntegrationTest.scala +++ b/src/it/scala/djinni/GeneratorIntegrationTest.scala @@ -5,6 +5,8 @@ import matchers.should.Matchers._ import org.scalatest.prop.TableDrivenPropertyChecks._ +import java.nio.file.Paths + class GeneratorIntegrationTest extends IntegrationTest with GivenWhenThen { describe("djinni file generation") { @@ -312,7 +314,12 @@ class GeneratorIntegrationTest extends IntegrationTest with GivenWhenThen { Then( "the file `generated-files.txt` should contain all generated files" ) - assertFileContentEquals(idlFile, "", List("generated-files.txt")) + assertFileContentEquals( + idlFile, + "", + List("generated-files.txt"), + s => Paths.get(s) + ) } } @@ -340,7 +347,82 @@ class GeneratorIntegrationTest extends IntegrationTest with GivenWhenThen { assertFileContentEquals(idlFile, CPP_HEADERS, cppHeaderFilenames) Then("the file `generated-files.txt` should contain all generated files") - assertFileContentEquals(idlFile, "", List("generated-files.txt")) + assertFileContentEquals( + idlFile, + "", + List("generated-files.txt"), + s => Paths.get(s) + ) + } + + it( + "should be able to generate C++/CLI outputs for interfaces with external dependencies" + ) { + val idlFile = "cppcli_extern_dependent_interface" + When(s"generating C++ language-bridges from `$idlFile.djinni`") + val cppcliFilenames = + CppCli("DependentInterface.hpp", "DependentInterface.cpp") + val cmd = djinniParams( + idlFile, + cpp = false, + objc = false, + java = false, + python = false, + cWrapper = false, + cppCLI = true, + useNNHeader = false + ) + djinni(cmd) + + Then( + s"the expected created C++/CLI files should be: ${cppcliFilenames.mkString(", ")}" + ) + assertFileContentEquals(idlFile, CPPCLI, cppcliFilenames) + + Then("the file `generated-files.txt` should contain all generated files") + assertFileContentEquals( + idlFile, + "", + List("generated-files.txt"), + s => Paths.get(s) + ) + } + + it( + "should be able to generate C++/CLI outputs for circular dependencies" + ) { + val idlFile = "cppcli_circular_dependent_interface" + When(s"generating C++ language-bridges from `$idlFile.djinni`") + val cppcliFilenames = CppCli( + "OneInterface.hpp", + "OneInterface.cpp", + "AnotherInterface.hpp", + "AnotherInterface.cpp" + ) + val cmd = djinniParams( + idlFile, + cpp = false, + objc = false, + java = false, + python = false, + cWrapper = false, + cppCLI = true, + useNNHeader = false + ) + djinni(cmd) + + Then( + s"the expected created C++/CLI files should be: ${cppcliFilenames.mkString(", ")}" + ) + assertFileContentEquals(idlFile, CPPCLI, cppcliFilenames) + + Then("the file `generated-files.txt` should contain all generated files") + assertFileContentEquals( + idlFile, + "", + List("generated-files.txt"), + s => Paths.get(s) + ) } it("should be able to generate C++ records without a default constructor") { @@ -367,7 +449,47 @@ class GeneratorIntegrationTest extends IntegrationTest with GivenWhenThen { assertFileContentEquals(idlFile, CPP_HEADERS, cppHeaderFilenames) Then("the file `generated-files.txt` should contain all generated files") - assertFileContentEquals(idlFile, "", List("generated-files.txt")) + assertFileContentEquals( + idlFile, + "", + List("generated-files.txt"), + s => Paths.get(s) + ) + } + + it( + "should be able to generate C++/CLI outputs with non-null pointers" + ) { + val idlFile = "cppcli_interface_nonnull" + When(s"generating C++ language-bridges from `$idlFile.djinni`") + val cppcliFilenames = CppCli( + "MyCppInterface.hpp", + "MyCppInterface.cpp" + ) + val cmd = djinniParams( + idlFile, + cpp = false, + objc = false, + java = false, + python = false, + cWrapper = false, + cppCLI = true, + useNNHeader = true + ) + djinni(cmd) + + Then( + s"the expected created C++/CLI files should be: ${cppcliFilenames.mkString(", ")}" + ) + assertFileContentEquals(idlFile, CPPCLI, cppcliFilenames) + + Then("the file `generated-files.txt` should contain all generated files") + assertFileContentEquals( + idlFile, + "", + List("generated-files.txt"), + s => Paths.get(s) + ) } it("should be able to only generate Java output") { @@ -642,7 +764,12 @@ class GeneratorIntegrationTest extends IntegrationTest with GivenWhenThen { assertFileContentEquals(idlFile, CPP_HEADERS, cppHeaderFilenames) Then("the file `generated-files.txt` should contain all generated files") - assertFileContentEquals(idlFile, "", List("generated-files.txt")) + assertFileContentEquals( + idlFile, + "", + List("generated-files.txt"), + s => Paths.get(s) + ) } } diff --git a/src/it/scala/djinni/IntegrationTest.scala b/src/it/scala/djinni/IntegrationTest.scala index c7accc4..a8e45eb 100644 --- a/src/it/scala/djinni/IntegrationTest.scala +++ b/src/it/scala/djinni/IntegrationTest.scala @@ -10,6 +10,7 @@ import scala.sys.process._ import scala.reflect.io.Directory import java.io.File +import java.nio.file.Paths // Base class for integration tests, providing a few handy helper functions class IntegrationTest extends AnyFunSpec { @@ -64,7 +65,9 @@ class IntegrationTest extends AnyFunSpec { * command-line output of the executed djinni-cli */ def djinni(parameters: String): String = { - "target/bin/djinni " + parameters !! + val binExt = + if (System.getProperty("os.name").startsWith("Windows")) ".bat" else "" + toUnixLineSeparator(s"target/bin/djinni${binExt} " + parameters !!) } /** Generates the command line parameters to pass to the djinni generator. @@ -170,10 +173,27 @@ class IntegrationTest extends AnyFunSpec { return djinni(djinniParams(idl)) } + /** Transform the string generated on Windows to Linux format in order to + * compare it with the test data generated on Linux. + * @param str + * the string generated on Windows to be transformed + * @return + * the string that \r is removed and \\ is replaced by / from str + */ + def toUnixLineSeparator( + str: String + ): String = { + if (System.lineSeparator != "\n") { + str.replace(System.lineSeparator, "\n") + } else { + str + } + } + /** Asserts that all expected files have been created & have the expected * content. It basically compares the contents of the generator output in - * `resources/result/$lang` with the expectations defined in - * `resources/expected/$lang`. + * `resources/result/$idl/$lang/$filename` with the expectations defined in + * `resources/expected/$idl/$lang/$filename`. * @param idl * filename of the input-idl (without file extension) * @param lang @@ -182,18 +202,32 @@ class IntegrationTest extends AnyFunSpec { * @param filenames * list of expected filenames that should have been generated for the given * language + * @param lineTransformation + * function to transform a line, e.g. to transform the line to a Path type */ def assertFileContentEquals( idl: String, lang: String, - filenames: List[String] + filenames: List[String], + lineTransformation: String => Any = (s: String) => s ): Unit = { for (filename <- filenames) { val resultFile = Source.fromFile(s"src/it/resources/result/$idl/$lang/$filename") val expectationFile = Source.fromFile(s"src/it/resources/expected/$idl/$lang/$filename") - resultFile.mkString should equal(expectationFile.mkString) + val resultFileLines = resultFile.getLines + val expectationFileLines = expectationFile.getLines + for ( + (result, expectation) <- resultFileLines.zipAll( + expectationFileLines, + "", + "" + ) + ) { + lineTransformation(result) should equal(lineTransformation(expectation)) + } + resultFile.close() expectationFile.close() } diff --git a/src/main/scala/djinni/CppCliGenerator.scala b/src/main/scala/djinni/CppCliGenerator.scala index c352c93..c2ce456 100644 --- a/src/main/scala/djinni/CppCliGenerator.scala +++ b/src/main/scala/djinni/CppCliGenerator.scala @@ -41,16 +41,21 @@ class CppCliGenerator(spec: Spec) extends Generator(spec) { ) ) - def find(ty: TypeRef) { find(ty.resolved) } - def find(tm: MExpr) { - tm.args.foreach(find) - find(tm.base) + def find(ty: TypeRef, forwardDeclareOnly: Boolean) { + find(ty.resolved, forwardDeclareOnly) } - def find(m: Meta): Unit = for (r <- marshal.references(m, name)) addRefs(r) + def find(tm: MExpr, forwardDeclareOnly: Boolean) { + tm.args.foreach(x => find(x, forwardDeclareOnly)) + find(tm.base, forwardDeclareOnly) + } + def find(m: Meta, forwardDeclareOnly: Boolean): Unit = for ( + r <- marshal.references(m, name, forwardDeclareOnly) + ) addRefs(r) private def addRefs(r: SymbolReference) = r match { case ImportRef(arg) => hpp.add("#include " + arg) // TODO add to `cpp` - case DeclRef(_, _) => + case DeclRef(decl, Some(spec.cppCliNamespace)) => hppFwds.add(decl) + case DeclRef(_, _) => } } @@ -196,9 +201,9 @@ class CppCliGenerator(spec: Spec) extends Generator(spec) { r: Record ) { val refs = new CppCliRefs(ident.name) - refs.find(MString) // for: String^ ToString(); - r.fields.foreach(f => refs.find(f.ty)) - r.consts.foreach(c => refs.find(c.ty)) + refs.find(MString, false) // for: String^ ToString(); + r.fields.foreach(f => refs.find(f.ty, false)) + r.consts.foreach(c => refs.find(c.ty, false)) def call(f: Field) = { f.ty.resolved.base match { @@ -445,6 +450,9 @@ class CppCliGenerator(spec: Spec) extends Generator(spec) { ) } + def nnCheck(expr: String): String = + spec.cppNnCheckExpression.fold(expr)(check => s"$check($expr)") + override def generateInterface( origin: String, ident: Ident, @@ -454,13 +462,22 @@ class CppCliGenerator(spec: Spec) extends Generator(spec) { ) { val refs = new CppCliRefs(ident.name) i.methods.foreach(m => { - m.params.foreach(p => refs.find(p.ty)) - m.ret.foreach(x => refs.find(x)) + m.params.foreach(p => refs.find(p.ty, true)) + m.ret.foreach(x => refs.find(x, true)) + }) + i.consts.map(c => { + refs.find(c.ty, true) }) val self = marshal.typename(ident, i) val cppSelf = cppMarshal.fqTypename(ident, i) + spec.cppNnHeader match { + case Some(nnHdr) => + refs.hpp.add("#include " + q(nnHdr)) + case _ => + } + refs.hpp.add("#include ") val methodNamesInScope = i.methods.map(m => idCs.method(m.ident)) @@ -710,14 +727,21 @@ class CppCliGenerator(spec: Spec) extends Generator(spec) { val CppOptType = s"$self::CppOptType" val CsType = s"$self::CsType" w.wl(s"$CppType $self::ToCpp($CsType cs)").braced { + // Handle null w.w("if (!cs)").braced { - w.wl("return nullptr;") + if (spec.cppNnType.isEmpty) { + w.wl("return nullptr;") + } else { + w.wl( + s"""throw gcnew System::Exception("$self::ToCpp requires non-nil object.");""" + ) + } } if (i.ext.cpp && !i.ext.cppcli) { // C++ only. In this case we *must* unwrap a proxy object - the dynamic_cast will // throw bad_cast if we gave it something of the wrong type. w.wl( - s"return dynamic_cast<$cppProxySelf^>(cs)->djinni_private_get_proxied_cpp_object();" + s"return ${nnCheck(s"dynamic_cast<$cppProxySelf^>(cs)->djinni_private_get_proxied_cpp_object()")};" ) } else if (i.ext.cpp || i.ext.cppcli) { // C# only, or C# and C++. @@ -726,11 +750,13 @@ class CppCliGenerator(spec: Spec) extends Generator(spec) { w.wl(s"if (auto cs_ref = dynamic_cast<$cppProxySelf^>(cs))") .braced { w.wl( - "return cs_ref->djinni_private_get_proxied_cpp_object();" + s"return ${nnCheck("cs_ref->djinni_private_get_proxied_cpp_object()")};" ) } } - w.wl(s"return ::djinni::get_cs_proxy<$csProxySelf>(cs);") + w.wl( + s"return ${nnCheck(s"::djinni::get_cs_proxy<$csProxySelf>(cs)")};" + ) } else { // Neither C# nor C++. Unusable, but generate compilable code. w.wl( diff --git a/src/main/scala/djinni/CppCliMarshal.scala b/src/main/scala/djinni/CppCliMarshal.scala index 201e609..784d400 100644 --- a/src/main/scala/djinni/CppCliMarshal.scala +++ b/src/main/scala/djinni/CppCliMarshal.scala @@ -76,15 +76,40 @@ class CppCliMarshal(spec: Spec) extends Marshal(spec) { case e: Enum => false } - def references(m: Meta, exclude: String): Seq[SymbolReference] = m match { + def references( + m: Meta, + exclude: String, + forwardDeclareOnly: Boolean + ): Seq[SymbolReference] = m match { case d: MDef => - if (d.name != exclude) { - List(ImportRef(include(d.name))) - } else { - List() + d.body match { + case i: Interface => + if (d.name != exclude) { + if (forwardDeclareOnly) { + List( + ImportRef(include(d.name)), + // TODO: Only add reference if two classes reference each other + DeclRef( + s"ref class ${typename(d.name, d.body)};", + Some(spec.cppCliNamespace) + ) + ) + } else { + List(ImportRef(include(d.name))) + } + } else { + List() + } + case _ => + if (d.name != exclude) { + List(ImportRef(include(d.name))) + } else { + List() + } } - case e: MExtern => List(ImportRef(e.cs.header.get)) - case _ => List() + case e: MExtern => + List(ImportRef(e.cs.header.get)) + case _ => List() } private def toCppCliType( diff --git a/src/main/scala/djinni/CppMarshal.scala b/src/main/scala/djinni/CppMarshal.scala index 76e6704..a3adb0b 100644 --- a/src/main/scala/djinni/CppMarshal.scala +++ b/src/main/scala/djinni/CppMarshal.scala @@ -122,7 +122,7 @@ class CppMarshal(spec: Spec) extends Marshal(spec) { List(ImportRef("")) } spec.cppNnHeader match { - case Some(nnHdr) => ImportRef(nnHdr) :: base + case Some(nnHdr) => ImportRef(q(nnHdr)) :: base case _ => base } } diff --git a/src/main/scala/djinni/JNIGenerator.scala b/src/main/scala/djinni/JNIGenerator.scala index 0c7193c..f212c89 100644 --- a/src/main/scala/djinni/JNIGenerator.scala +++ b/src/main/scala/djinni/JNIGenerator.scala @@ -84,7 +84,7 @@ class JNIGenerator(spec: Spec) extends Generator(spec) { "#include " + q(jniMarshal.jniBaseLibIncludePrefix + "djinni_support.hpp") ) spec.cppNnHeader match { - case Some(nnHdr) => jniHpp.add("#include " + nnHdr) + case Some(nnHdr) => jniHpp.add("#include " + q(nnHdr)) case _ => } diff --git a/src/main/scala/djinni/ObjcppGenerator.scala b/src/main/scala/djinni/ObjcppGenerator.scala index a6aeb7f..0c6c210 100644 --- a/src/main/scala/djinni/ObjcppGenerator.scala +++ b/src/main/scala/djinni/ObjcppGenerator.scala @@ -113,7 +113,7 @@ class ObjcppGenerator(spec: Spec) extends BaseObjcGenerator(spec) { ) spec.cppNnHeader match { - case Some(nnHdr) => refs.privHeader.add("#include " + nnHdr) + case Some(nnHdr) => refs.privHeader.add("#include " + q(nnHdr)) case _ => }