From b03bc8948258fe15ae7695cf6241aef412489fcd Mon Sep 17 00:00:00 2001 From: Michael Simons Date: Fri, 27 Sep 2019 17:28:39 +0200 Subject: [PATCH] Add support for native compilation. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit adds the following: - Oracle SVM, part of the Graal VM that allows substitutes for classes and methods, deleting them and aliasing them on the GraalVM. - Substitutions to make Netty work natively with SSL. Those substitutions live under internal/svm and are not visible when used outside GraalVM The PR also removes Nettys own native-image properties and replaces them with the setting needed for our driver. Those settings cannot be easily rewritten using the Shade transformer, so it’s more simple to do it by hand. The changes have been tested sucessfully on Graal 19.2.0.1 with Quarkus and Helidon microservices framework compiled natively. --- driver/pom.xml | 12 + .../internal/svm/NettySubstitutions.java | 312 ++++++++++++++++++ .../internal/svm/ZLibSubstitutions.java | 88 +++++ .../org.neo4j/driver/native-image.properties | 18 + .../org.neo4j/driver/reflection-config.json | 22 ++ pom.xml | 7 + 6 files changed, 459 insertions(+) create mode 100644 driver/src/main/java/org/neo4j/driver/internal/svm/NettySubstitutions.java create mode 100644 driver/src/main/java/org/neo4j/driver/internal/svm/ZLibSubstitutions.java create mode 100644 driver/src/main/resources/META-INF/native-image/org.neo4j/driver/native-image.properties create mode 100644 driver/src/main/resources/META-INF/native-image/org.neo4j/driver/reflection-config.json diff --git a/driver/pom.xml b/driver/pom.xml index e4b17991a2..98ceab6bb7 100644 --- a/driver/pom.xml +++ b/driver/pom.xml @@ -78,6 +78,10 @@ io.projectreactor reactor-test + + com.oracle.substratevm + svm + @@ -232,6 +236,14 @@ + + + io.netty:* + + META-INF/native-image/** + + + true true diff --git a/driver/src/main/java/org/neo4j/driver/internal/svm/NettySubstitutions.java b/driver/src/main/java/org/neo4j/driver/internal/svm/NettySubstitutions.java new file mode 100644 index 0000000000..113b4aa304 --- /dev/null +++ b/driver/src/main/java/org/neo4j/driver/internal/svm/NettySubstitutions.java @@ -0,0 +1,312 @@ +/* + * Copyright (c) 2002-2019 "Neo4j," + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.neo4j.driver.internal.svm; + +import com.oracle.svm.core.annotate.Alias; +import com.oracle.svm.core.annotate.Substitute; +import com.oracle.svm.core.annotate.TargetClass; +import com.oracle.svm.core.jdk.JDK11OrLater; + +import java.security.PrivateKey; +import java.security.Provider; +import java.security.cert.X509Certificate; +import java.util.Queue; +import java.util.concurrent.LinkedBlockingDeque; +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLException; +import javax.net.ssl.TrustManagerFactory; + +import io.netty.bootstrap.AbstractBootstrapConfig; +import io.netty.bootstrap.ChannelFactory; +import io.netty.buffer.ByteBufAllocator; +import io.netty.channel.Channel; +import io.netty.channel.ChannelFuture; +import io.netty.channel.DefaultChannelPromise; +import io.netty.handler.ssl.ApplicationProtocolConfig; +import io.netty.handler.ssl.ApplicationProtocolConfig.SelectorFailureBehavior; +import io.netty.handler.ssl.CipherSuiteFilter; +import io.netty.handler.ssl.ClientAuth; +import io.netty.handler.ssl.JdkAlpnApplicationProtocolNegotiator; +import io.netty.handler.ssl.JdkApplicationProtocolNegotiator; +import io.netty.handler.ssl.SslContext; +import io.netty.handler.ssl.SslProvider; +import io.netty.util.concurrent.GlobalEventExecutor; +import io.netty.util.internal.logging.InternalLoggerFactory; +import io.netty.util.internal.logging.JdkLoggerFactory; + +/** + * This substitution avoid having loggers added to the build + */ +@TargetClass(className = "io.netty.util.internal.logging.InternalLoggerFactory") +final class Target_io_netty_util_internal_logging_InternalLoggerFactory { + + @Substitute + private static InternalLoggerFactory newDefaultFactory( String name) { + return JdkLoggerFactory.INSTANCE; + } +} + +// SSL +// This whole section is mostly about removing static analysis references to openssl/tcnative + +@TargetClass(className = "io.netty.handler.ssl.JdkSslServerContext") +final class Target_io_netty_handler_ssl_JdkSslServerContext { + + @Alias + Target_io_netty_handler_ssl_JdkSslServerContext( Provider provider, + X509Certificate[] trustCertCollection, TrustManagerFactory trustManagerFactory, + X509Certificate[] keyCertChain, PrivateKey key, String keyPassword, + KeyManagerFactory keyManagerFactory, Iterable ciphers, CipherSuiteFilter cipherFilter, + ApplicationProtocolConfig apn, long sessionCacheSize, long sessionTimeout, + ClientAuth clientAuth, String[] protocols, boolean startTls, + String keyStore) + throws SSLException + { + } +} + +@TargetClass(className = "io.netty.handler.ssl.JdkSslClientContext") +final class Target_io_netty_handler_ssl_JdkSslClientContext { + + @Alias + Target_io_netty_handler_ssl_JdkSslClientContext( Provider sslContextProvider, + X509Certificate[] trustCertCollection, + TrustManagerFactory trustManagerFactory, X509Certificate[] keyCertChain, PrivateKey key, + String keyPassword, KeyManagerFactory keyManagerFactory, Iterable ciphers, + CipherSuiteFilter cipherFilter, ApplicationProtocolConfig apn, String[] protocols, + long sessionCacheSize, long sessionTimeout, String keyStoreType) + throws SSLException + { + + } +} + +@TargetClass(className = "io.netty.handler.ssl.SslHandler$SslEngineType") +final class Target_io_netty_handler_ssl_SslHandler$SslEngineType { + + @Alias + public static Target_io_netty_handler_ssl_SslHandler$SslEngineType JDK; + + @Substitute + static Target_io_netty_handler_ssl_SslHandler$SslEngineType forEngine( SSLEngine engine) { + return JDK; + } +} + +@TargetClass(className = "io.netty.handler.ssl.JdkAlpnApplicationProtocolNegotiator$AlpnWrapper", onlyWith = JDK11OrLater.class) +final class Target_io_netty_handler_ssl_JdkAlpnApplicationProtocolNegotiator_AlpnWrapper { + @Substitute + @SuppressWarnings( "deprecation" ) + public SSLEngine wrapSslEngine( SSLEngine engine, ByteBufAllocator alloc, + JdkApplicationProtocolNegotiator applicationNegotiator, boolean isServer) { + return (SSLEngine) (Object) new Target_io_netty_handler_ssl_Java9SslEngine( + engine, applicationNegotiator, isServer); + } + +} + +@TargetClass(className = "io.netty.handler.ssl.Java9SslEngine", onlyWith = JDK11OrLater.class) +final class Target_io_netty_handler_ssl_Java9SslEngine { + @Alias + @SuppressWarnings( "deprecation" ) + Target_io_netty_handler_ssl_Java9SslEngine(final SSLEngine engine, + final JdkApplicationProtocolNegotiator applicationNegotiator, final boolean isServer) { + + } +} + +@TargetClass(className = "io.netty.handler.ssl.SslContext") +final class Target_io_netty_handler_ssl_SslContext { + + @Substitute + static SslContext newServerContextInternal(SslProvider provider, + Provider sslContextProvider, + X509Certificate[] trustCertCollection, TrustManagerFactory trustManagerFactory, + X509Certificate[] keyCertChain, PrivateKey key, String keyPassword, KeyManagerFactory keyManagerFactory, + Iterable ciphers, CipherSuiteFilter cipherFilter, ApplicationProtocolConfig apn, + long sessionCacheSize, long sessionTimeout, ClientAuth clientAuth, String[] protocols, boolean startTls, + boolean enableOcsp, String keyStoreType) + throws SSLException + { + + if (enableOcsp) { + throw new IllegalArgumentException("OCSP is not supported with this SslProvider: " + provider); + } + return (SslContext) (Object) new Target_io_netty_handler_ssl_JdkSslServerContext( + sslContextProvider, + trustCertCollection, trustManagerFactory, keyCertChain, key, keyPassword, + keyManagerFactory, ciphers, cipherFilter, apn, sessionCacheSize, sessionTimeout, + clientAuth, protocols, startTls, keyStoreType); + } + + @Substitute + static SslContext newClientContextInternal( + SslProvider provider, + Provider sslContextProvider, + X509Certificate[] trustCert, TrustManagerFactory trustManagerFactory, + X509Certificate[] keyCertChain, PrivateKey key, String keyPassword, KeyManagerFactory keyManagerFactory, + Iterable ciphers, CipherSuiteFilter cipherFilter, ApplicationProtocolConfig apn, String[] protocols, + long sessionCacheSize, long sessionTimeout, boolean enableOcsp, String keyStoreType) throws SSLException + { + if (enableOcsp) { + throw new IllegalArgumentException("OCSP is not supported with this SslProvider: " + provider); + } + return (SslContext) (Object) new Target_io_netty_handler_ssl_JdkSslClientContext( + sslContextProvider, + trustCert, trustManagerFactory, keyCertChain, key, keyPassword, + keyManagerFactory, ciphers, cipherFilter, apn, protocols, sessionCacheSize, + sessionTimeout, keyStoreType); + } + +} + +@TargetClass(className = "io.netty.handler.ssl.JdkDefaultApplicationProtocolNegotiator") +final class Target_io_netty_handler_ssl_JdkDefaultApplicationProtocolNegotiator { + + @Alias + public static Target_io_netty_handler_ssl_JdkDefaultApplicationProtocolNegotiator INSTANCE; +} + +@TargetClass(className = "io.netty.handler.ssl.JdkSslContext") +final class Target_io_netty_handler_ssl_JdkSslContext { + + @Substitute + static JdkApplicationProtocolNegotiator toNegotiator(ApplicationProtocolConfig config, boolean isServer) { + if (config == null) { + return (JdkApplicationProtocolNegotiator) (Object) Target_io_netty_handler_ssl_JdkDefaultApplicationProtocolNegotiator.INSTANCE; + } + + switch (config.protocol()) { + case NONE: + return (JdkApplicationProtocolNegotiator) (Object) Target_io_netty_handler_ssl_JdkDefaultApplicationProtocolNegotiator.INSTANCE; + case ALPN: + if (isServer) { + // GRAAL RC9 bug: https://github.com/oracle/graal/issues/813 + // switch(config.selectorFailureBehavior()) { + // case FATAL_ALERT: + // return new JdkAlpnApplicationProtocolNegotiator(true, config.supportedProtocols()); + // case NO_ADVERTISE: + // return new JdkAlpnApplicationProtocolNegotiator(false, config.supportedProtocols()); + // default: + // throw new UnsupportedOperationException(new StringBuilder("JDK provider does not support ") + // .append(config.selectorFailureBehavior()).append(" failure behavior").toString()); + // } + SelectorFailureBehavior behavior = config.selectorFailureBehavior(); + if (behavior == SelectorFailureBehavior.FATAL_ALERT) + return new JdkAlpnApplicationProtocolNegotiator(true, config.supportedProtocols()); + else if (behavior == SelectorFailureBehavior.NO_ADVERTISE) + return new JdkAlpnApplicationProtocolNegotiator(false, config.supportedProtocols()); + else { + throw new UnsupportedOperationException(new StringBuilder("JDK provider does not support ") + .append(config.selectorFailureBehavior()).append(" failure behavior").toString()); + } + } else { + switch (config.selectedListenerFailureBehavior()) { + case ACCEPT: + return new JdkAlpnApplicationProtocolNegotiator(false, config.supportedProtocols()); + case FATAL_ALERT: + return new JdkAlpnApplicationProtocolNegotiator(true, config.supportedProtocols()); + default: + throw new UnsupportedOperationException(new StringBuilder("JDK provider does not support ") + .append(config.selectedListenerFailureBehavior()).append(" failure behavior") + .toString()); + } + } + default: + throw new UnsupportedOperationException( + new StringBuilder("JDK provider does not support ").append(config.protocol()).append(" protocol") + .toString()); + } + } + +} + +/* + * This one only prints exceptions otherwise we get a useless bogus + * exception message: https://github.com/eclipse-vertx/vert.x/issues/1657 + */ +@TargetClass(className = "io.netty.bootstrap.AbstractBootstrap") +final class Target_io_netty_bootstrap_AbstractBootstrap { + + @Alias + private ChannelFactory channelFactory; + + @Alias + void init(Channel channel) throws Exception + { + } + + @Alias + public AbstractBootstrapConfig config() { + return null; + } + + @Substitute + final ChannelFuture initAndRegister() { + Channel channel = null; + try { + channel = channelFactory.newChannel(); + init(channel); + } catch ( Throwable t) { + // THE FIX IS HERE: + t.printStackTrace(); + if (channel != null) { + // channel can be null if newChannel crashed (eg SocketException("too many open files")) + channel.unsafe().closeForcibly(); + } + // as the Channel is not registered yet we need to force the usage of the GlobalEventExecutor + return new DefaultChannelPromise(channel, GlobalEventExecutor.INSTANCE).setFailure(t); + } + + ChannelFuture regFuture = config().group().register(channel); + if (regFuture.cause() != null) { + if (channel.isRegistered()) { + channel.close(); + } else { + channel.unsafe().closeForcibly(); + } + } + + // If we are here and the promise is not failed, it's one of the following cases: + // 1) If we attempted registration from the event loop, the registration has been completed at this point. + // i.e. It's safe to attempt bind() or connect() now because the channel has been registered. + // 2) If we attempted registration from the other thread, the registration request has been successfully + // added to the event loop's task queue for later execution. + // i.e. It's safe to attempt bind() or connect() now: + // because bind() or connect() will be executed *after* the scheduled registration task is executed + // because register(), bind(), and connect() are all bound to the same thread. + + return regFuture; + + } +} + +@TargetClass(className = "io.netty.channel.nio.NioEventLoop") +final class Target_io_netty_channel_nio_NioEventLoop { + + @Substitute + private static Queue newTaskQueue0(int maxPendingTasks) { + return new LinkedBlockingDeque<>(); + } +} + +class NettySubstitutions { + +} diff --git a/driver/src/main/java/org/neo4j/driver/internal/svm/ZLibSubstitutions.java b/driver/src/main/java/org/neo4j/driver/internal/svm/ZLibSubstitutions.java new file mode 100644 index 0000000000..45ff698453 --- /dev/null +++ b/driver/src/main/java/org/neo4j/driver/internal/svm/ZLibSubstitutions.java @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2002-2019 "Neo4j," + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.neo4j.driver.internal.svm; + +import com.oracle.svm.core.annotate.Substitute; +import com.oracle.svm.core.annotate.TargetClass; +import io.netty.handler.codec.compression.JdkZlibDecoder; +import io.netty.handler.codec.compression.JdkZlibEncoder; +import io.netty.handler.codec.compression.ZlibDecoder; +import io.netty.handler.codec.compression.ZlibEncoder; +import io.netty.handler.codec.compression.ZlibWrapper; + +/** + * This substitution avoid having jcraft zlib added to the build + */ +@TargetClass(className = "io.netty.handler.codec.compression.ZlibCodecFactory") +final class Target_org_neo4j_driver_internal_shaded_io_netty_handler_codec_compression_ZlibCodecFactory { + + @Substitute + public static ZlibEncoder newZlibEncoder(int compressionLevel) { + return new JdkZlibEncoder(compressionLevel); + } + + @Substitute + public static ZlibEncoder newZlibEncoder( ZlibWrapper wrapper) { + return new JdkZlibEncoder(wrapper); + } + + @Substitute + public static ZlibEncoder newZlibEncoder( ZlibWrapper wrapper, int compressionLevel) { + return new JdkZlibEncoder(wrapper, compressionLevel); + } + + @Substitute + public static ZlibEncoder newZlibEncoder( ZlibWrapper wrapper, int compressionLevel, int windowBits, int memLevel) { + return new JdkZlibEncoder(wrapper, compressionLevel); + } + + @Substitute + public static ZlibEncoder newZlibEncoder(byte[] dictionary) { + return new JdkZlibEncoder(dictionary); + } + + @Substitute + public static ZlibEncoder newZlibEncoder(int compressionLevel, byte[] dictionary) { + return new JdkZlibEncoder(compressionLevel, dictionary); + } + + @Substitute + public static ZlibEncoder newZlibEncoder(int compressionLevel, int windowBits, int memLevel, byte[] dictionary) { + return new JdkZlibEncoder(compressionLevel, dictionary); + } + + @Substitute + public static ZlibDecoder newZlibDecoder() { + return new JdkZlibDecoder(); + } + + @Substitute + public static ZlibDecoder newZlibDecoder( ZlibWrapper wrapper) { + return new JdkZlibDecoder(wrapper); + } + + @Substitute + public static ZlibDecoder newZlibDecoder(byte[] dictionary) { + return new JdkZlibDecoder(dictionary); + } +} + +class ZLibSubstitutions { + +} diff --git a/driver/src/main/resources/META-INF/native-image/org.neo4j/driver/native-image.properties b/driver/src/main/resources/META-INF/native-image/org.neo4j/driver/native-image.properties new file mode 100644 index 0000000000..646ac84bc9 --- /dev/null +++ b/driver/src/main/resources/META-INF/native-image/org.neo4j/driver/native-image.properties @@ -0,0 +1,18 @@ +Args = -H:ReflectionConfigurationResources=${.}/reflection-config.json \ + --initialize-at-run-time=org.neo4j.driver.internal.async.connection.BoltProtocolUtil \ + --initialize-at-run-time=org.neo4j.driver.internal.async.connection.ChannelConnectedListener \ + --initialize-at-run-time=org.neo4j.driver.internal.shaded.io.netty.buffer.AbstractReferenceCountedByteBuf \ + --initialize-at-run-time=org.neo4j.driver.internal.shaded.io.netty.buffer.ByteBufAllocator \ + --initialize-at-run-time=org.neo4j.driver.internal.shaded.io.netty.buffer.ByteBufUtil \ + --initialize-at-run-time=org.neo4j.driver.internal.shaded.io.netty.buffer.PooledByteBufAllocator \ + --initialize-at-run-time=org.neo4j.driver.internal.shaded.io.netty.buffer.UnpooledHeapByteBuf \ + --initialize-at-run-time=org.neo4j.driver.internal.shaded.io.netty.buffer.UnreleasableByteBuf \ + --initialize-at-run-time=org.neo4j.driver.internal.shaded.io.netty.handler.ssl.Conscrypt \ + --initialize-at-run-time=org.neo4j.driver.internal.shaded.io.netty.handler.ssl.ConscryptAlpnSslEngine \ + --initialize-at-run-time=org.neo4j.driver.internal.shaded.io.netty.handler.ssl.JdkNpnApplicationProtocolNegotiator \ + --initialize-at-run-time=org.neo4j.driver.internal.shaded.io.netty.handler.ssl.ReferenceCountedOpenSslEngine \ + --initialize-at-run-time=org.neo4j.driver.internal.shaded.io.netty.handler.ssl.util.ThreadLocalInsecureRandom \ + --initialize-at-run-time=org.neo4j.driver.internal.shaded.io.netty.util.AbstractReferenceCounted \ + --initialize-at-run-time=org.neo4j.driver.internal.shaded.io.netty.util.internal.logging.Log4JLogger \ + -Dio.netty.noUnsafe=true \ + -Dio.netty.leakDetection.level=DISABLED diff --git a/driver/src/main/resources/META-INF/native-image/org.neo4j/driver/reflection-config.json b/driver/src/main/resources/META-INF/native-image/org.neo4j/driver/reflection-config.json new file mode 100644 index 0000000000..869951c69f --- /dev/null +++ b/driver/src/main/resources/META-INF/native-image/org.neo4j/driver/reflection-config.json @@ -0,0 +1,22 @@ +[ + { + "name": "org.neo4j.driver.internal.shaded.io.netty.channel.socket.nio.NioSocketChannel", + "methods": [ + { "name": "", "parameterTypes": [] } + ] + }, + { + "name": "org.neo4j.driver.internal.shaded.io.netty.channel.socket.nio.NioServerSocketChannel", + "methods": [ + { "name": "", "parameterTypes": [] } + ] + }, + { + "name" : "org.neo4j.driver.internal.shaded.io.netty.buffer.AbstractByteBufAllocator", + "allDeclaredMethods" : true + }, + { + "name" : "org.neo4j.driver.internal.shaded.io.netty.util.ReferenceCountUtil", + "allDeclaredMethods" : true + } +] diff --git a/pom.xml b/pom.xml index e253af1498..22a2a57e91 100644 --- a/pom.xml +++ b/pom.xml @@ -140,6 +140,13 @@ 3.2.6.RELEASE test + + com.oracle.substratevm + svm + 19.2.0.1 + + provided +