Skip to content

Commit 8f5248d

Browse files
committed
Add NativeLibrary utility class
1 parent d17af8b commit 8f5248d

File tree

2 files changed

+350
-1
lines changed

2 files changed

+350
-1
lines changed

graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/ImpModuleBuiltins.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -559,7 +559,7 @@ Object run(
559559
}
560560

561561
@TruffleBoundary
562-
static String getSoAbi(PythonContext ctxt) {
562+
public static String getSoAbi(PythonContext ctxt) {
563563
PythonModule sysModule = ctxt.getCore().lookupBuiltinModule("sys");
564564
Object implementationObj = ReadAttributeFromObjectNode.getUncached().execute(sysModule, "implementation");
565565
// sys.implementation.cache_tag
Lines changed: 349 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,349 @@
1+
/*
2+
* Copyright (c) 2020, 2020, Oracle and/or its affiliates. All rights reserved.
3+
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4+
*
5+
* The Universal Permissive License (UPL), Version 1.0
6+
*
7+
* Subject to the condition set forth below, permission is hereby granted to any
8+
* person obtaining a copy of this software, associated documentation and/or
9+
* data (collectively the "Software"), free of charge and under any and all
10+
* copyright rights in the Software, and any and all patent rights owned or
11+
* freely licensable by each licensor hereunder covering either (i) the
12+
* unmodified Software as contributed to or provided by such licensor, or (ii)
13+
* the Larger Works (as defined below), to deal in both
14+
*
15+
* (a) the Software, and
16+
*
17+
* (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if
18+
* one is included with the Software each a "Larger Work" to which the Software
19+
* is contributed by such licensors),
20+
*
21+
* without restriction, including without limitation the rights to copy, create
22+
* derivative works of, display, perform, and distribute the Software and make,
23+
* use, sell, offer for sale, import, export, have made, and have sold the
24+
* Software and the Larger Work(s), and to sublicense the foregoing rights on
25+
* either these or other terms.
26+
*
27+
* This license is subject to the following condition:
28+
*
29+
* The above copyright notice and either this complete permission notice or at a
30+
* minimum a reference to the UPL must be included in all copies or substantial
31+
* portions of the Software.
32+
*
33+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
34+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
35+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
36+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
37+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
38+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
39+
* SOFTWARE.
40+
*/
41+
package com.oracle.graal.python.runtime;
42+
43+
import java.util.Objects;
44+
import java.util.logging.Level;
45+
46+
import com.oracle.graal.python.PythonLanguage;
47+
import com.oracle.graal.python.builtins.PythonBuiltinClassType;
48+
import com.oracle.graal.python.builtins.modules.ImpModuleBuiltins;
49+
import com.oracle.graal.python.nodes.PNodeWithContext;
50+
import com.oracle.graal.python.nodes.PRaiseNode;
51+
import com.oracle.graal.python.runtime.NativeLibraryFactory.InvokeNativeFunctionNodeGen;
52+
import com.oracle.truffle.api.CompilerAsserts;
53+
import com.oracle.truffle.api.CompilerDirectives;
54+
import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary;
55+
import com.oracle.truffle.api.TruffleFile;
56+
import com.oracle.truffle.api.TruffleLogger;
57+
import com.oracle.truffle.api.dsl.Cached;
58+
import com.oracle.truffle.api.dsl.CachedContext;
59+
import com.oracle.truffle.api.dsl.Specialization;
60+
import com.oracle.truffle.api.interop.ArityException;
61+
import com.oracle.truffle.api.interop.InteropLibrary;
62+
import com.oracle.truffle.api.interop.UnknownIdentifierException;
63+
import com.oracle.truffle.api.interop.UnsupportedMessageException;
64+
import com.oracle.truffle.api.interop.UnsupportedTypeException;
65+
import com.oracle.truffle.api.library.CachedLibrary;
66+
import com.oracle.truffle.api.profiles.ValueProfile;
67+
import com.oracle.truffle.api.source.Source;
68+
69+
/**
70+
* Wraps a native library loaded via NFI and provides caching for functions looked up in the
71+
* library. The set of functions to be loaded from the library is expressed as Java enum
72+
* implementing {@link NativeFunction}. This is runtime object: it should not be cached in the AST
73+
* and is expected to be stored in the context.
74+
* <p>
75+
* Because of Truffle DSL restrictions this class cannot be generic, but users should work with
76+
* generic subclass {@link TypedNativeLibrary}, which can be created with one of the {@code create}
77+
* factory methods.
78+
* <p>
79+
* For now, until there is no need to access the library and function objects directly, this object
80+
* is opaque to the outside code and the only entrypoint is {@link InvokeNativeFunction}, which
81+
* lazily loads the library, the requested function and invokes it. This node takes care of
82+
* efficient caching of the loaded NFI objects.
83+
*/
84+
public class NativeLibrary {
85+
private static final TruffleLogger LOGGER = PythonLanguage.getLogger(NativeLibrary.class);
86+
87+
/**
88+
* This interface is intended to be implemented by enums.
89+
*/
90+
interface NativeFunction {
91+
String signature();
92+
93+
String name();
94+
95+
int ordinal();
96+
}
97+
98+
enum NFIBackend {
99+
NATIVE(""),
100+
LLVM("with llvm ");
101+
102+
private final String withClause;
103+
104+
NFIBackend(String withClause) {
105+
this.withClause = withClause;
106+
}
107+
}
108+
109+
private final int functionsCount;
110+
private final String name;
111+
private final NFIBackend nfiBackend;
112+
113+
/**
114+
* If given functionality has a fully managed variant that can be configured, this help message
115+
* should explain how to switch to it. It will be printed if loading of the native library
116+
* fails.
117+
*/
118+
private final String noNativeAccessHelp;
119+
120+
private volatile Object[] cachedFunctions;
121+
private volatile Object cachedLibrary;
122+
private volatile InteropLibrary cachedLibraryInterop;
123+
private volatile Object dummy;
124+
125+
public NativeLibrary(String name, int functionsCount, NFIBackend nfiBackend, String noNativeAccessHelp) {
126+
this.functionsCount = functionsCount;
127+
this.name = name;
128+
this.nfiBackend = nfiBackend;
129+
this.noNativeAccessHelp = noNativeAccessHelp;
130+
}
131+
132+
private Object getCachedLibrary(PythonContext context) {
133+
if (cachedLibrary == null) {
134+
// This should be a one-off thing for each context
135+
CompilerDirectives.transferToInterpreter();
136+
synchronized (this) {
137+
if (cachedLibrary == null) {
138+
Object lib = loadLibrary(context);
139+
cachedLibraryInterop = InteropLibrary.getUncached(lib);
140+
cachedLibrary = lib;
141+
}
142+
}
143+
}
144+
return cachedLibrary;
145+
}
146+
147+
private Object getCachedFunction(PythonContext context, NativeFunction function) {
148+
Object lib = getCachedLibrary(context);
149+
if (cachedFunctions == null) {
150+
// This should be a one-off thing for each context
151+
CompilerDirectives.transferToInterpreter();
152+
synchronized (this) {
153+
if (cachedFunctions == null) {
154+
cachedFunctions = new Object[functionsCount];
155+
}
156+
}
157+
}
158+
int functionIndex = function.ordinal();
159+
if (cachedFunctions[functionIndex] == null) {
160+
// This should be a one-off thing for each context
161+
CompilerDirectives.transferToInterpreter();
162+
synchronized (this) {
163+
dummy = getFunction(lib, function);
164+
// it is OK to overwrite cachedFunctions[functionIndex] that may have been
165+
// written from another thread: no need to double check that it's still null.
166+
// dummy is volatile, the object must be fully initialized at this point
167+
cachedFunctions[functionIndex] = dummy;
168+
}
169+
}
170+
return cachedFunctions[functionIndex];
171+
}
172+
173+
private Object getFunction(PythonContext context, NativeFunction function) {
174+
CompilerAsserts.neverPartOfCompilation();
175+
Object lib = getCachedLibrary(context);
176+
return getFunction(lib, function);
177+
}
178+
179+
private Object getFunction(Object lib, NativeFunction function) {
180+
CompilerAsserts.neverPartOfCompilation();
181+
try {
182+
Object symbol = cachedLibraryInterop.readMember(lib, function.name());
183+
return InteropLibrary.getUncached().invokeMember(symbol, "bind", function.signature());
184+
} catch (UnsupportedMessageException | UnknownIdentifierException | ArityException | UnsupportedTypeException e) {
185+
throw new IllegalStateException(String.format("Cannot load symbol '%s' from the internal shared library '%s'", function.name(), name), e);
186+
}
187+
}
188+
189+
private Object loadLibrary(PythonContext context) {
190+
CompilerAsserts.neverPartOfCompilation();
191+
if (!context.getEnv().isNativeAccessAllowed()) {
192+
throw PRaiseNode.getUncached().raise(PythonBuiltinClassType.SystemError,
193+
"Cannot load supporting native library '%s' because the native access is not allowed. " +
194+
"The native access should be allowed when running GraalPython via the graalpython command. " +
195+
"If you are embedding GraalPython using the Context API, make sure to allow native access using 'allowNativeAccess(true)'. %s",
196+
name,
197+
noNativeAccessHelp);
198+
}
199+
String path = getLibPath(context);
200+
String src = String.format("%sload (RTLD_LOCAL) \"%s\"", nfiBackend.withClause, path);
201+
if (LOGGER.isLoggable(Level.FINE)) {
202+
LOGGER.fine(String.format("Loading native library %s from path %s %s", name, path, nfiBackend.withClause));
203+
}
204+
Source loadSrc = Source.newBuilder("nfi", src, "load:" + name).internal(true).build();
205+
try {
206+
return context.getEnv().parseInternal(loadSrc).call();
207+
} catch (UnsatisfiedLinkError ex) {
208+
LOGGER.log(Level.SEVERE, ex, () -> String.format("Error while opening shared library at '%s'.\nFull NFI source: %s.", path, src));
209+
throw PRaiseNode.getUncached().raise(PythonBuiltinClassType.SystemError,
210+
"Cannot load supporting native library '%s'. " +
211+
"Either the shared library file does not exist, or your system may be missing some dependencies. " +
212+
"Turn on logging with --log.%s.level=INFO for more details. %s",
213+
name,
214+
NativeLibrary.class.getName(),
215+
noNativeAccessHelp);
216+
}
217+
}
218+
219+
private String getLibPath(PythonContext context) {
220+
CompilerAsserts.neverPartOfCompilation();
221+
String libPythonName = name + ImpModuleBuiltins.ExtensionSuffixesNode.getSoAbi(context);
222+
TruffleFile homePath = context.getEnv().getInternalTruffleFile(context.getCAPIHome());
223+
TruffleFile file = homePath.resolve(libPythonName);
224+
return file.getPath();
225+
}
226+
227+
public static <T extends Enum<T> & NativeFunction> TypedNativeLibrary<T> create(String name, T[] functions, String noNativeAccessHelp) {
228+
return create(name, functions, NFIBackend.NATIVE, noNativeAccessHelp);
229+
}
230+
231+
public static <T extends Enum<T> & NativeFunction> TypedNativeLibrary<T> create(String name, T[] functions, NFIBackend nfiBackendName, String noNativeAccessHelp) {
232+
return new TypedNativeLibrary<>(name, functions.length, nfiBackendName, noNativeAccessHelp);
233+
}
234+
235+
public static final class TypedNativeLibrary<T extends Enum<T> & NativeFunction> extends NativeLibrary {
236+
public TypedNativeLibrary(String name, int functionsCount, NFIBackend nfiBackendName, String noNativeAccessHelp) {
237+
super(name, functionsCount, nfiBackendName, noNativeAccessHelp);
238+
}
239+
}
240+
241+
public abstract static class InvokeNativeFunction extends PNodeWithContext {
242+
private static final InvokeNativeFunction UNCACHED = InvokeNativeFunctionNodeGen.create(InteropLibrary.getUncached());
243+
@Child private InteropLibrary resultInterop;
244+
245+
public InvokeNativeFunction(InteropLibrary resultInterop) {
246+
this.resultInterop = resultInterop;
247+
}
248+
249+
public static InvokeNativeFunction create() {
250+
return InvokeNativeFunctionNodeGen.create(null);
251+
}
252+
253+
public static InvokeNativeFunction getUncached() {
254+
return UNCACHED;
255+
}
256+
257+
public <T extends Enum<T> & NativeFunction> Object call(TypedNativeLibrary<T> lib, T function, Object... args) {
258+
return execute(lib, function, args);
259+
}
260+
261+
public <T extends Enum<T> & NativeFunction> long callLong(TypedNativeLibrary<T> lib, T function, Object... args) {
262+
try {
263+
return ensureResultInterop().asLong(call(lib, function, args));
264+
} catch (UnsupportedMessageException e) {
265+
throw CompilerDirectives.shouldNotReachHere(function.name(), e);
266+
}
267+
}
268+
269+
public <T extends Enum<T> & NativeFunction> int callInt(TypedNativeLibrary<T> lib, T function, Object... args) {
270+
try {
271+
return ensureResultInterop().asInt(call(lib, function, args));
272+
} catch (UnsupportedMessageException e) {
273+
throw CompilerDirectives.shouldNotReachHere(function.name(), e);
274+
}
275+
}
276+
277+
protected abstract Object execute(NativeLibrary lib, NativeFunction function, Object[] args);
278+
279+
@Specialization(guards = {"function == cachedFunction", "lib == cachedLib"}, assumptions = "singleContextAssumption()", limit = "3")
280+
static Object doSingleContext(@SuppressWarnings("unused") NativeLibrary lib, @SuppressWarnings("unused") NativeFunction function, Object[] args,
281+
@SuppressWarnings("unused") @Cached(value = "lib", weak = true) NativeLibrary cachedLib,
282+
@Cached("function") NativeFunction cachedFunction,
283+
@Cached(value = "getFunction(lib, function)", weak = true) Object funObj,
284+
@CachedLibrary("funObj") InteropLibrary funInterop) {
285+
return invoke(cachedFunction, args, funObj, funInterop);
286+
}
287+
288+
@Specialization(replaces = "doSingleContext")
289+
static Object doMultiContext(NativeLibrary lib, NativeFunction functionIn, Object[] args,
290+
@Cached("createIdentityProfile()") ValueProfile functionProfile,
291+
@Cached("createClassProfile()") ValueProfile functionClassProfile,
292+
@CachedContext(PythonLanguage.class) PythonContext ctx,
293+
@CachedLibrary(limit = "1") InteropLibrary funInterop) {
294+
NativeFunction function = functionClassProfile.profile(functionProfile.profile(functionIn));
295+
Object funObj = lib.getCachedFunction(ctx, function);
296+
return invoke(function, args, funObj, funInterop);
297+
}
298+
299+
private static Object invoke(NativeFunction function, Object[] args, Object funObj, InteropLibrary funInterop) {
300+
try {
301+
if (LOGGER.isLoggable(Level.FINEST)) {
302+
LOGGER.finest(buildLogMessage(function, args));
303+
}
304+
Object result = funInterop.execute(funObj, args);
305+
if (LOGGER.isLoggable(Level.FINEST)) {
306+
LOGGER.finest(buildReturnLogMessage(function, result));
307+
}
308+
return result;
309+
} catch (UnsupportedTypeException | ArityException | UnsupportedMessageException e) {
310+
throw CompilerDirectives.shouldNotReachHere(function.name(), e);
311+
}
312+
}
313+
314+
protected static Object getFunction(NativeLibrary lib, NativeFunction fun) {
315+
return lib.getFunction(PythonLanguage.getContext(), fun);
316+
}
317+
318+
@TruffleBoundary
319+
private static String buildLogMessage(NativeFunction function, Object[] args) {
320+
StringBuilder sb = new StringBuilder("Executing native function ");
321+
sb.append(function.name()).append(" with arguments: ");
322+
for (Object arg : args) {
323+
sb.append(safeToString(arg)).append(',');
324+
}
325+
return sb.toString();
326+
}
327+
328+
@TruffleBoundary
329+
private static String buildReturnLogMessage(NativeFunction function, Object result) {
330+
return "Finished executing native function " + function.name() + " with result: " + safeToString(result);
331+
}
332+
333+
private static String safeToString(Object value) {
334+
try {
335+
return Objects.toString(value);
336+
} catch (Exception ex) {
337+
return String.format("%s (toString threw %s),", value.getClass().getSimpleName(), ex.getClass().getSimpleName());
338+
}
339+
}
340+
341+
public InteropLibrary ensureResultInterop() {
342+
if (resultInterop == null) {
343+
CompilerDirectives.transferToInterpreterAndInvalidate();
344+
resultInterop = insert(InteropLibrary.getFactory().createDispatched(2));
345+
}
346+
return resultInterop;
347+
}
348+
}
349+
}

0 commit comments

Comments
 (0)