From 35fdcacf9160fa24437b822d49131173918591a4 Mon Sep 17 00:00:00 2001 From: jothepro Date: Sun, 25 Apr 2021 21:02:04 +0200 Subject: [PATCH] Csharp support (#25) * migrate cppcli support from https://github.com/iit-reacts/djinni/tree/iit * migrate csharp unit tests to be configured by cmake entirely * rename `--cpp-optional-header` from `` to `"optional"` because otherwise the command is broken on powershell * replace file(READ ...) with file(STRINGS ...) in Djinni.cmake because it is shorter * fix missing header that the MSVC compiler complained about. Co-authored-by: Leandro Freitas Co-authored-by: Harald --- .github/workflows/build.yml | 29 ++ .gitignore | 3 + CMakeLists.txt | 54 +++- djinni/cppcli/Assert.hpp | 23 ++ djinni/cppcli/AutoPtr.hpp | 70 +++++ djinni/cppcli/Error.cpp | 33 +++ djinni/cppcli/Error.hpp | 38 +++ djinni/cppcli/Marshal.hpp | 277 ++++++++++++++++++ djinni/cppcli/WrapperCache.cpp | 41 +++ djinni/cppcli/WrapperCache.hpp | 139 +++++++++ djinni/proxy_cache_impl.hpp | 90 +++++- docs/developer-guide.md | 19 ++ test-suite/CMakeLists.txt | 64 +++- test-suite/Djinni.cmake | 59 +++- test-suite/djinni/client_interface.djinni | 2 +- .../djinni/single_language_interfaces.djinni | 5 +- test-suite/djinni/user_token.djinni | 2 +- .../djinni/vendor/third-party/date.yaml | 6 +- .../djinni/vendor/third-party/duration.yaml | 42 +-- .../handwritten-src/cpp/Duration-cs.hpp | 37 +++ .../handwritten-src/cpp/test_helpers.cpp | 1 + .../handwritten-src/cs/ClientInterfaceImpl.cs | 32 ++ .../handwritten-src/cs/ClientInterfaceTest.cs | 44 +++ .../handwritten-src/cs/CppExceptionTest.cs | 23 ++ test-suite/handwritten-src/cs/DurationTest.cs | 41 +++ test-suite/handwritten-src/cs/EnumTest.cs | 63 ++++ .../handwritten-src/cs/MapRecordTest.cs | 69 +++++ .../handwritten-src/cs/MockRecordTest.cs | 25 ++ .../cs/NestedCollectionTest.cs | 35 +++ .../handwritten-src/cs/PrimitiveListTest.cs | 38 +++ .../handwritten-src/cs/PrimitivesTest.cs | 17 ++ .../cs/RecordWithDerivingsTest.cs | 74 +++++ .../handwritten-src/cs/SetRecordTest.cs | 27 ++ test-suite/handwritten-src/cs/TokenTest.cs | 53 ++++ test-suite/handwritten-src/cs/WcharTest.cs | 21 ++ test/cppcli_list.txt | 13 + 36 files changed, 1548 insertions(+), 61 deletions(-) create mode 100644 djinni/cppcli/Assert.hpp create mode 100644 djinni/cppcli/AutoPtr.hpp create mode 100644 djinni/cppcli/Error.cpp create mode 100644 djinni/cppcli/Error.hpp create mode 100644 djinni/cppcli/Marshal.hpp create mode 100644 djinni/cppcli/WrapperCache.cpp create mode 100644 djinni/cppcli/WrapperCache.hpp create mode 100644 test-suite/handwritten-src/cpp/Duration-cs.hpp create mode 100644 test-suite/handwritten-src/cs/ClientInterfaceImpl.cs create mode 100644 test-suite/handwritten-src/cs/ClientInterfaceTest.cs create mode 100644 test-suite/handwritten-src/cs/CppExceptionTest.cs create mode 100644 test-suite/handwritten-src/cs/DurationTest.cs create mode 100644 test-suite/handwritten-src/cs/EnumTest.cs create mode 100644 test-suite/handwritten-src/cs/MapRecordTest.cs create mode 100644 test-suite/handwritten-src/cs/MockRecordTest.cs create mode 100644 test-suite/handwritten-src/cs/NestedCollectionTest.cs create mode 100644 test-suite/handwritten-src/cs/PrimitiveListTest.cs create mode 100644 test-suite/handwritten-src/cs/PrimitivesTest.cs create mode 100644 test-suite/handwritten-src/cs/RecordWithDerivingsTest.cs create mode 100644 test-suite/handwritten-src/cs/SetRecordTest.cs create mode 100644 test-suite/handwritten-src/cs/TokenTest.cs create mode 100644 test-suite/handwritten-src/cs/WcharTest.cs create mode 100644 test/cppcli_list.txt diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 23d34a4..031c092 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -66,3 +66,32 @@ jobs: - name: Test if expected files have been installed working-directory: build/check_install_root run: diff -u ../../test/jni_list.txt <(du -a | tac | awk -F ' ' '{print $2}' | sort) + + build-on-windows-for-cppcli: + runs-on: windows-latest + steps: + - uses: actions/checkout@v2 + - name: install djinni + run: | + $VERSION = 'v' + [regex]::Match((Get-Content .tool-versions), '^djinni (\d+.\d+.\d+)').captures.groups[1].value + $URL = 'https://github.com/cross-language-cpp/djinni-generator/releases/download/' + $VERSION + '/djinni.bat' + Invoke-WebRequest -Uri $URL -OutFile djinni.bat + - name: Configure cmake + run: cmake -S . -B build -DDJINNI_WITH_CPPCLI=ON -DDJINNI_STATIC_LIB=ON -DCMAKE_INSTALL_PREFIX=build/check_install_root -DDJINNI_EXECUTABLE="$(((Get-Location).Path) -replace "\\","/")/djinni.bat" -G "Visual Studio 16 2019" + - name: Install nuget dependencies + working-directory: build/test-suite + run: dotnet restore DjinniCppCliTest.csproj --runtime win-x64 + - name: Build release + run: cmake --build build --config Release + - name: Run tests + working-directory: build/test-suite + run: dotnet test Release/DjinniCppCliTest.dll + - name: Install files + run: cmake --build build --config Release --target install + - name: List installed files + working-directory: build/check_install_root + run: Resolve-Path -Path (Get-ChildItem -Recurse).FullName -Relative + - name: Test if expected files have been installed + working-directory: build/check_install_root + run: if((Compare-Object (Get-Content ..\..\test\cppcli_list.txt) (Resolve-Path -Path (Get-ChildItem -Recurse).FullName -Relative))) { Write-Error "file list not equal" } + diff --git a/.gitignore b/.gitignore index 8cbb67c..d664638 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ build/* .DS_Store *.pyc +.idea/ +cmake-build-debug/ +check_install_root/ diff --git a/CMakeLists.txt b/CMakeLists.txt index 12b63a9..abe693a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,6 +1,12 @@ cmake_minimum_required(VERSION 3.6.0) -project(djinni_support_lib) +if(DJINNI_WITH_OBJC) + set(PROJECT_LANGUAGES OBJC OBJCXX) +elseif(DJINNI_WITH_CPPCLI) + set(PROJECT_LANGUAGES CSharp) +endif() + +project(djinni_support_lib CXX ${PROJECT_LANGUAGES}) include(GNUInstallDirs) @@ -33,6 +39,16 @@ set(SRC_C_WRAPPER "djinni/cwrapper/wrapper_marshal.hpp" ) +set(SRC_CPPCLI + "djinni/cppcli/Assert.hpp" + "djinni/cppcli/AutoPtr.hpp" + "djinni/cppcli/Error.hpp" + "djinni/cppcli/Error.cpp" + "djinni/cppcli/Marshal.hpp" + "djinni/cppcli/WrapperCache.hpp" + "djinni/cppcli/WrapperCache.cpp" +) + option(DJINNI_STATIC_LIB "Build Djinni support library as a static library instead of dynamic (the default)." off) if(DJINNI_STATIC_LIB) add_library(djinni_support_lib STATIC ${SRC_SHARED}) @@ -89,6 +105,8 @@ if(DJINNI_WITH_OBJC) endif() +target_include_directories(djinni_support_lib PUBLIC "$") + # JNI support option(DJINNI_WITH_JNI "Include the JNI support code in Djinni support library." off) option(DJINNI_JNI_WITH_MAIN "Include the default minimal JNI_OnLoad and JNI_OnUnload implementation in Djinni support library." on) @@ -130,9 +148,39 @@ if(DJINNI_WITH_PYTHON) ) endif() +option(DJINNI_WITH_CPPCLI "Include the C++/CLI support code in Djinni support library." off) +if(DJINNI_WITH_CPPCLI) + + if(NOT MSVC) + message(FATAL_ERROR "Enabling DJINNI_WITH_CPPCLI without MSVC is not supported") + endif() + + if(NOT DJINNI_STATIC_LIB) + message(FATAL_ERROR "DJINNI_WITH_CPPCLI requires DJINNI_STATIC_LIB to be true") + endif() + + target_include_directories(djinni_support_lib PUBLIC "$") + target_sources(djinni_support_lib PRIVATE ${SRC_CPPCLI}) + source_group("cppcli" FILES ${SRC_CPPCLI}) + set_target_properties(djinni_support_lib PROPERTIES + VS_DOTNET_REFERENCES "System;System.Core" + COMMON_LANGUAGE_RUNTIME "" + ) + + install( + FILES + "djinni/cppcli/Assert.hpp" + "djinni/cppcli/AutoPtr.hpp" + "djinni/cppcli/Error.hpp" + "djinni/cppcli/Marshal.hpp" + "djinni/cppcli/WrapperCache.hpp" + DESTINATION + ${CMAKE_INSTALL_INCLUDEDIR}/djinni/cppcli + ) +endif() -if(NOT (DJINNI_WITH_OBJC OR DJINNI_WITH_JNI OR DJINNI_WITH_PYTHON)) - message(FATAL_ERROR "At least one of DJINNI_WITH_OBJC or DJINNI_WITH_JNI or DJINNI_WITH_PYTHON must be enabled.") +if(NOT (DJINNI_WITH_OBJC OR DJINNI_WITH_JNI OR DJINNI_WITH_PYTHON OR DJINNI_WITH_CPPCLI)) + message(FATAL_ERROR "At least one of DJINNI_WITH_OBJC or DJINNI_WITH_JNI or DJINNI_WITH_PYTHON or DJINNI_WITH_CPPCLI must be enabled.") endif() diff --git a/djinni/cppcli/Assert.hpp b/djinni/cppcli/Assert.hpp new file mode 100644 index 0000000..eeb7a8c --- /dev/null +++ b/djinni/cppcli/Assert.hpp @@ -0,0 +1,23 @@ +// +// Copyright 2021 cross-language-cpp +// +// 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. +// + +#pragma once + +#ifdef _DEBUG +#define ASSERT(condition, ...) ::System::Diagnostics::Debug::Assert(condition, ##__VA_ARGS__) +#else +#define ASSERT(condition, ...) // This macro will completely evaporate in Release. +#endif diff --git a/djinni/cppcli/AutoPtr.hpp b/djinni/cppcli/AutoPtr.hpp new file mode 100644 index 0000000..0766773 --- /dev/null +++ b/djinni/cppcli/AutoPtr.hpp @@ -0,0 +1,70 @@ +// +// Copyright 2021 cross-language-cpp +// +// 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. +// + +#pragma once + +#include "Assert.hpp" + +template +ref struct AutoPtr { + AutoPtr() : _ptr(0) {} + AutoPtr(T* ptr) : _ptr(ptr) {} + AutoPtr(AutoPtr% right) : _ptr(right.Release()) {} + + ~AutoPtr() { + delete _ptr; + _ptr = 0; + } + !AutoPtr() { + //ASSERT(0 == _ptr); + delete _ptr; + } + T* operator->() { + if (0 == _ptr) { + throw gcnew System::ObjectDisposedException(System::String::Empty); + } + + return _ptr; + } + + T* GetPointer() { + return _ptr; + } + T& GetRef() { + if (0 == _ptr) { + throw gcnew System::ObjectDisposedException(System::String::Empty); + } + + return *_ptr; + } + T* Release() { + T* released = _ptr; + _ptr = 0; + return released; + } + void Reset() { + Reset(0); + } + void Reset(T* ptr) { + if (ptr != _ptr) { + delete _ptr; + _ptr = ptr; + } + } + +private: + T* _ptr; +}; diff --git a/djinni/cppcli/Error.cpp b/djinni/cppcli/Error.cpp new file mode 100644 index 0000000..423cd0d --- /dev/null +++ b/djinni/cppcli/Error.cpp @@ -0,0 +1,33 @@ +// +// Copyright 2021 cross-language-cpp +// +// 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. +// + +#include "Error.hpp" + +namespace djinni { + +void ThrowUnimplemented(const char *, const char * msg) { + throw gcnew System::NotImplementedException(msclr::interop::marshal_as(msg)); +} + +void ThrowNativeExceptionFromCurrent(const char *) { + try { + throw; + } catch (const std::exception & e) { + throw gcnew System::Exception(msclr::interop::marshal_as(e.what())); + } +} + +} diff --git a/djinni/cppcli/Error.hpp b/djinni/cppcli/Error.hpp new file mode 100644 index 0000000..0d3ad27 --- /dev/null +++ b/djinni/cppcli/Error.hpp @@ -0,0 +1,38 @@ +// +// Copyright 2021 cross-language-cpp +// +// 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. +// + +#pragma once + +#include + +namespace djinni { + +// Throws an exception for an unimplemented method call. +void ThrowUnimplemented(const char * ctx, const char * msg); + +// Helper function for exception translation. Do not call directly! +void ThrowNativeExceptionFromCurrent(const char * ctx); + +} + +#define DJINNI_UNIMPLEMENTED(msg) \ + ::djinni::ThrowUnimplemented(__FUNCTION__, msg); \ + return nullptr; // Silence warning C4715: not all control paths return a value + +#define DJINNI_TRANSLATE_EXCEPTIONS() \ + catch (const std::exception&) { \ + ::djinni::ThrowNativeExceptionFromCurrent(__FUNCTION__); \ + } \ No newline at end of file diff --git a/djinni/cppcli/Marshal.hpp b/djinni/cppcli/Marshal.hpp new file mode 100644 index 0000000..021bad0 --- /dev/null +++ b/djinni/cppcli/Marshal.hpp @@ -0,0 +1,277 @@ +// +// Copyright 2021 cross-language-cpp +// +// 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. +// + +#pragma once + +#include + +#include +#include +#include +#include +#include +#include + +#include "Assert.hpp" + +namespace djinni { + +template +struct Primitive { + using CppType = CppTypeArg; + using CsType = CsTypeArg; + + static CppType ToCpp(CsType x) noexcept { + return x; + } + static CsType FromCpp(CppType x) noexcept { + return x; + } +}; + +using Bool = Primitive; +using I8 = Primitive; +using I16 = Primitive; +using I32 = Primitive; +using I64 = Primitive; +using F32 = Primitive; +using F64 = Primitive; + +template +struct Enum { + using CppType = CppEnum; + using CsType = CsEnum; + + static CppType ToCpp(CsType e) noexcept { + return static_cast(e); + } + + static CsType FromCpp(CppType e) noexcept { + return static_cast(e); + } +}; + +struct String { + using CppType = std::string; + using CsType = System::String^; + + static CppType ToCpp(CsType string) { + ASSERT(string != nullptr); + cli::array^ bytes = System::Text::Encoding::UTF8->GetBytes(string); + CppType cpp_str; + cpp_str.resize(bytes->Length); + System::Runtime::InteropServices::Marshal::Copy(bytes,0,System::IntPtr(const_cast(cpp_str.data())),cpp_str.length()); + return cpp_str; + } + + static CsType FromCpp(const CppType& string) { + cli::array^ bytes = gcnew cli::array(string.length()); + for(size_t i=0; i< string.length(); i++) { + bytes[i] = string[i]; + } + return System::Text::Encoding::UTF8->GetString(bytes); + } +}; + +struct WString { + using CppType = std::wstring; + using CsType = System::String^; + + static CppType ToCpp(CsType string) { + ASSERT(string != nullptr); + return msclr::interop::marshal_as(string); + } + + static CsType FromCpp(const CppType& string) { + return msclr::interop::marshal_as(string); + } +}; + +struct Date { + using CppType = std::chrono::system_clock::time_point; + using CsType = System::DateTime; + + using Ticks = std::chrono::duration>; + static const auto TicksBeforeEpoch = 621355968000000000; + + static CppType ToCpp(CsType date) { + auto ticks = Ticks(date.Ticks - TicksBeforeEpoch); + return std::chrono::system_clock::time_point(ticks); + } + + static CsType FromCpp(const CppType& date) { + auto ticks = std::chrono::duration_cast(date.time_since_epoch()).count(); + return CsType(ticks + TicksBeforeEpoch); + } +}; + +struct Binary { + using CppType = std::vector; + using CsType = array^; + + using Boxed = Binary; + + static CppType ToCpp(CsType data) { + ASSERT(data != nullptr); + CppType v; + v.reserve(data->Length); + for each (auto value in data) { + v.emplace_back(value); + } + return v; + } + + static CsType FromCpp(const CppType& bytes) { + auto len = bytes.size(); + auto ret = gcnew array(len); + System::Runtime::InteropServices::Marshal::Copy(System::IntPtr(const_cast(&bytes[0])), ret, 0, len); + return ret; + } +}; + +template +struct IsRef : std::false_type {}; + +template +struct IsRef : std::true_type {}; + +template +struct CsOptional { + typedef System::Nullable type; +}; + +template +struct CsOptional { + typedef T^ type; +}; + +template class OptionalType, class T> +struct Optional { + // SFINAE helper: if C::CppOptType exists, opt_type(nullptr) will return + // that type. If not, it returns OptionalType. This is necessary + // because we special-case optional interfaces to be represented as a nullable + // std::shared_ptr, not optional> or optional>>. + template static OptionalType opt_type(...); + template static typename C::CppOptType opt_type(typename C::CppOptType*); + + using CppType = decltype(opt_type(0)); + using CsType = typename T::CsType; + + using CsOptionalType = typename ::djinni::CsOptional::type; + + // Enabled for reference types (^). + template ::value, int>::type = 0> + static CppType ToCpp(O obj) { + if (obj != nullptr) { + return T::ToCpp(obj); + } + return CppType(); + } + + // Enabled for value types that require System::Nullable<>. + template ::value, int>::type = 0> + static CppType ToCpp(O obj) { + if (obj.HasValue) { + return T::ToCpp(obj.Value); + } + return CppType(); + } + + // FromCpp used for normal optionals + static CsOptionalType FromCpp(const OptionalType& opt) { + return opt ? T::FromCpp(*opt) : CsOptionalType(); + } + + // FromCpp used for nullable objects + template + static CsType FromCpp(const typename C::CppOptType& cppOpt) { + return T::FromCppOpt(cppOpt); + } +}; + +template +struct List { + using CppType = std::vector; + using CsType = System::Collections::Generic::List^; + + static CppType ToCpp(CsType l) { + ASSERT(l != nullptr); + CppType v; + v.reserve(l->Count); + for each (auto value in l) { + v.emplace_back(T::ToCpp(value)); + } + return v; + } + + static CsType FromCpp(const CppType& v) { + auto l = gcnew System::Collections::Generic::List(); + for (const auto& value : v) { + l->Add(T::FromCpp(value)); + } + return l; + } +}; + +template +class Set { +public: + using CppType = std::unordered_set; + using CsType = System::Collections::Generic::HashSet^; + + static CppType ToCpp(CsType set) { + ASSERT(set != nullptr); + CppType s; + for each (auto value in set) { + s.insert(T::ToCpp(value)); + } + return s; + } + + static CsType FromCpp(const CppType& s) { + auto set = gcnew System::Collections::Generic::HashSet(); + for (const auto& value : s) { + set->Add(T::FromCpp(value)); + } + return set; + } +}; + +template +struct Map { + using CppType = std::unordered_map; + using CsType = System::Collections::Generic::Dictionary^; + + static CppType ToCpp(CsType map) { + ASSERT(map != nullptr); + CppType m; + m.reserve(map->Count); + for each (auto& kvp in map) { + m.emplace(Key::ToCpp(kvp.Key), Value::ToCpp(kvp.Value)); + } + return m; + } + + static CsType FromCpp(const CppType& m) { + auto map = gcnew System::Collections::Generic::Dictionary(m.size()); + for (const auto& kvp : m) { + map->Add(Key::FromCpp(kvp.first), Value::FromCpp(kvp.second)); + } + return map; + } +}; + +} // namespace djinni diff --git a/djinni/cppcli/WrapperCache.cpp b/djinni/cppcli/WrapperCache.cpp new file mode 100644 index 0000000..35553e8 --- /dev/null +++ b/djinni/cppcli/WrapperCache.cpp @@ -0,0 +1,41 @@ +// +// Copyright 2021 cross-language-cpp +// +// 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. +// + +#pragma once + +#include "WrapperCache.hpp" +#include "../proxy_cache_impl.hpp" + +namespace djinni { + +using UnowningImplPointer = CsProxyCacheTraits::UnowningImplPointer; + +struct CsHashCode { size_t operator() (UnowningImplPointer obj) const; }; +struct CsReferenceEquals { bool operator() (UnowningImplPointer obj1, UnowningImplPointer obj2) const; }; + +size_t CsHashCode::operator() (UnowningImplPointer obj) const { + return obj.hash_code(); +} +bool CsReferenceEquals::operator() (UnowningImplPointer obj1, UnowningImplPointer obj2) const { + auto ptr1 = obj1.lock(); + auto ptr2 = obj2.lock(); + return System::Object::ReferenceEquals(ptr1.get(), ptr2.get()); +} + +template class ProxyCache; +template class ProxyCache; + +} diff --git a/djinni/cppcli/WrapperCache.hpp b/djinni/cppcli/WrapperCache.hpp new file mode 100644 index 0000000..22f9c10 --- /dev/null +++ b/djinni/cppcli/WrapperCache.hpp @@ -0,0 +1,139 @@ +// +// Copyright 2021 cross-language-cpp +// +// 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. +// + +#pragma once + +#include "AutoPtr.hpp" +#include "../proxy_cache_interface.hpp" +#include + +namespace djinni { + +template +class CsRef; + +template +class CsRef { +public: + CsRef() : _ref(nullptr) {} + CsRef(CsType ref) : _ref(ref) {} + + CsRef(CsRef&&) = default; + CsRef& operator=(CsRef&&) = default; + + CsRef(const CsRef&) = default; + CsRef& operator=(const CsRef&) = default; + + operator bool() { return static_cast(_ref) != nullptr; } + CsType operator->() const { return get(); } + CsType get() const { return _ref; } + +private: + gcroot _ref; +}; + +/* +* Native wrapper to C#'s WeakReference type. Instances of this type do not prevent the +* garbage collector from reclaiming referred-to objects. +*/ +class WeakCsRef { +public: + WeakCsRef(System::Object^ ref) + : _weak_ref(gcnew System::WeakReference(ref)) + , _hash_code(ref->GetHashCode()) {} + + WeakCsRef(const CsRef& ref) + : _weak_ref(gcnew System::WeakReference(ref.get())) + , _hash_code(ref->GetHashCode()) {} + + CsRef lock() const { return dynamic_cast(_weak_ref->Target); } + bool expired() const { return !_weak_ref->IsAlive; } + size_t hash_code() { return _hash_code; } + +private: + CsRef _weak_ref; + size_t _hash_code; +}; + +struct CsHashCode; +struct CsReferenceEquals; +struct CsProxyCacheTraits { + using UnowningImplPointer = WeakCsRef; + using OwningImplPointer = CsRef; + using OwningProxyPointer = std::shared_ptr; + using WeakProxyPointer = std::weak_ptr; + using UnowningImplPointerHash = CsHashCode; + using UnowningImplPointerEqual = CsReferenceEquals; +}; + +// This declares that GenericProxyCache will be instantiated separately. The actual +// explicit instantiations are in DJIProxyCaches.mm. +extern template class ProxyCache; +using CsProxyCache = ProxyCache; + +template +static std::shared_ptr get_cs_proxy(CsType cs) { + return std::static_pointer_cast(CsProxyCache::get( + typeid(CppType), + cs, + [](const CsProxyCacheTraits::OwningImplPointer& cs) -> std::pair, CsProxyCacheTraits::UnowningImplPointer> { + return std::make_pair, CsProxyCacheTraits::UnowningImplPointer>(std::make_shared(cs), cs); + } + )); +} + +struct CppProxyCacheTraits { + using UnowningImplPointer = void *; + using OwningImplPointer = std::shared_ptr; + using OwningProxyPointer = CsRef; + using WeakProxyPointer = WeakCsRef; + using UnowningImplPointerHash = std::hash; + using UnowningImplPointerEqual = std::equal_to; +}; + +// This declares that GenericProxyCache will be instantiated separately. The actual +// explicit instantiations are in CppWrapperCache.cpp. +extern template class ProxyCache; +using CppProxyCache = ProxyCache; + +// If the type T has a handle declarator (^), provides the member typedef type which is the type referred to by T. Otherwise type is T. +template struct remove_handle { typedef T type; }; +template struct remove_handle { typedef T type; }; + +// Helper for get_cpp_proxy_impl that takes a std::shared_ptr. +template +CsType get_cpp_proxy_impl(const std::shared_ptr & cppRef) { + auto proxy = CppProxyCache::get( + typeid(cppRef), + cppRef, + [](const std::shared_ptr & cppRef) -> std::pair { + return { + CsRef(gcnew typename remove_handle::type(std::static_pointer_cast(cppRef))), + cppRef.get() + }; + } + ); + return dynamic_cast(proxy.get()); +} + +// get_cpp_proxy takes any smart pointer type, as long as it can be implicitly cast +// to std::shared_ptr. This means get_cpp_proxy can also be passed non-nullable pointers. +template +CsType get_cpp_proxy(const CppPtrType& cppRef) { + return get_cpp_proxy_impl::type>(cppRef); +} + +} diff --git a/djinni/proxy_cache_impl.hpp b/djinni/proxy_cache_impl.hpp index 897701b..9c3716a 100644 --- a/djinni/proxy_cache_impl.hpp +++ b/djinni/proxy_cache_impl.hpp @@ -21,6 +21,49 @@ #include #include +#ifdef __cplusplus_cli +#include + +class Mutex { + CRITICAL_SECTION _lock; +public: + Mutex(const Mutex&) = delete; + Mutex(Mutex&&) = delete; + Mutex& operator=(const Mutex&) = delete; + Mutex& operator=(Mutex&&) = delete; + + Mutex() throw() { + InitializeCriticalSection(&_lock); + } + ~Mutex() throw() { + DeleteCriticalSection(&_lock); + } + void lock() throw() { + EnterCriticalSection(&_lock); + } + void unlock() throw() { + LeaveCriticalSection(&_lock); + } +}; + +template +class UniqueLock { +public: + UniqueLock(T& mutex) throw() : _mutex(&mutex) { + _mutex->lock(); + } + ~UniqueLock() throw() { + _mutex->unlock(); + } +private: + T* _mutex; +}; +#else // __cplusplus_cli +#include +using Mutex = std::mutex; +template using UniqueLock = std::unique_lock; +#endif // __cplusplus_cli + // """ // This place is not a place of honor. // No highly esteemed deed is commemorated here. @@ -79,7 +122,7 @@ class ProxyCache::Pimpl { OwningProxyPointer get(const std::type_index & tag, const OwningImplPointer & impl, AllocatorFunction * alloc) { - std::unique_lock lock(m_mutex); + UniqueLock lock(m_mutex); UnowningImplPointer ptr = get_unowning(impl); auto existing_proxy_iter = m_mapping.find({tag, ptr}); if (existing_proxy_iter != m_mapping.end()) { @@ -101,7 +144,7 @@ class ProxyCache::Pimpl { * Erase an object from the proxy cache. */ void remove(const std::type_index & tag, const UnowningImplPointer & impl_unowning) { - std::unique_lock lock(m_mutex); + UniqueLock lock(m_mutex); auto it = m_mapping.find({tag, impl_unowning}); if (it != m_mapping.end()) { // The entry in the map should already be expired: this is called from Handle's @@ -119,6 +162,7 @@ class ProxyCache::Pimpl { } } + Pimpl() = default; private: struct KeyHash { std::size_t operator()(const Key & k) const { @@ -134,11 +178,7 @@ class ProxyCache::Pimpl { }; std::unordered_map m_mapping; - std::mutex m_mutex; - - // Only ProxyCache::get_base() can allocate these objects. - Pimpl() = default; - friend class ProxyCache; + Mutex m_mutex; }; template @@ -148,6 +188,38 @@ void ProxyCache::cleanup(const std::shared_ptr & base, base->remove(tag, ptr); } +#ifdef __cplusplus_cli +/* + * In C++/CLI (Windows), native static variables can only be initialized in the default + * AppDomain. As that would be a huge limiting factor for the users of this library, we + * work around it by holding the Pimpl instance in a managed class. Also, since managed + * classes can't hold native types by value, we keep the instance as a pointer to + * shared_ptr, instantiate it in the static constructor, and schedule it's deletion to take + * place when the current AppDomain gets unloaded. + */ +template +private ref class SingletonHolder { +private: + using Pimpl = typename ProxyCache::Pimpl; + using PimplPtr = std::shared_ptr; + static PimplPtr* _instance; + static SingletonHolder() { + _instance = new PimplPtr(new Pimpl); + System::AppDomain::CurrentDomain->DomainUnload += gcnew System::EventHandler(&OnDomainUnload); + } + static void OnDomainUnload(System::Object^, System::EventArgs^) { + System::AppDomain::CurrentDomain->DomainUnload -= gcnew System::EventHandler(&OnDomainUnload); + delete _instance; + } +public: + static property PimplPtr* Instance { + PimplPtr* get() { + return _instance; + } + } +}; +#endif // __cplusplus_cli + /* * Magic-static singleton. * @@ -159,10 +231,14 @@ void ProxyCache::cleanup(const std::shared_ptr & base, */ template auto ProxyCache::get_base() -> const std::shared_ptr & { +#ifndef __cplusplus_cli static const std::shared_ptr instance(new Pimpl); // Return by const-ref. This is safe to call any time except during static destruction. // Returning by reference lets us avoid touching the refcount unless needed. return instance; +#else // __cplusplus_cli + return *SingletonHolder::Instance; +#endif // __cplusplus_cli } template diff --git a/docs/developer-guide.md b/docs/developer-guide.md index 6e91562..98ab0d2 100644 --- a/docs/developer-guide.md +++ b/docs/developer-guide.md @@ -32,8 +32,27 @@ A custom `djinni` executable can be specified with the CMake option ### Running Tests +#### Java, Objective-C + ```bash cd build/test-suite ctest ``` +#### C# + +1. Generate Visual Studio Solution with `-G "Visual Studio 16 2019"`: + ```sh + cmake -S . -B build -DDJINNI_WITH_CPPCLI=ON -DDJINNI_STATIC_LIB=ON -G "Visual Studio 16 2019" + ``` +2. Open the solution `djinni_support_lib.sln` in Visual Studio. +3. Build `DjinniCppCliTest`. +4. Run the tests: Test > Run All Tests. + +## Release process + +To release a new version of the support-lib, the following steps must be followed: + +1. Create a [new release](https://github.com/cross-language-cpp/djinni-support-lib/releases/new) on Github like [described here](https://docs.github.com/en/github/administering-a-repository/managing-releases-in-a-repository). + Set a tag version following [semantic versioning](https://semver.org/) rules (`v..`) and describe what has changed in the new version. +3. Create a PR to the [conan-center-index](https://github.com/conan-io/conan-center-index/tree/master/recipes/djinni-support-lib) to publish the new version to [Conan Center](https://conan.io/center/djinni-support-lib). diff --git a/test-suite/CMakeLists.txt b/test-suite/CMakeLists.txt index e47fb30..fa7a21d 100644 --- a/test-suite/CMakeLists.txt +++ b/test-suite/CMakeLists.txt @@ -2,17 +2,13 @@ cmake_minimum_required(VERSION 3.6.0) set(CMAKE_CXX_STANDARD 17) - if(APPLE) set(MACOSX_RPATH TRUE) set(CMAKE_MACOSX_RPATH ${CMAKE_CURRENT_BINARY_DIR}) endif() - - enable_testing() - include(Djinni.cmake) add_djinni_target(DjinniAllTests @@ -22,7 +18,7 @@ add_djinni_target(DjinniAllTests CPP_NAMESPACE "testsuite" IDENT_CPP_ENUM_TYPE "foo_bar" CPP_OPTIONAL_TEMPLATE "std::optional" - CPP_OPTIONAL_HEADER "" + CPP_OPTIONAL_HEADER "\"optional\"" CPP_EXTENDED_RECORD_INCLUDE_PREFIX "test-suite/handwritten-src/cpp/" CPP_ENUM_HASH_WORKAROUND JAVA_OUT "${CMAKE_CURRENT_BINARY_DIR}/generated-src/java" @@ -38,6 +34,9 @@ add_djinni_target(DjinniAllTests OBJC_OUT "${CMAKE_CURRENT_BINARY_DIR}/generated-src/objc" OBJC_TYPE_PREFIX "DB" OBJCPP_OUT "${CMAKE_CURRENT_BINARY_DIR}/generated-src/objc" + CPPCLI_OUT "${CMAKE_CURRENT_BINARY_DIR}/generated-src/cppcli" + CPPCLI_NAMESPACE "Djinni::TestSuite" + CPPCLI_INCLUDE_CPP_PREFIX "cpp/" YAML_OUT "${CMAKE_CURRENT_BINARY_DIR}/generated-src/yaml" YAML_PREFIX "test_" @@ -46,6 +45,7 @@ add_djinni_target(DjinniAllTests JNI_OUT_FILES JNI_GENERATED_SRCS OBJC_OUT_FILES OBJC_GENERATED_SRCS OBJCPP_OUT_FILES OBJCPP_GENERATED_SRCS + CPPCLI_OUT_FILES CPPCLI_GENERATED_SRCS YAML_OUT_FILE YAML_GENERATED_SRCS ) @@ -55,7 +55,7 @@ add_djinni_target(DjinniWCharTests CPP_NAMESPACE "testsuite" IDENT_CPP_ENUM_TYPE "foo_bar" CPP_OPTIONAL_TEMPLATE "std::optional" - CPP_OPTIONAL_HEADER "" + CPP_OPTIONAL_HEADER "\"optional\"" CPP_EXTENDED_RECORD_INCLUDE_PREFIX "test-suite/handwritten-src/cpp/" CPP_ENUM_HASH_WORKAROUND CPP_USE_WIDE_STRINGS @@ -72,6 +72,9 @@ add_djinni_target(DjinniWCharTests OBJC_OUT "${CMAKE_CURRENT_BINARY_DIR}/generated-src/objc" OBJC_TYPE_PREFIX "DB" OBJCPP_OUT "${CMAKE_CURRENT_BINARY_DIR}/generated-src/objc" + CPPCLI_OUT "${CMAKE_CURRENT_BINARY_DIR}/generated-src/cppcli" + CPPCLI_NAMESPACE "Djinni::TestSuite" + CPPCLI_INCLUDE_CPP_PREFIX "cpp/" YAML_OUT "${CMAKE_CURRENT_BINARY_DIR}/generated-src/yaml" YAML_PREFIX "test_" @@ -80,6 +83,7 @@ add_djinni_target(DjinniWCharTests JNI_OUT_FILES WCHAR_JNI_GENERATED_SRCS OBJC_OUT_FILES WCHAR_OBJC_GENERATED_SRCS OBJCPP_OUT_FILES WCHAR_OBJCPP_GENERATED_SRCS + CPPCLI_OUT_FILES WCHAR_CPPCLI_GENERATED_SRCS YAML_OUT_FILE WCHAR_YAML_GENERATED_SRCS ) @@ -94,7 +98,7 @@ add_djinni_target(DjinniPyTests CPP_NAMESPACE "testsuite" IDENT_CPP_ENUM_TYPE "foo_bar" CPP_OPTIONAL_TEMPLATE "std::optional" - CPP_OPTIONAL_HEADER "" + CPP_OPTIONAL_HEADER "\"optional\"" PY_OUT_FILES PYTHON_GENERATED_SRCS PYCFFI_OUT_FILES PYCFFI_GENERATED_SRCS @@ -252,8 +256,52 @@ if(DJINNI_WITH_PYTHON) COMMENT "Building CFFI lib" VERBATIM ) - + add_test(NAME PythonTests COMMAND ${CMAKE_COMMAND} -E env PYTHONPATH=${PYTHONPATH} ${PYTHON_EXECUTABLE} -m pytest -s "${CMAKE_CURRENT_SOURCE_DIR}/handwritten-src/python" ) endif() + +if(DJINNI_WITH_CPPCLI) + add_library(DjinniCppCli SHARED + ${CPP_HANDWRITTEN_SRCS} + ${CPP_GENERATED_SRCS} + ${CPPCLI_GENERATED_SRCS} + ${WCHAR_CPP_GENERATED_SRCS} + ${WCHAR_CPPCLI_GENERATED_SRCS} + ) + target_include_directories(DjinniCppCli PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR}/handwritten-src/cpp + ${CMAKE_CURRENT_SOURCE_DIR}/handwritten-src + ${CMAKE_CURRENT_BINARY_DIR}/generated-src/cpp + ${CMAKE_CURRENT_BINARY_DIR}/generated-src + ) + set_target_properties(DjinniCppCli PROPERTIES + VS_DOTNET_REFERENCES "System;System.Core" + COMMON_LANGUAGE_RUNTIME "" + ) + target_link_libraries(DjinniCppCli djinni_support_lib) + + add_library(DjinniCppCliTest SHARED + handwritten-src/cs/ClientInterfaceImpl.cs + handwritten-src/cs/ClientInterfaceTest.cs + handwritten-src/cs/CppExceptionTest.cs + handwritten-src/cs/DurationTest.cs + handwritten-src/cs/EnumTest.cs + handwritten-src/cs/MapRecordTest.cs + handwritten-src/cs/MockRecordTest.cs + handwritten-src/cs/NestedCollectionTest.cs + handwritten-src/cs/PrimitiveListTest.cs + handwritten-src/cs/PrimitivesTest.cs + handwritten-src/cs/RecordWithDerivingsTest.cs + handwritten-src/cs/SetRecordTest.cs + handwritten-src/cs/TokenTest.cs + handwritten-src/cs/WcharTest.cs + ) + set_target_properties(DjinniCppCliTest PROPERTIES + VS_DOTNET_REFERENCES "System;System.Core" + VS_PACKAGE_REFERENCES "nunit_3.13.1;NUnit3TestAdapter_3.17.0;Microsoft.NET.Test.Sdk_16.9.4" + ) + + target_link_libraries(DjinniCppCliTest DjinniCppCli) +endif() diff --git a/test-suite/Djinni.cmake b/test-suite/Djinni.cmake index 075573a..082acb5 100644 --- a/test-suite/Djinni.cmake +++ b/test-suite/Djinni.cmake @@ -1,17 +1,20 @@ - -# Resolve DJINNI_EXECUTABLE -set(DJINNI_EXECUTABLE_NAMES "djinni") -find_program(DJINNI_EXECUTABLE ${DJINNI_EXECUTABLE_NAMES} - DOC "Path to the Djinni executable" - PATHS ${DJINNI_EXECUTABLE_PATH}) -if(DJINNI_EXECUTABLE) - execute_process(COMMAND ${DJINNI_EXECUTABLE} "--version" OUTPUT_VARIABLE DJINNI_VERSION) - string(REGEX REPLACE "\n+$" "" DJINNI_VERSION "${DJINNI_VERSION}") - message(STATUS "Found Djinni: ${DJINNI_EXECUTABLE} (${DJINNI_VERSION})") +cmake_minimum_required(VERSION 3.18) + +# find Djinni executable. +# On windows find_program() does not work for finding the `djinni.bat` script. +# The script must either be on the PATH or `DJINNI_EXECUTABLE` must explicitly be predefined. +if(CMAKE_HOST_SYSTEM_NAME STREQUAL "Windows") + if(NOT DEFINED CACHE{DJINNI_EXECUTABLE}) + set(DJINNI_EXECUTABLE djinni.bat CACHE FILEPATH "path of djinni binary") + endif() else() - message(FATAL_ERROR "Could not find DJINNI_EXECUTABLE using the following names: ${DJINNI_EXECUTABLE_NAMES}") + find_program(DJINNI_EXECUTABLE djinni REQUIRED) endif() +execute_process(COMMAND ${DJINNI_EXECUTABLE} "--version" OUTPUT_VARIABLE DJINNI_VERSION) +string(REGEX REPLACE "\n+$" "" DJINNI_VERSION "${DJINNI_VERSION}") +message(STATUS "Found Djinni: ${DJINNI_EXECUTABLE} (${DJINNI_VERSION})") + macro(append_if_defined LIST OPTION) if(NOT "${ARGN}" STREQUAL "") @@ -43,8 +46,7 @@ macro(resolve_djinni_inputs) message(FATAL_ERROR ${DJINNI_STDERR}) endif() - file(READ ${DJINNI_INPUTS_TXT} ${DJINNI_RESULT}) - string(REGEX REPLACE "\n" ";" ${DJINNI_RESULT} ${${DJINNI_RESULT}}) + file(STRINGS ${DJINNI_INPUTS_TXT} ${DJINNI_RESULT}) endmacro() macro(resolve_djinni_outputs) @@ -53,6 +55,7 @@ macro(resolve_djinni_outputs) cmake_parse_arguments(DJINNI "" "${SINGLE_VALUE}" "${MULTI_VALUE}" "${ARGN}") set(DJINNI_OUTPUTS_TXT "${CMAKE_CURRENT_BINARY_DIR}/${DJINNI_RESULT}.txt") + execute_process( COMMAND ${DJINNI_COMMAND} "--skip-generation" "true" "--list-out-files" "${DJINNI_OUTPUTS_TXT}" RESULT_VARIABLE DJINNI_CONFIGURATION_RESULT @@ -63,9 +66,7 @@ macro(resolve_djinni_outputs) message(FATAL_ERROR ${DJINNI_STDERR}) endif() - file(READ ${DJINNI_OUTPUTS_TXT} FILE_CONTENTS) - file(READ ${DJINNI_OUTPUTS_TXT} ${DJINNI_RESULT}) - string(REGEX REPLACE "\n" ";" ${DJINNI_RESULT} ${${DJINNI_RESULT}}) + file(STRINGS ${DJINNI_OUTPUTS_TXT} ${DJINNI_RESULT}) endmacro() @@ -173,6 +174,12 @@ function(add_djinni_target) IDENT_PY_CONST + CPPCLI_OUT + CPPCLI_OUT_FILES + CPPCLI_NAMESPACE + CPPCLI_INCLUDE_CPP_PREFIX + CPPCLI_BASE_LIB_INCLUDE_PREFIX + YAML_OUT YAML_OUT_FILE YAML_PREFIX @@ -263,6 +270,10 @@ function(add_djinni_target) append_if_defined(DJINNI_GENERATION_COMMAND "--ident-py-enum" ${DJINNI_IDENT_PY_ENUM}) append_if_defined(DJINNI_GENERATION_COMMAND "--ident-py-const" ${DJINNI_IDENT_PY_CONST}) + append_if_defined(DJINNI_GENERATION_COMMAND "--cppcli-namespace" ${DJINNI_CPPCLI_NAMESPACE}) + append_if_defined(DJINNI_GENERATION_COMMAND "--cppcli-include-cpp-prefix" ${DJINNI_CPPCLI_INCLUDE_CPP_PREFIX}) + append_if_defined(DJINNI_GENERATION_COMMAND "--cppcli-base-lib-include-prefix" ${DJINNI_CPPCLI_BASE_LIB_INCLUDE_PREFIX}) + if(DEFINED DJINNI_CPP_OUT_FILES) set(DJINNI_CPP_GENERATION_COMMAND ${DJINNI_GENERATION_COMMAND}) append_if_defined(DJINNI_CPP_GENERATION_COMMAND "--cpp-out" ${DJINNI_CPP_OUT}) @@ -395,5 +406,21 @@ function(add_djinni_target) set(${DJINNI_C_WRAPPER_OUT_FILES} ${C_WRAPPER_OUT_FILES} PARENT_SCOPE) endif() + if(DEFINED DJINNI_CPPCLI_OUT_FILES) + set(DJINNI_CPPCLI_GENERATION_COMMAND ${DJINNI_GENERATION_COMMAND}) + append_if_defined(DJINNI_CPPCLI_GENERATION_COMMAND "--cppcli-out" ${DJINNI_CPPCLI_OUT}) + + resolve_djinni_outputs(COMMAND "${DJINNI_CPPCLI_GENERATION_COMMAND}" RESULT CPPCLI_OUT_FILES) + + add_custom_command( + OUTPUT ${CPPCLI_OUT_FILES} + DEPENDS ${DJINNI_INPUTS} + COMMAND ${DJINNI_CPPCLI_GENERATION_COMMAND} + COMMENT "Generating Djinni C++/CLI bindings from ${DJINNI_IDL}" + VERBATIM + ) + set(${DJINNI_CPPCLI_OUT_FILES} ${CPPCLI_OUT_FILES} PARENT_SCOPE) + endif() + endfunction() diff --git a/test-suite/djinni/client_interface.djinni b/test-suite/djinni/client_interface.djinni index c3e5330..87a88ea 100755 --- a/test-suite/djinni/client_interface.djinni +++ b/test-suite/djinni/client_interface.djinni @@ -6,7 +6,7 @@ client_returned_record = record { } deriving (parcelable) # Client interface -client_interface = interface +p +j +o { +client_interface = interface +j +o +p +s { # Returns record of given string get_record(record_id: i64, utf8string: string, misc: optional): client_returned_record; identifier_check(data: binary, r: i32, jret: i64): f64; diff --git a/test-suite/djinni/single_language_interfaces.djinni b/test-suite/djinni/single_language_interfaces.djinni index 6ffa09c..282e853 100644 --- a/test-suite/djinni/single_language_interfaces.djinni +++ b/test-suite/djinni/single_language_interfaces.djinni @@ -1,14 +1,17 @@ objc_only_listener = interface +o {} java_only_listener = interface +j {} py_only_listener = interface +p {} +cs_only_listener = interface +s {} # Generating and compiling this makes sure other languages don't break # on references to interfaces they don't need. -uses_single_language_listeners = interface +o +j +c { +uses_single_language_listeners = interface +o +j +c +s { callForObjC(l: objc_only_listener); returnForObjC(): objc_only_listener; callForJava(l: java_only_listener); returnForJava(): java_only_listener; callForPy(l: py_only_listener); returnForPy(): py_only_listener; + callForCs(l: cs_only_listener); + returnForCs(): cs_only_listener; } diff --git a/test-suite/djinni/user_token.djinni b/test-suite/djinni/user_token.djinni index b9ecde4..92424f3 100644 --- a/test-suite/djinni/user_token.djinni +++ b/test-suite/djinni/user_token.djinni @@ -1,3 +1,3 @@ -user_token = interface +c +j +o { +user_token = interface +c +j +o +s { whoami() : string; } diff --git a/test-suite/djinni/vendor/third-party/date.yaml b/test-suite/djinni/vendor/third-party/date.yaml index 3b408d7..334673f 100644 --- a/test-suite/djinni/vendor/third-party/date.yaml +++ b/test-suite/djinni/vendor/third-party/date.yaml @@ -31,7 +31,7 @@ jni: typename: jobject typeSignature: 'Ljava/util/Date;' cs: - translator: 'coming soon' - header: 'coming soon' - typename: 'coming soon' + translator: '::djinni::Date' + header: '"djinni/cppcli/Marshal.hpp"' + typename: 'System::DateTime' reference: false diff --git a/test-suite/djinni/vendor/third-party/duration.yaml b/test-suite/djinni/vendor/third-party/duration.yaml index dc3aa17..70f13ed 100755 --- a/test-suite/djinni/vendor/third-party/duration.yaml +++ b/test-suite/djinni/vendor/third-party/duration.yaml @@ -32,9 +32,9 @@ jni: typename: jobject typeSignature: 'Ljava/time/Duration;' cs: - translator: 'coming soon' - header: 'coming soon' - typename: 'coming soon' + translator: '::djinni::Duration' + header: '"Duration-cs.hpp"' + typename: 'System::TimeSpan' reference: false --- name: h @@ -66,9 +66,9 @@ jni: typename: jobject typeSignature: 'Ljava/time/Duration;' cs: - translator: 'coming soon' - header: 'coming soon' - typename: 'coming soon' + translator: '::djinni::Duration_h' + header: '"Duration-cs.hpp"' + typename: 'System::TimeSpan' reference: false --- name: min @@ -100,9 +100,9 @@ jni: typename: jobject typeSignature: 'Ljava/time/Duration;' cs: - translator: 'coming soon' - header: 'coming soon' - typename: 'coming soon' + translator: '::djinni::Duration_min' + header: '"Duration-cs.hpp"' + typename: 'System::TimeSpan' reference: false --- name: s @@ -134,9 +134,9 @@ jni: typename: jobject typeSignature: 'Ljava/time/Duration;' cs: - translator: 'coming soon' - header: 'coming soon' - typename: 'coming soon' + translator: '::djinni::Duration_s' + header: '"Duration-cs.hpp"' + typename: 'System::TimeSpan' reference: false --- name: ms @@ -168,9 +168,9 @@ jni: typename: jobject typeSignature: 'Ljava/time/Duration;' cs: - translator: 'coming soon' - header: 'coming soon' - typename: 'coming soon' + translator: '::djinni::Duration_ms' + header: '"Duration-cs.hpp"' + typename: 'System::TimeSpan' reference: false --- name: us @@ -202,9 +202,9 @@ jni: typename: jobject typeSignature: 'Ljava/time/Duration;' cs: - translator: 'coming soon' - header: 'coming soon' - typename: 'coming soon' + translator: '::djinni::Duration_us' + header: '"Duration-cs.hpp"' + typename: 'System::TimeSpan' reference: false --- name: ns @@ -236,7 +236,7 @@ jni: typename: jobject typeSignature: 'Ljava/time/Duration;' cs: - translator: 'coming soon' - header: 'coming soon' - typename: 'coming soon' + translator: '::djinni::Duration_ns' + header: '"Duration-cs.hpp"' + typename: 'System::TimeSpan' reference: false diff --git a/test-suite/handwritten-src/cpp/Duration-cs.hpp b/test-suite/handwritten-src/cpp/Duration-cs.hpp new file mode 100644 index 0000000..41aacea --- /dev/null +++ b/test-suite/handwritten-src/cpp/Duration-cs.hpp @@ -0,0 +1,37 @@ +#pragma once + +#include + +namespace djinni { + +// This is only a helper, trying to use it as member/param will fail +template +struct DurationPeriod; + +using Duration_h = DurationPeriod>; +using Duration_min = DurationPeriod>; +using Duration_s = DurationPeriod>; +using Duration_ms = DurationPeriod; +using Duration_us = DurationPeriod; +using Duration_ns = DurationPeriod; + +template +struct Duration; + +template +struct Duration> { + using CppType = std::chrono::duration; + using CsType = System::TimeSpan; + + using Ticks = std::chrono::duration>; + + static CppType ToCpp(CsType dt) { + return std::chrono::duration_cast(Ticks(dt.Ticks)); + } + static CsType FromCpp(CppType dt) { + auto ms = std::chrono::duration_cast(dt).count(); + return System::TimeSpan::FromTicks(ms); + } +}; + +} \ No newline at end of file diff --git a/test-suite/handwritten-src/cpp/test_helpers.cpp b/test-suite/handwritten-src/cpp/test_helpers.cpp index d1a3cf5..89ac2bf 100644 --- a/test-suite/handwritten-src/cpp/test_helpers.cpp +++ b/test-suite/handwritten-src/cpp/test_helpers.cpp @@ -9,6 +9,7 @@ #include "primitive_list.hpp" #include "set_record.hpp" #include +#include namespace testsuite { diff --git a/test-suite/handwritten-src/cs/ClientInterfaceImpl.cs b/test-suite/handwritten-src/cs/ClientInterfaceImpl.cs new file mode 100644 index 0000000..52e6b1b --- /dev/null +++ b/test-suite/handwritten-src/cs/ClientInterfaceImpl.cs @@ -0,0 +1,32 @@ +using Djinni.TestSuite; + +namespace Djinni.Testing.Unit +{ + public class ClientInterfaceImpl : ClientInterface + { + public override ClientReturnedRecord GetRecord(long recordId, string utf8String, string misc) + { + return new ClientReturnedRecord(recordId, utf8String, misc); + } + + public override double IdentifierCheck(byte[] data, int r, long jret) + { + throw new System.NotImplementedException(); + } + + public override string ReturnStr() + { + return "test"; + } + + public override string MethTakingInterface(ClientInterface i) + { + return i != null ? i.ReturnStr() : ""; + } + + public override string MethTakingOptionalInterface(ClientInterface i) + { + return i != null ? i.ReturnStr() : ""; + } + } +} \ No newline at end of file diff --git a/test-suite/handwritten-src/cs/ClientInterfaceTest.cs b/test-suite/handwritten-src/cs/ClientInterfaceTest.cs new file mode 100644 index 0000000..603552c --- /dev/null +++ b/test-suite/handwritten-src/cs/ClientInterfaceTest.cs @@ -0,0 +1,44 @@ +using Djinni.TestSuite; +using NUnit.Framework; + +namespace Djinni.Testing.Unit +{ + [TestFixture] + public class ClientInterfaceTest + { + private ClientInterface _csClientInterface; + + [SetUp] + public void SetUp() + { + _csClientInterface = new ClientInterfaceImpl(); + } + + [Test] + public void TestClientReturn() + { + Assert.That(() => TestHelpers.CheckClientInterfaceAscii(_csClientInterface), Throws.Nothing); + } + + [Test] + public void TestClientReturnUtf8() + { + Assert.That(() => TestHelpers.CheckClientInterfaceNonascii(_csClientInterface), Throws.Nothing); + } + + [Test] + public void TestClientInterfaceArgs() + { + Assert.That(() => TestHelpers.CheckClientInterfaceArgs(_csClientInterface), Throws.Nothing); + } + + [Test] + public void TestReverseClientInterfaceArgs() + { + var i = ReverseClientInterface.Create(); + + Assert.That(() => i.MethTakingInterface(i), Is.EqualTo("test")); + Assert.That(() => i.MethTakingOptionalInterface(i), Is.EqualTo("test")); + } + } +} \ No newline at end of file diff --git a/test-suite/handwritten-src/cs/CppExceptionTest.cs b/test-suite/handwritten-src/cs/CppExceptionTest.cs new file mode 100644 index 0000000..08df53f --- /dev/null +++ b/test-suite/handwritten-src/cs/CppExceptionTest.cs @@ -0,0 +1,23 @@ +using Djinni.TestSuite; +using NUnit.Framework; + +namespace Djinni.Testing.Unit +{ + [TestFixture] + public class CppExceptionTest + { + private CppException _cppInterface; + + [SetUp] + public void SetUp() + { + _cppInterface = CppException.Get(); + } + + [Test] + public void TestCppException() + { + Assert.That(() => _cppInterface.ThrowAnException(), Throws.Exception.Message.EqualTo("Exception Thrown")); + } + } +} \ No newline at end of file diff --git a/test-suite/handwritten-src/cs/DurationTest.cs b/test-suite/handwritten-src/cs/DurationTest.cs new file mode 100644 index 0000000..fb07bf8 --- /dev/null +++ b/test-suite/handwritten-src/cs/DurationTest.cs @@ -0,0 +1,41 @@ +using System; +using Djinni.TestSuite; +using NUnit.Framework; + +namespace Djinni.Testing.Unit +{ + [TestFixture] + public class DurationTest + { + [Test] + public void Test() + { + Assert.That(() => TestDuration.HoursString(TimeSpan.FromHours(1)), Is.EqualTo("1")); + Assert.That(() => TestDuration.MinutesString(TimeSpan.FromMinutes(1)), Is.EqualTo("1")); + Assert.That(() => TestDuration.SecondsString(TimeSpan.FromSeconds(1)), Is.EqualTo("1")); + Assert.That(() => TestDuration.MillisString(TimeSpan.FromMilliseconds(1)), Is.EqualTo("1")); + Assert.That(() => TestDuration.MicrosString(TimeSpan.FromTicks(10)), Is.EqualTo("1")); // 1 tick = 100 nanoseconds + Assert.That(() => TestDuration.NanosString(TimeSpan.FromTicks(1)), Is.EqualTo("100")); // 1 tick = 100 nanoseconds + + Assert.That(() => TestDuration.Hours(1).Hours, Is.EqualTo(1)); + Assert.That(() => TestDuration.Minutes(1).Minutes, Is.EqualTo(1)); + Assert.That(() => TestDuration.Seconds(1).Seconds, Is.EqualTo(1)); + Assert.That(() => TestDuration.Millis(1).Milliseconds, Is.EqualTo(1)); + Assert.That(() => TestDuration.Micros(1).Ticks, Is.EqualTo(10)); // 1 tick = 100 nanoseconds + Assert.That(() => TestDuration.Nanos(100).Ticks, Is.EqualTo(1)); // 1 tick = 100 nanoseconds + + Assert.That(() => TestDuration.Hoursf(1.5).TotalMinutes, Is.EqualTo(90)); + Assert.That(() => TestDuration.Minutesf(1.5).TotalSeconds, Is.EqualTo(90)); + Assert.That(() => TestDuration.Secondsf(1.5).TotalMilliseconds, Is.EqualTo(1500)); + Assert.That(() => TestDuration.Millisf(1.5).Ticks, Is.EqualTo(1500 * 10)); + Assert.That(() => TestDuration.Microsf(1.5).Ticks, Is.EqualTo(15)); // 1 tick = 100 nanoseconds + Assert.That(() => TestDuration.Nanosf(100.5).Ticks, Is.EqualTo(1)); // 1 tick = 100 nanoseconds + + Assert.That(() => TestDuration.Box(1)?.Seconds, Is.EqualTo(1)); + Assert.That(() => TestDuration.Box(-1), Is.Null); + + Assert.That(() => TestDuration.Unbox(TimeSpan.FromSeconds(1)), Is.EqualTo(1)); + Assert.That(() => TestDuration.Unbox(null), Is.EqualTo(-1)); + } + } +} \ No newline at end of file diff --git a/test-suite/handwritten-src/cs/EnumTest.cs b/test-suite/handwritten-src/cs/EnumTest.cs new file mode 100644 index 0000000..c1ab059 --- /dev/null +++ b/test-suite/handwritten-src/cs/EnumTest.cs @@ -0,0 +1,63 @@ +using System.Collections.Generic; +using Djinni.TestSuite; +using NUnit.Framework; + +namespace Djinni.Testing.Unit +{ + [TestFixture] + public class EnumTest + { + [Test] + public void TestEnumKey() + { + var m = new Dictionary + { + {Color.Red, "red"}, + {Color.Orange, "orange"}, + {Color.Yellow, "yellow"}, + {Color.Green, "green"}, + {Color.Blue, "blue"}, + {Color.Indigo, "indigo"}, + {Color.Violet, "violet"} + }; + Assert.That(() => TestHelpers.CheckEnumMap(m), Throws.Nothing); + } + + [Test] + public void TestAccessFlagRoundtrip() + { + AccessFlags[] flags = + { + AccessFlags.Nobody, + AccessFlags.Everybody, + AccessFlags.OwnerRead, + AccessFlags.OwnerRead | AccessFlags.OwnerWrite, + AccessFlags.OwnerRead | AccessFlags.OwnerWrite | AccessFlags.OwnerExecute + }; + + foreach (var flag in flags) + { + Assert.That(FlagRoundtrip.RoundtripAccess(flag), Is.EqualTo(flag)); + Assert.That(FlagRoundtrip.RoundtripAccessBoxed(flag), Is.EqualTo(flag)); + } + Assert.That(FlagRoundtrip.RoundtripAccessBoxed(null), Is.Null); + } + + [Test] + public void TestEmptyFlagRoundtrip() + { + EmptyFlags[] flags = + { + EmptyFlags.None, + EmptyFlags.All + }; + + foreach (var flag in flags) + { + Assert.That(FlagRoundtrip.RoundtripEmpty(flag), Is.EqualTo(flag)); + Assert.That(FlagRoundtrip.RoundtripEmptyBoxed(flag), Is.EqualTo(flag)); + } + Assert.That(FlagRoundtrip.RoundtripEmptyBoxed(null), Is.Null); + } + } +} \ No newline at end of file diff --git a/test-suite/handwritten-src/cs/MapRecordTest.cs b/test-suite/handwritten-src/cs/MapRecordTest.cs new file mode 100644 index 0000000..9f1d52a --- /dev/null +++ b/test-suite/handwritten-src/cs/MapRecordTest.cs @@ -0,0 +1,69 @@ +using System.Collections.Generic; +using System.Linq; +using Djinni.TestSuite; +using NUnit.Framework; + +namespace Djinni.Testing.Unit +{ + [TestFixture] + public class MapRecordTest + { + [Test] + public void TestCppMapToCsMap() + { + CheckCsMap(TestHelpers.GetMap()); + } + + [Test] + public void TestEmptyCppMapToCsMap() + { + Assert.That(TestHelpers.GetEmptyMap, Is.Empty); + } + + [Test] + public void TestCppMapListToCsMapList() + { + var mapListRecord = TestHelpers.GetMapListRecord(); + var mapList = mapListRecord.MapList; + Assert.That(() => mapList, Has.Count.EqualTo(1)); + CheckCsMap(mapList.First()); + } + + [Test] + public void TestCsMapToCppMap() + { + Assert.That(() => TestHelpers.CheckMap(CsMap), Is.True); + } + + [Test] + public void TestEmptyCsMapToCppMap() + { + Assert.That(() => TestHelpers.CheckEmptyMap(new Dictionary()), Is.True); + } + + [Test] + public void TestCsMapListToCppMapList() + { + var mapList = new List>{ CsMap }; + Assert.That(() => TestHelpers.CheckMapListRecord(new MapListRecord(mapList)), Is.True); + } + + private static Dictionary CsMap => new Dictionary + { + {"String1", 1}, + {"String2", 2}, + {"String3", 3} + }; + + private void CheckCsMap(Dictionary map) + { + var expectedMap = new Dictionary + { + {"String1", 1}, + {"String2", 2}, + {"String3", 3} + }; + Assert.That(() => map, Is.EquivalentTo(expectedMap)); + } + } +} \ No newline at end of file diff --git a/test-suite/handwritten-src/cs/MockRecordTest.cs b/test-suite/handwritten-src/cs/MockRecordTest.cs new file mode 100644 index 0000000..529cce2 --- /dev/null +++ b/test-suite/handwritten-src/cs/MockRecordTest.cs @@ -0,0 +1,25 @@ +using System.Runtime.InteropServices; +using Djinni.TestSuite; +using NUnit.Framework; + +namespace Djinni.Testing.Unit +{ + [TestFixture] + public class MockRecordTest + { + [Test] + public void TestMockConstants() + { + Constants mock = new MockConstants(); + Assert.That(() => mock.ToString(), Is.EqualTo("MockConstants{}"), "The ToString() method should be overridden."); + } + } + + public class MockConstants : Constants + { + public override string ToString() + { + return "MockConstants{}"; + } + } +} \ No newline at end of file diff --git a/test-suite/handwritten-src/cs/NestedCollectionTest.cs b/test-suite/handwritten-src/cs/NestedCollectionTest.cs new file mode 100644 index 0000000..7819a1c --- /dev/null +++ b/test-suite/handwritten-src/cs/NestedCollectionTest.cs @@ -0,0 +1,35 @@ +using System.Collections.Generic; +using Djinni.TestSuite; +using NUnit.Framework; + +namespace Djinni.Testing.Unit +{ + [TestFixture] + public class NestedCollectionTest + { + private NestedCollection _nestedCollection; + + [SetUp] + public void SetUp() + { + _nestedCollection = new NestedCollection(new List> + { + new HashSet { "String1", "String2" }, + new HashSet { "StringA", "StringB" } + }); + } + + [Test] + public void TestCppNestedRecordToCsNestedCollection() + { + var converted = TestHelpers.GetNestedCollection(); + Assert.That(() => converted.SetList, Is.EquivalentTo(_nestedCollection.SetList)); + } + + [Test] + public void TestCsNestedRecordToCppNestedCollection() + { + Assert.That(() => TestHelpers.CheckNestedCollection(_nestedCollection), Is.True); + } + } +} \ No newline at end of file diff --git a/test-suite/handwritten-src/cs/PrimitiveListTest.cs b/test-suite/handwritten-src/cs/PrimitiveListTest.cs new file mode 100644 index 0000000..20330bc --- /dev/null +++ b/test-suite/handwritten-src/cs/PrimitiveListTest.cs @@ -0,0 +1,38 @@ +using System.Collections.Generic; +using Djinni.TestSuite; +using NUnit.Framework; + +namespace Djinni.Testing.Unit +{ + [TestFixture] + public class PrimitiveListTest + { + [SetUp] + public void SetUp() + { + _primitiveList = new PrimitiveList(new List {1, 2, 3}); + } + + private PrimitiveList _primitiveList; + + [Test] + public void TestCsPrimitiveListToCpp() + { + Assert.That(() => TestHelpers.CheckPrimitiveList(_primitiveList), Is.True); + } + + [Test] + public void TestCppPrimitiveListToCs() + { + var converted = TestHelpers.GetPrimitiveList(); + Assert.That(() => converted.List, Is.EquivalentTo(_primitiveList.List)); + } + + [Test] + public void TestBinary() + { + byte[] b = {1, 2, 3}; + Assert.That(() => TestHelpers.IdBinary(b), Is.EqualTo(b)); + } + } +} \ No newline at end of file diff --git a/test-suite/handwritten-src/cs/PrimitivesTest.cs b/test-suite/handwritten-src/cs/PrimitivesTest.cs new file mode 100644 index 0000000..941ee08 --- /dev/null +++ b/test-suite/handwritten-src/cs/PrimitivesTest.cs @@ -0,0 +1,17 @@ +using Djinni.TestSuite; +using NUnit.Framework; + +namespace Djinni.Testing.Unit +{ + [TestFixture] + public class PrimitivesTest + { + [Test] + public void TestPrimitives() + { + var p = new AssortedPrimitives(true, 123, 20000, 1000000000, 1234567890123456789L, 1.23f, 1.23d, + true, 123, 20000, 1000000000, 1234567890123456789L, 1.23f, 1.23d); + Assert.That(() => TestHelpers.AssortedPrimitivesId(p), Is.EqualTo(p)); + } + } +} \ No newline at end of file diff --git a/test-suite/handwritten-src/cs/RecordWithDerivingsTest.cs b/test-suite/handwritten-src/cs/RecordWithDerivingsTest.cs new file mode 100644 index 0000000..847590a --- /dev/null +++ b/test-suite/handwritten-src/cs/RecordWithDerivingsTest.cs @@ -0,0 +1,74 @@ +using System; +using Djinni.TestSuite; +using NUnit.Framework; + +namespace Djinni.Testing.Unit +{ + [TestFixture] + public class RecordWithDerivingsTest + { + [SetUp] + public void SetUp() + { + _record1 = new RecordWithDerivings(1, 2, 3, 4, 5.0f, 6.0, new DateTime(1970, 1, 1), "String8"); + _record1A = new RecordWithDerivings(1, 2, 3, 4, 5.0f, 6.0, new DateTime(1970, 1, 1), "String8"); + _record2 = new RecordWithDerivings(1, 2, 3, 4, 5.0f, 6.0, new DateTime(1970, 1, 1), "String888"); + _record3 = new RecordWithDerivings(111, 2, 3, 4, 5.0f, 6.0, new DateTime(1970, 1, 1), "String8"); + + _nestedRecord1 = new RecordWithNestedDerivings(1, _record1); + _nestedRecord1A = new RecordWithNestedDerivings(1, _record1A); + _nestedRecord2 = new RecordWithNestedDerivings(1, _record2); + } + + private RecordWithDerivings _record1; + private RecordWithDerivings _record1A; + private RecordWithDerivings _record2; + private RecordWithDerivings _record3; + + private RecordWithNestedDerivings _nestedRecord1; + private RecordWithNestedDerivings _nestedRecord1A; + private RecordWithNestedDerivings _nestedRecord2; + + [Test] + public void TestNestedRecordEq() + { + Assert.That(() => _nestedRecord1, Is.EqualTo(_nestedRecord1A)); + Assert.That(() => _nestedRecord1, Is.Not.EqualTo(_nestedRecord2)); + Assert.That(() => _nestedRecord2, Is.Not.EqualTo(_nestedRecord1)); + } + + [Test] + public void TestNestedRecordOrd() + { + Assert.That(() => _nestedRecord1.CompareTo(_nestedRecord1A), Is.Zero); + Assert.That(() => _nestedRecord1.CompareTo(_nestedRecord2), Is.LessThan(0)); + Assert.That(() => _nestedRecord2.CompareTo(_nestedRecord1), Is.GreaterThan(0)); + } + + [Test] + public void TestRecordEq() + { + Assert.That(() => _record1, Is.EqualTo(_record1A)); + Assert.That(() => _record1A, Is.EqualTo(_record1)); + Assert.That(() => _record1, Is.Not.EqualTo(_record2)); + Assert.That(() => _record2, Is.Not.EqualTo(_record1)); + Assert.That(() => _record1, Is.Not.EqualTo(_record3)); + Assert.That(() => _record3, Is.Not.EqualTo(_record1)); + Assert.That(() => _record2, Is.Not.EqualTo(_record3)); + Assert.That(() => _record3, Is.Not.EqualTo(_record2)); + } + + [Test] + public void TestRecordOrd() + { + Assert.That(() => _record1.CompareTo(_record1A), Is.Zero); + Assert.That(() => _record1A.CompareTo(_record1), Is.Zero); + Assert.That(() => _record1.CompareTo(_record2), Is.LessThan(0)); + Assert.That(() => _record2.CompareTo(_record1), Is.GreaterThan(0)); + Assert.That(() => _record1.CompareTo(_record3), Is.LessThan(0)); + Assert.That(() => _record3.CompareTo(_record1), Is.GreaterThan(0)); + Assert.That(() => _record2.CompareTo(_record3), Is.LessThan(0)); + Assert.That(() => _record3.CompareTo(_record2), Is.GreaterThan(0)); + } + } +} \ No newline at end of file diff --git a/test-suite/handwritten-src/cs/SetRecordTest.cs b/test-suite/handwritten-src/cs/SetRecordTest.cs new file mode 100644 index 0000000..020deb3 --- /dev/null +++ b/test-suite/handwritten-src/cs/SetRecordTest.cs @@ -0,0 +1,27 @@ +using System.Collections.Generic; +using Djinni.TestSuite; +using NUnit.Framework; + +namespace Djinni.Testing.Unit +{ + [TestFixture] + public class SetRecordTest + { + [Test] + public void TestCppSetToCsSet() + { + var setRecord = TestHelpers.GetSetRecord(); + var set = setRecord.Set; + Assert.That(() => set, Is.EquivalentTo(new HashSet {"StringA", "StringB", "StringC"})); + } + + [Test] + public void TestCsSetToCppSet() + { + var sSet = new HashSet {"StringA", "StringB", "StringC"}; + var iSet = new HashSet(); + var setRecord = new SetRecord(sSet, iSet); + Assert.That(() => TestHelpers.CheckSetRecord(setRecord), Is.True); + } + } +} \ No newline at end of file diff --git a/test-suite/handwritten-src/cs/TokenTest.cs b/test-suite/handwritten-src/cs/TokenTest.cs new file mode 100644 index 0000000..7286551 --- /dev/null +++ b/test-suite/handwritten-src/cs/TokenTest.cs @@ -0,0 +1,53 @@ +using Djinni.TestSuite; +using NUnit.Framework; + +namespace Djinni.Testing.Unit +{ + [TestFixture] + public class TokenTest + { + private class CsToken : UserToken + { + public override string Whoami() + { + return "C#"; + } + } + + [Test] + public void TestTokens() + { + UserToken token = new CsToken(); + Assert.That(TestHelpers.TokenId(token), Is.SameAs(token)); + } + + [Test] + public void TestNullToken() + { + Assert.That(TestHelpers.TokenId(null), Is.EqualTo(null)); + } + + [Test] + public void TestCppToken() + { + var token = TestHelpers.CreateCppToken(); + Assert.That(TestHelpers.TokenId(token), Is.SameAs(token)); + Assert.That(() => TestHelpers.CheckCppToken(token), Throws.Nothing); + } + + [Test] + public void TestTokenType() + { + Assert.That(() => TestHelpers.CheckTokenType(new CsToken(), "C#"), Throws.Nothing); + Assert.That(() => TestHelpers.CheckTokenType(TestHelpers.CreateCppToken(), "C++"), Throws.Nothing); + Assert.That(() => TestHelpers.CheckTokenType(new CsToken(), "foo"), Throws.Exception); + Assert.That(() => TestHelpers.CheckTokenType(TestHelpers.CreateCppToken(), "foo"), Throws.Exception); + } + + [Test] + public void TestNotCppToken() + { + Assert.That(() => TestHelpers.CheckCppToken(new CsToken()), Throws.Exception); + } +} +} \ No newline at end of file diff --git a/test-suite/handwritten-src/cs/WcharTest.cs b/test-suite/handwritten-src/cs/WcharTest.cs new file mode 100644 index 0000000..bdd884a --- /dev/null +++ b/test-suite/handwritten-src/cs/WcharTest.cs @@ -0,0 +1,21 @@ +using Djinni.TestSuite; +using NUnit.Framework; + +namespace Djinni.Testing.Unit +{ + [TestFixture] + public class WcharTest + { + private const string Str1 = "some string with unicode \u0000, \u263A, \uD83D\uDCA9 symbols"; + private const string Str2 = "another string with unicode \u263B, \uD83D\uDCA8 symbols"; + + [Test] + public void Test() + { + Assert.That(WcharTestHelpers.GetRecord().S, Is.EqualTo(Str1)); + Assert.That(WcharTestHelpers.GetString(), Is.EqualTo(Str2)); + Assert.That(WcharTestHelpers.CheckString(Str2), Is.True); + Assert.That(WcharTestHelpers.CheckRecord(new WcharTestRec(Str1)), Is.True); + } + } +} \ No newline at end of file diff --git a/test/cppcli_list.txt b/test/cppcli_list.txt new file mode 100644 index 0000000..f3323fe --- /dev/null +++ b/test/cppcli_list.txt @@ -0,0 +1,13 @@ +.\include +.\lib +.\include\djinni +.\include\djinni\cppcli +.\include\djinni\proxy_cache_interface.hpp +.\include\djinni\proxy_cache_impl.hpp +.\include\djinni\djinni_common.hpp +.\include\djinni\cppcli\Marshal.hpp +.\include\djinni\cppcli\AutoPtr.hpp +.\include\djinni\cppcli\Error.hpp +.\include\djinni\cppcli\Assert.hpp +.\include\djinni\cppcli\WrapperCache.hpp +.\lib\djinni_support_lib.lib