Split out mdsal-binding-loader
[mdsal.git] / binding / mdsal-binding-loader / src / main / java / org / opendaylight / mdsal / binding / loader / BindingClassLoader.java
1 /*
2  * Copyright (c) 2019 PANTHEON.tech, s.r.o. and others.  All rights reserved.
3  *
4  * This program and the accompanying materials are made available under the
5  * terms of the Eclipse Public License v1.0 which accompanies this distribution,
6  * and is available at http://www.eclipse.org/legal/epl-v10.html
7  */
8 package org.opendaylight.mdsal.binding.loader;
9
10 import static com.google.common.base.Preconditions.checkArgument;
11 import static com.google.common.base.Verify.verify;
12 import static java.util.Objects.requireNonNull;
13
14 import com.google.common.collect.ImmutableMap;
15 import com.google.common.collect.ImmutableSet;
16 import java.io.File;
17 import java.io.IOException;
18 import java.security.AccessController;
19 import java.security.PrivilegedAction;
20 import java.util.Collection;
21 import java.util.HashSet;
22 import java.util.Set;
23 import java.util.function.Supplier;
24 import net.bytebuddy.dynamic.DynamicType.Unloaded;
25 import net.bytebuddy.dynamic.loading.ClassLoadingStrategy;
26 import org.eclipse.jdt.annotation.NonNull;
27 import org.eclipse.jdt.annotation.Nullable;
28 import org.slf4j.Logger;
29 import org.slf4j.LoggerFactory;
30
31 /**
32  * A ClassLoader hosting types generated for a particular type. A root instance is attached to a
33  * BindingCodecContext instance, so any generated classes from it can be garbage-collected when the context
34  * is destroyed, as well as to prevent two contexts trampling over each other.
35  *
36  * <p>
37  * It semantically combines two class loaders: the class loader in which this class is loaded and the class loader in
38  * which a target Binding interface/class is loaded. This inherently supports multi-classloader environments -- the root
39  * instance has visibility only into codec classes and for each classloader we encounter when presented with a binding
40  * class we create a leaf instance and cache it in the root instance. Leaf instances are using the root loader as their
41  * parent, but consult the binding class's class loader if the root loader fails to load a particular class.
42  *
43  * <p>In single-classloader environments, obviously, the root loader can load all binding classes, and hence no leaf
44  * loader is created.
45  */
46 public abstract sealed class BindingClassLoader extends ClassLoader
47         permits LeafBindingClassLoader, RootBindingClassLoader {
48     /**
49      * A class generator, generating a class of a particular type.
50      *
51      * @param <T> Type of generated class
52      */
53     public interface ClassGenerator<T> {
54         /**
55          * Generate a class.
56          *
57          * @param fqcn Generated class Fully-qualified class name
58          * @param bindingInterface Binding interface for which the class is being generated
59          * @return A result.
60          */
61         GeneratorResult<T> generateClass(BindingClassLoader loader, String fqcn, Class<?> bindingInterface);
62
63         /**
64          * Run the specified loader in a customized environment. The environment customizations must be cleaned up by
65          * the time this method returns. The default implementation performs no customization.
66          *
67          * @param loader Class loader to execute
68          * @return Class returned by the loader
69          */
70         default Class<T> customizeLoading(final @NonNull Supplier<Class<T>> loader) {
71             return loader.get();
72         }
73     }
74
75     /**
76      * Result of class generation.
77      *
78      * @param <T> Type of generated class.
79      */
80     public static final class GeneratorResult<T> {
81         private final @NonNull ImmutableSet<Class<?>> dependecies;
82         private final @NonNull Unloaded<T> result;
83
84         GeneratorResult(final Unloaded<T> result, final ImmutableSet<Class<?>> dependecies) {
85             this.result = requireNonNull(result);
86             this.dependecies = requireNonNull(dependecies);
87         }
88
89         public static <T> @NonNull GeneratorResult<T> of(final Unloaded<T> result) {
90             return new GeneratorResult<>(result, ImmutableSet.of());
91         }
92
93         public static <T> @NonNull GeneratorResult<T> of(final Unloaded<T> result,
94                 final Collection<Class<?>> dependencies) {
95             return dependencies.isEmpty() ? of(result) : new GeneratorResult<>(result,
96                 ImmutableSet.copyOf(dependencies));
97         }
98
99         @NonNull Unloaded<T> getResult() {
100             return result;
101         }
102
103         @NonNull ImmutableSet<Class<?>> getDependencies() {
104             return dependecies;
105         }
106     }
107
108     private static final ClassLoadingStrategy<BindingClassLoader> STRATEGY = (classLoader, types) -> {
109         verify(types.size() == 1, "Unexpected multiple types", types);
110         final var entry = types.entrySet().iterator().next();
111         return ImmutableMap.of(entry.getKey(), classLoader.loadClass(entry.getKey().getName(), entry.getValue()));
112     };
113
114     static {
115         verify(ClassLoader.registerAsParallelCapable());
116     }
117
118     private static final Logger LOG = LoggerFactory.getLogger(BindingClassLoader.class);
119
120     private final @Nullable File dumpDir;
121
122     BindingClassLoader(final ClassLoader parentLoader, final @Nullable File dumpDir) {
123         super(parentLoader);
124         this.dumpDir = dumpDir;
125     }
126
127     BindingClassLoader(final BindingClassLoader parentLoader) {
128         this(parentLoader, parentLoader.dumpDir);
129     }
130
131     /**
132      * Instantiate a new BindingClassLoader, which serves as the root of generated code loading.
133      *
134      * @param rootClass Class from which to derive the class loader
135      * @param dumpDir Directory in which to dump loaded bytecode
136      * @return A new BindingClassLoader.
137      * @throws NullPointerException if {@code parentLoader} is {@code null}
138      */
139     public static @NonNull BindingClassLoader create(final Class<?> rootClass, final @Nullable File dumpDir) {
140         final var parentLoader = rootClass.getClassLoader();
141         return AccessController.doPrivileged(
142             (PrivilegedAction<BindingClassLoader>)() -> new RootBindingClassLoader(parentLoader, dumpDir));
143     }
144
145     /**
146      * The name of the target class is formed through concatenation of the name of a {@code bindingInterface} and
147      * specified {@code suffix}.
148      *
149      * @param <T> Type of generated class
150      * @param bindingInterface Binding compile-time-generated interface
151      * @param suffix Suffix to use
152      * @param generator Code generator to run
153      * @return A generated class object
154      * @throws NullPointerException if any argument is null
155      */
156     public final <T> Class<T> generateClass(final Class<?> bindingInterface, final String suffix,
157             final ClassGenerator<T> generator)  {
158         return findClassLoader(requireNonNull(bindingInterface)).doGenerateClass(bindingInterface, suffix, generator);
159     }
160
161     public final @NonNull Class<?> getGeneratedClass(final Class<?> bindingInterface, final String suffix) {
162         final var loader = findClassLoader(requireNonNull(bindingInterface));
163         final var fqcn = generatedClassName(bindingInterface, suffix);
164
165         final Class<?> ret;
166         synchronized (loader.getClassLoadingLock(fqcn)) {
167             ret = loader.findLoadedClass(fqcn);
168         }
169
170         checkArgument(ret != null, "Failed to find generated class %s for %s of %s", fqcn, suffix, bindingInterface);
171         return ret;
172     }
173
174     /**
175      * Append specified loaders to this class loader for the purposes of looking up generated classes. Note that the
176      * loaders are expected to have required classes already loaded. This is required to support generation of
177      * inter-dependent structures, such as those used for streaming binding interfaces.
178      *
179      * @param newLoaders Loaders to append
180      * @throws NullPointerException if {@code loaders} is null
181      */
182     abstract void appendLoaders(@NonNull Set<LeafBindingClassLoader> newLoaders);
183
184     /**
185      * Find the loader responsible for holding classes related to a binding class.
186      *
187      * @param bindingClass Class to locate
188      * @return a Loader instance
189      * @throws NullPointerException if {@code bindingClass} is null
190      */
191     abstract @NonNull BindingClassLoader findClassLoader(@NonNull Class<?> bindingClass);
192
193     private <T> Class<T> doGenerateClass(final Class<?> bindingInterface, final String suffix,
194             final ClassGenerator<T> generator)  {
195         final var fqcn = generatedClassName(bindingInterface, suffix);
196
197         synchronized (getClassLoadingLock(fqcn)) {
198             // Attempt to find a loaded class
199             final var existing = findLoadedClass(fqcn);
200             if (existing != null) {
201                 return (Class<T>) existing;
202             }
203
204             final var result = generator.generateClass(this, fqcn, bindingInterface);
205             final var unloaded = result.getResult();
206             verify(fqcn.equals(unloaded.getTypeDescription().getName()), "Unexpected class in %s", unloaded);
207             verify(unloaded.getAuxiliaryTypes().isEmpty(), "Auxiliary types present in %s", unloaded);
208             dumpBytecode(unloaded);
209
210             processDependencies(result.getDependencies());
211             return generator.customizeLoading(() -> (Class<T>) unloaded.load(this, STRATEGY).getLoaded());
212         }
213     }
214
215     final Class<?> loadClass(final String fqcn, final byte[] byteCode) {
216         synchronized (getClassLoadingLock(fqcn)) {
217             final var existing = findLoadedClass(fqcn);
218             verify(existing == null, "Attempted to load existing %s", existing);
219             return defineClass(fqcn, byteCode, 0, byteCode.length);
220         }
221     }
222
223     private void processDependencies(final Collection<Class<?>> deps) {
224         final var depLoaders = new HashSet<LeafBindingClassLoader>();
225         for (var dep : deps) {
226             final var depLoader = dep.getClassLoader();
227             verify(depLoader instanceof BindingClassLoader, "Dependency %s is not a generated class", dep);
228             if (equals(depLoader)) {
229                 // Same loader, skip
230                 continue;
231             }
232
233             try {
234                 loadClass(dep.getName());
235             } catch (ClassNotFoundException e) {
236                 LOG.debug("Cannot find {} in local loader, attempting to compensate", dep, e);
237                 // Root loader is always visible from a leaf, hence the dependency can only be a leaf
238                 verify(depLoader instanceof LeafBindingClassLoader, "Dependency loader %s is not a leaf", depLoader);
239                 depLoaders.add((LeafBindingClassLoader) depLoader);
240             }
241         }
242
243         if (!depLoaders.isEmpty()) {
244             appendLoaders(depLoaders);
245         }
246     }
247
248     private void dumpBytecode(final Unloaded<?> unloaded) {
249         final var dir = dumpDir;
250         if (dir != null) {
251             try {
252                 unloaded.saveIn(dir);
253             } catch (IOException | IllegalArgumentException e) {
254                 LOG.info("Failed to save {}", unloaded.getTypeDescription().getName(), e);
255             }
256         }
257     }
258
259     private static String generatedClassName(final Class<?> bindingInterface, final String suffix) {
260         return bindingInterface.getName() + "$$$" + suffix;
261     }
262 }