diff --git a/shared/jni/setupBuild.gradle b/shared/jni/setupBuild.gradle index b3f2de0b75..35f4cf2d06 100644 --- a/shared/jni/setupBuild.gradle +++ b/shared/jni/setupBuild.gradle @@ -16,8 +16,10 @@ ext { apply from: "${rootDir}/shared/java/javacommon.gradle" dependencies { - compile project(':wpiutil') - devCompile project(':wpiutil') + if (!project.hasProperty('noWpiutil')) { + compile project(':wpiutil') + devCompile project(':wpiutil') + } } project(':').libraryBuild.dependsOn build @@ -69,7 +71,9 @@ model { it.buildable = false return } - lib project: ':wpiutil', library: 'wpiutil', linkage: 'shared' + if (!project.hasProperty('noWpiutil')) { + lib project: ':wpiutil', library: 'wpiutil', linkage: 'shared' + } if (project.hasProperty('splitSetup')) { splitSetup(it) } @@ -93,8 +97,10 @@ model { } } } - binaries.all { - lib project: ':wpiutil', library: 'wpiutil', linkage: 'shared' + if (!project.hasProperty('noWpiutil')) { + binaries.all { + lib project: ':wpiutil', library: 'wpiutil', linkage: 'shared' + } } appendDebugPathToBinaries(binaries) } @@ -132,7 +138,9 @@ model { return } lib library: "${nativeName}", linkage: 'shared' - lib project: ':wpiutil', library: 'wpiutil', linkage: 'shared' + if (!project.hasProperty('noWpiutil')) { + lib project: ':wpiutil', library: 'wpiutil', linkage: 'shared' + } if (project.hasProperty('jniSplitSetup')) { jniSplitSetup(it) } @@ -170,7 +178,9 @@ model { it.buildable = false return } - lib project: ':wpiutil', library: 'wpiutil', linkage: 'static' + if (!project.hasProperty('noWpiutil')) { + lib project: ':wpiutil', library: 'wpiutil', linkage: 'static' + } if (project.hasProperty('jniSplitSetup')) { jniSplitSetup(it) } @@ -198,7 +208,9 @@ model { binaries.all { lib library: nativeName, linkage: 'shared' lib library: "${nativeName}JNIShared", linkage: 'shared' - lib project: ':wpiutil', library: 'wpiutil', linkage: 'shared' + if (!project.hasProperty('noWpiutil')) { + lib project: ':wpiutil', library: 'wpiutil', linkage: 'shared' + } } } } @@ -229,7 +241,9 @@ model { binaries { withType(GoogleTestTestSuiteBinarySpec) { lib library: nativeName, linkage: 'shared' - lib project: ':wpiutil', library: 'wpiutil', linkage: 'shared' + if (!project.hasProperty('noWpiutil')) { + lib project: ':wpiutil', library: 'wpiutil', linkage: 'shared' + } } } tasks { diff --git a/wpiutil/CMakeLists.txt b/wpiutil/CMakeLists.txt index 171cb801af..0ec1724198 100644 --- a/wpiutil/CMakeLists.txt +++ b/wpiutil/CMakeLists.txt @@ -5,9 +5,12 @@ include(GenResources) include(CompileWarnings) include(AddTest) +file(GLOB wpiutil_jni_src src/main/native/cpp/jni/WPIUtilJNI.cpp) + # Java bindings if (NOT WITHOUT_JAVA) - find_package(Java) + find_package(Java REQUIRED) + find_package(JNI REQUIRED) include(UseJava) set(CMAKE_JAVA_COMPILE_FLAGS "-Xlint:unchecked") @@ -42,14 +45,42 @@ if (NOT WITHOUT_JAVA) execute_process(COMMAND python3 ${CMAKE_SOURCE_DIR}/wpiutil/generate_numbers.py ${CMAKE_BINARY_DIR}/wpiutil) + set(CMAKE_JNI_TARGET true) + file(GLOB_RECURSE JAVA_SOURCES src/main/java/*.java ${CMAKE_BINARY_DIR}/wpiutil/generated/*.java) - add_jar(wpiutil_jar ${JAVA_SOURCES} INCLUDE_JARS ${EJML_JARS} OUTPUT_NAME wpiutil) + + if(${CMAKE_VERSION} VERSION_LESS "3.11.0") + set(CMAKE_JAVA_COMPILE_FLAGS "-h" "${CMAKE_CURRENT_BINARY_DIR}/jniheaders") + add_jar(wpiutil_jar ${JAVA_SOURCES} INCLUDE_JARS ${EJML_JARS} OUTPUT_NAME wpiutil) + else() + add_jar(wpiutil_jar ${JAVA_SOURCES} INCLUDE_JARS ${EJML_JARS} OUTPUT_NAME wpiutil GENERATE_NATIVE_HEADERS wpiutil_jni_headers) + endif() get_property(WPIUTIL_JAR_FILE TARGET wpiutil_jar PROPERTY JAR_FILE) install(FILES ${WPIUTIL_JAR_FILE} DESTINATION "${java_lib_dest}") set_property(TARGET wpiutil_jar PROPERTY FOLDER "java") + add_library(wpiutiljni ${wpiutil_jni_src}) + wpilib_target_warnings(wpiutiljni) + target_link_libraries(wpiutiljni PUBLIC wpiutil) + + set_property(TARGET wpiutiljni PROPERTY FOLDER "libraries") + + if(${CMAKE_VERSION} VERSION_LESS "3.11.0") + target_include_directories(wpiutiljni PRIVATE ${JNI_INCLUDE_DIRS}) + target_include_directories(wpiutiljni PRIVATE "${CMAKE_CURRENT_BINARY_DIR}/jniheaders") + else() + target_link_libraries(wpiutiljni PRIVATE wpiutil_jni_headers) + endif() + add_dependencies(wpiutiljni wpiutil_jar) + + if (MSVC) + install(TARGETS wpiutiljni RUNTIME DESTINATION "${jni_lib_dest}" COMPONENT Runtime) + endif() + + install(TARGETS wpiutiljni EXPORT wpiutiljni DESTINATION "${main_lib_dest}") + endif() set(THREADS_PREFER_PTHREAD_FLAG ON) @@ -65,6 +96,7 @@ endif() GENERATE_RESOURCES(src/main/native/resources generated/main/cpp WPI wpi wpiutil_resources_src) file(GLOB_RECURSE wpiutil_native_src src/main/native/cpp/*.cpp) +list(REMOVE_ITEM wpiutil_native_src ${wpiutil_jni_src}) file(GLOB_RECURSE wpiutil_unix_src src/main/native/unix/*.cpp) file(GLOB_RECURSE wpiutil_windows_src src/main/native/windows/*.cpp) diff --git a/wpiutil/build.gradle b/wpiutil/build.gradle index ef9d5589e6..588316ef8b 100644 --- a/wpiutil/build.gradle +++ b/wpiutil/build.gradle @@ -1,6 +1,7 @@ apply from: "${rootDir}/shared/resources.gradle" ext { + noWpiutil = true baseId = 'wpiutil' groupId = 'edu.wpi.first.wpiutil' @@ -8,7 +9,7 @@ ext { devMain = 'edu.wpi.first.wpiutil.DevMain' def generateTask = createGenerateResourcesTask('main', 'WPI', 'wpi', project) - extraSetup = { + splitSetup = { it.tasks.withType(CppCompile) { dependsOn generateTask } @@ -152,7 +153,7 @@ file("$projectDir/examples").list(new FilenameFilter() { examplesMap.put(it, []) } -apply from: "${rootDir}/shared/javacpp/setupBuild.gradle" +apply from: "${rootDir}/shared/jni/setupBuild.gradle" nativeUtils.exportsConfigs { wpiutil { diff --git a/wpiutil/src/main/java/edu/wpi/first/wpiutil/WPIUtilJNI.java b/wpiutil/src/main/java/edu/wpi/first/wpiutil/WPIUtilJNI.java new file mode 100644 index 0000000000..941e691d78 --- /dev/null +++ b/wpiutil/src/main/java/edu/wpi/first/wpiutil/WPIUtilJNI.java @@ -0,0 +1,56 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2019 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +package edu.wpi.first.wpiutil; + +import java.io.IOException; +import java.util.concurrent.atomic.AtomicBoolean; + +public final class WPIUtilJNI { + static boolean libraryLoaded = false; + static RuntimeLoader loader = null; + + public static class Helper { + private static AtomicBoolean extractOnStaticLoad = new AtomicBoolean(true); + + public static boolean getExtractOnStaticLoad() { + return extractOnStaticLoad.get(); + } + + public static void setExtractOnStaticLoad(boolean load) { + extractOnStaticLoad.set(load); + } + } + + static { + if (Helper.getExtractOnStaticLoad()) { + try { + loader = new RuntimeLoader<>("wpiutiljni", RuntimeLoader.getDefaultExtractionRoot(), WPIUtilJNI.class); + loader.loadLibrary(); + } catch (IOException ex) { + ex.printStackTrace(); + System.exit(1); + } + libraryLoaded = true; + } + } + + /** + * Force load the library. + */ + public static synchronized void forceLoad() throws IOException { + if (libraryLoaded) { + return; + } + loader = new RuntimeLoader<>("wpiutiljni", RuntimeLoader.getDefaultExtractionRoot(), WPIUtilJNI.class); + loader.loadLibrary(); + libraryLoaded = true; + } + + public static native void addPortForwarder(int port, String remoteHost, int remotePort); + public static native void removePortForwarder(int port); +} diff --git a/wpiutil/src/main/java/edu/wpi/first/wpiutil/net/PortForwarder.java b/wpiutil/src/main/java/edu/wpi/first/wpiutil/net/PortForwarder.java new file mode 100644 index 0000000000..a67b064293 --- /dev/null +++ b/wpiutil/src/main/java/edu/wpi/first/wpiutil/net/PortForwarder.java @@ -0,0 +1,39 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2019 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +package edu.wpi.first.wpiutil; + +/** + * Forward ports to another host. This is primarily useful for accessing + * Ethernet-connected devices from a computer tethered to the RoboRIO USB port. + */ +public final class PortForwarder { + private PortForwarder() { + throw new UnsupportedOperationException("This is a utility class!"); + } + + /** + * Forward a local TCP port to a remote host and port. + * Note that local ports less than 1024 won't work as a normal user. + * + * @param port local port number + * @param remoteHost remote IP address / DNS name + * @param remotePort remote port number + */ + public static void add(int port, String remoteHost, int remotePort) { + WPIUtilJNI.addPortForwarder(port, remoteHost, remotePort); + } + + /** + * Stop TCP forwarding on a port. + * + * @param port local port number + */ + public static void remove(int port) { + WPIUtilJNI.removePortForwarder(port); + } +} diff --git a/wpiutil/src/main/native/cpp/PortForwarder.cpp b/wpiutil/src/main/native/cpp/PortForwarder.cpp new file mode 100644 index 0000000000..fe54b63599 --- /dev/null +++ b/wpiutil/src/main/native/cpp/PortForwarder.cpp @@ -0,0 +1,150 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2019 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +#include "wpi/PortForwarder.h" + +#include "wpi/DenseMap.h" +#include "wpi/EventLoopRunner.h" +#include "wpi/SmallString.h" +#include "wpi/raw_ostream.h" +#include "wpi/uv/GetAddrInfo.h" +#include "wpi/uv/Tcp.h" +#include "wpi/uv/Timer.h" + +using namespace wpi; + +struct PortForwarder::Impl { + public: + EventLoopRunner runner; + DenseMap> servers; +}; + +PortForwarder::PortForwarder() : m_impl{new Impl} {} + +PortForwarder& PortForwarder::GetInstance() { + static PortForwarder instance; + return instance; +} + +static void CopyStream(uv::Stream& in, std::weak_ptr outWeak) { + in.data.connect([&in, outWeak](uv::Buffer& buf, size_t len) { + uv::Buffer buf2 = buf.Dup(); + buf2.len = len; + auto out = outWeak.lock(); + if (!out) { + in.Close(); + return; + } + out->Write(buf2, [](auto bufs, uv::Error) { + for (auto buf : bufs) buf.Deallocate(); + }); + }); +} + +void PortForwarder::Add(unsigned int port, const Twine& remoteHost, + unsigned int remotePort) { + m_impl->runner.ExecSync([&](uv::Loop& loop) { + auto server = uv::Tcp::Create(loop); + + // bind to local port + server->Bind("", port); + + // when we get a connection, accept it + server->connection.connect([serverPtr = server.get(), + host = remoteHost.str(), remotePort] { + auto& loop = serverPtr->GetLoopRef(); + auto client = serverPtr->Accept(); + if (!client) return; + + // close on error + client->error.connect( + [clientPtr = client.get()](uv::Error err) { clientPtr->Close(); }); + + // connected flag + auto connected = std::make_shared(false); + client->SetData(connected); + + auto remote = uv::Tcp::Create(loop); + remote->error.connect( + [remotePtr = remote.get(), + clientWeak = std::weak_ptr(client)](uv::Error err) { + remotePtr->Close(); + if (auto client = clientWeak.lock()) client->Close(); + }); + + // convert port to string + SmallString<16> remotePortStr; + raw_svector_ostream(remotePortStr) << remotePort; + + // resolve address + uv::GetAddrInfo( + loop, + [clientWeak = std::weak_ptr(client), + remoteWeak = std::weak_ptr(remote)](const addrinfo& addr) { + auto remote = remoteWeak.lock(); + if (!remote) return; + + // connect to remote address/port + remote->Connect(*addr.ai_addr, [remotePtr = remote.get(), + remoteWeak, clientWeak] { + auto client = clientWeak.lock(); + if (!client) { + remotePtr->Close(); + return; + } + *(client->GetData()) = true; + + // close both when either side closes + client->end.connect([clientPtr = client.get(), remoteWeak] { + clientPtr->Close(); + if (auto remote = remoteWeak.lock()) remote->Close(); + }); + remotePtr->end.connect([remotePtr, clientWeak] { + remotePtr->Close(); + if (auto client = clientWeak.lock()) client->Close(); + }); + + // copy bidirectionally + client->StartRead(); + remotePtr->StartRead(); + CopyStream(*client, remoteWeak); + CopyStream(*remotePtr, clientWeak); + }); + }, + host, remotePortStr); + + // time out for connection + uv::Timer::SingleShot(loop, uv::Timer::Time{500}, + [connectedWeak = std::weak_ptr(connected), + clientWeak = std::weak_ptr(client), + remoteWeak = std::weak_ptr(remote)] { + if (auto connected = connectedWeak.lock()) { + if (!*connected) { + if (auto client = clientWeak.lock()) + client->Close(); + if (auto remote = remoteWeak.lock()) + remote->Close(); + } + } + }); + }); + + // start listening for incoming connections + server->Listen(); + + m_impl->servers[port] = server; + }); +} + +void PortForwarder::Remove(unsigned int port) { + m_impl->runner.ExecSync([&](uv::Loop& loop) { + if (auto server = m_impl->servers.lookup(port).lock()) { + server->Close(); + m_impl->servers.erase(port); + } + }); +} diff --git a/wpiutil/src/main/native/cpp/jni/WPIUtilJNI.cpp b/wpiutil/src/main/native/cpp/jni/WPIUtilJNI.cpp new file mode 100644 index 0000000000..c14b3f4c94 --- /dev/null +++ b/wpiutil/src/main/native/cpp/jni/WPIUtilJNI.cpp @@ -0,0 +1,54 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2019 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +#include + +#include "edu_wpi_first_wpiutil_WPIUtilJNI.h" +#include "wpi/PortForwarder.h" +#include "wpi/jni_util.h" + +using namespace wpi::java; + +extern "C" { + +JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved) { + JNIEnv* env; + if (vm->GetEnv(reinterpret_cast(&env), JNI_VERSION_1_6) != JNI_OK) + return JNI_ERR; + + return JNI_VERSION_1_6; +} + +JNIEXPORT void JNICALL JNI_OnUnload(JavaVM* vm, void* reserved) {} + +/* + * Class: edu_wpi_first_wpiutil_WPIUtilJNI + * Method: addPortForwarder + * Signature: (ILjava/lang/String;I)V + */ +JNIEXPORT void JNICALL +Java_edu_wpi_first_wpiutil_WPIUtilJNI_addPortForwarder + (JNIEnv* env, jclass, jint port, jstring remoteHost, jint remotePort) +{ + wpi::PortForwarder::GetInstance().Add(static_cast(port), + JStringRef{env, remoteHost}.str(), + static_cast(remotePort)); +} + +/* + * Class: edu_wpi_first_wpiutil_WPIUtilJNI + * Method: removePortForwarder + * Signature: (I)V + */ +JNIEXPORT void JNICALL +Java_edu_wpi_first_wpiutil_WPIUtilJNI_removePortForwarder + (JNIEnv* env, jclass, jint port) +{ + wpi::PortForwarder::GetInstance().Remove(port); +} + +} // extern "C" diff --git a/wpiutil/src/main/native/include/wpi/PortForwarder.h b/wpiutil/src/main/native/include/wpi/PortForwarder.h new file mode 100644 index 0000000000..9ee6ce670a --- /dev/null +++ b/wpiutil/src/main/native/include/wpi/PortForwarder.h @@ -0,0 +1,62 @@ +/*----------------------------------------------------------------------------*/ +/* Copyright (c) 2019 FIRST. All Rights Reserved. */ +/* Open Source Software - may be modified and shared by FRC teams. The code */ +/* must be accompanied by the FIRST BSD license file in the root directory of */ +/* the project. */ +/*----------------------------------------------------------------------------*/ + +#ifndef WPIUTIL_WPI_PORTFORWARDER_H_ +#define WPIUTIL_WPI_PORTFORWARDER_H_ + +#pragma once + +#include + +#include "wpi/Twine.h" + +namespace wpi { + +/** + * Forward ports to another host. This is primarily useful for accessing + * Ethernet-connected devices from a computer tethered to the RoboRIO USB port. + */ +class PortForwarder { + public: + PortForwarder(const PortForwarder&) = delete; + PortForwarder& operator=(const PortForwarder&) = delete; + + /** + * Get an instance of the PortForwarder class. + * + * This is a singleton to guarantee that there is only a single instance + * regardless of how many times GetInstance is called. + */ + static PortForwarder& GetInstance(); + + /** + * Forward a local TCP port to a remote host and port. + * Note that local ports less than 1024 won't work as a normal user. + * + * @param port local port number + * @param remoteHost remote IP address / DNS name + * @param remotePort remote port number + */ + void Add(unsigned int port, const Twine& remoteHost, unsigned int remotePort); + + /** + * Stop TCP forwarding on a port. + * + * @param port local port number + */ + void Remove(unsigned int port); + + private: + PortForwarder(); + + struct Impl; + std::unique_ptr m_impl; +}; + +} // namespace wpi + +#endif // WPIUTIL_WPI_PORTFORWARDER_H_