Skip to content

Commit

Permalink
[GR-18163] Save the ABI version in native extensions and check it whe…
Browse files Browse the repository at this point in the history
…n loading a native extension

PullRequest: truffleruby/2735
  • Loading branch information
eregon committed Jun 21, 2021
2 parents 2d8ba91 + 25c5423 commit d497bae
Show file tree
Hide file tree
Showing 11 changed files with 97 additions and 39 deletions.
2 changes: 1 addition & 1 deletion lib/cext/ABI_version.txt
@@ -1 +1 @@
3
4
3 changes: 2 additions & 1 deletion lib/cext/include/ruby/ruby.h
Expand Up @@ -24,14 +24,15 @@ extern "C" {
// Must be first, as it defines feature test macros like _GNU_SOURCE,
// which influences the definitions exposed by system header files.
#include "ruby/config.h"
#include <truffleruby/truffleruby-pre.h>
#ifdef RUBY_EXTCONF_H
#include RUBY_EXTCONF_H
#endif

#include "defines.h"
#include "ruby/assert.h"

#include <truffleruby/truffleruby-pre.h>

/* For MinGW, we need __declspec(dllimport) for RUBY_EXTERN on MJIT.
mswin's RUBY_EXTERN already has that. See also: win32/Makefile.sub */
#if defined(MJIT_HEADER) && defined(_WIN32) && defined(__GNUC__)
Expand Down
6 changes: 6 additions & 0 deletions lib/cext/include/truffleruby/truffleruby-pre.h
Expand Up @@ -53,6 +53,12 @@ POLYGLOT_DECLARE_TYPE(VALUE)
extern void* rb_tr_cext;
#define RUBY_CEXT rb_tr_cext

void* rb_tr_abi_version(void) __attribute__((weak));
void* rb_tr_abi_version(void) {
char* abi_version = STRINGIZE(TRUFFLERUBY_ABI_VERSION);

This comment has been minimized.

Copy link
@tomstuart

tomstuart Jun 22, 2021

Contributor

Unfortunately STRINGIZE(TRUFFLERUBY_ABI_VERSION) is just the string literal #TRUFFLERUBY_ABI_VERSION, so this causes errors like LoadError: The native extension […] has a different ABI version: "TRUFFLERUBY_ABI_VERSION" than the running TruffleRuby: "2.7.3.4".

This comment has been minimized.

Copy link
@eregon

eregon Jun 22, 2021

Author Member

On which OS?

This comment has been minimized.

Copy link
@eregon

eregon Jun 22, 2021

Author Member

On both macOS and Linux, as far as I see,

#define STRINGIZE0(expr) #expr
#define STRINGIZE(expr) STRINGIZE0(expr)

So it should work fine.
https://gcc.gnu.org/onlinedocs/cpp/Stringizing.html

This comment has been minimized.

Copy link
@tomstuart

tomstuart Jun 22, 2021

Contributor

Sorry, I’m confused — #expr is expanded to the string literal "TRUFFLERUBY_ABI_VERSION", right?

This comment has been minimized.

Copy link
@tomstuart

tomstuart Jun 22, 2021

Contributor

(The above error is on Linux, btw, just getting more details now.)

This comment has been minimized.

Copy link
@eregon

eregon Jun 22, 2021

Author Member

It's the xstr/str example in the link above.
Can you share for which gem/extension it happens?
If it was a logic bug it should happen for every extension, including libtruffleruby.
Maybe the problem is TRUFFLERUBY_ABI_VERSION doesn't get defined for some reason in that extension.

This comment has been minimized.

Copy link
@tomstuart

tomstuart Jun 22, 2021

Contributor

Moved to #2382.

return polyglot_from_string(abi_version, "US-ASCII");
}

// Wrapping and unwrapping of values.

extern void* (*rb_tr_unwrap)(VALUE obj);
Expand Down
2 changes: 1 addition & 1 deletion lib/truffle/rbconfig.rb
Expand Up @@ -73,7 +73,7 @@ module RbConfig

# Determine the various flags for native compilation
optflags = ''
debugflags = ''
debugflags = "-DTRUFFLERUBY_ABI_VERSION=#{ruby_abi_version}"
warnflags = [
'-Wimplicit-function-declaration', # To make missing C ext functions clear
'-Wno-int-conversion', # MRI has VALUE defined as long while we have it as void*
Expand Down
10 changes: 10 additions & 0 deletions lib/truffle/truffle/cext.rb
Expand Up @@ -118,6 +118,16 @@ def supported?
Interop.mime_type_supported?('application/x-sulong-library')
end

def check_abi_version(embedded_abi_version, extension_path)
runtime_abi_version = Truffle::GemUtil.abi_version
if embedded_abi_version != runtime_abi_version
message = "The native extension at #{extension_path} has a different ABI version: #{embedded_abi_version.inspect} " \
"than the running TruffleRuby: #{runtime_abi_version.inspect}"
warn message, uplevel: 1
raise LoadError, message
end
end

def rb_stdin
$stdin
end
Expand Down
3 changes: 2 additions & 1 deletion src/main/c/Makefile
Expand Up @@ -21,10 +21,11 @@ TRUFFLE_POSIX := truffleposix/libtruffleposix.$(SOEXT)
SPAWN_HELPER := spawn-helper/spawn-helper

RUBY_HEADERS := $(wildcard $(ROOT)/lib/cext/include/*.h) $(wildcard $(ROOT)/lib/cext/include/*/*.h) $(wildcard $(ROOT)/lib/cext/include/*/*/*.h)
ABI_VERSION := $(ROOT)/lib/cext/ABI_version.txt
RBCONFIG := $(ROOT)/lib/truffle/rbconfig.rb
MKMF := $(ROOT)/lib/mri/mkmf.rb
LIBTRUFFLERUBY = cext/libtruffleruby.$(SOEXT)
BASIC_EXTCONF_DEPS := $(SPAWN_HELPER) $(TRUFFLE_POSIX) $(RUBY_HEADERS) $(RBCONFIG) $(MKMF)
BASIC_EXTCONF_DEPS := $(SPAWN_HELPER) $(TRUFFLE_POSIX) $(RUBY_HEADERS) $(ABI_VERSION) $(RBCONFIG) $(MKMF)
# C extensions link against libtruffleruby (and might do have_func() checks against it), so it needs to be there before.
# However, if libtruffleruby is recompiled, there is no need to rebuild C extensions, so it's a order-only-prerequisite.
EXTCONF_DEPS := $(BASIC_EXTCONF_DEPS) | $(LIBTRUFFLERUBY)
Expand Down
3 changes: 2 additions & 1 deletion src/main/java/org/truffleruby/core/CoreLibrary.java
Expand Up @@ -184,6 +184,7 @@ public class CoreLibrary {
public final RubyClass truffleFFINullPointerErrorClass;
public final RubyModule truffleTypeModule;
public final RubyModule truffleModule;
public final RubyModule truffleCExtModule;
public final RubyModule truffleInternalModule;
public final RubyModule truffleBootModule;
public final RubyModule truffleExceptionOperationsModule;
Expand Down Expand Up @@ -489,7 +490,7 @@ public CoreLibrary(RubyContext context, RubyLanguage language) {
truffleInteropModule,
interopExceptionClass,
"UnknownKeyException");
defineModule(truffleModule, "CExt");
truffleCExtModule = defineModule(truffleModule, "CExt");
defineModule(truffleModule, "Debug");
defineModule(truffleModule, "ObjSpace");
defineModule(truffleModule, "Coverage");
Expand Down
67 changes: 61 additions & 6 deletions src/main/java/org/truffleruby/language/loader/FeatureLoader.java
Expand Up @@ -20,12 +20,15 @@
import java.util.Map;
import java.util.concurrent.locks.ReentrantLock;

import com.oracle.truffle.api.interop.UnknownIdentifierException;
import com.oracle.truffle.api.interop.UnsupportedMessageException;
import org.jcodings.Encoding;
import org.jcodings.specific.UTF8Encoding;
import org.truffleruby.RubyContext;
import org.truffleruby.RubyLanguage;
import org.truffleruby.collections.ConcurrentOperations;
import org.truffleruby.core.array.ArrayOperations;
import org.truffleruby.core.array.ArrayUtils;
import org.truffleruby.core.array.RubyArray;
import org.truffleruby.core.encoding.EncodingManager;
import org.truffleruby.core.module.RubyModule;
Expand All @@ -35,7 +38,9 @@
import org.truffleruby.core.thread.RubyThread;
import org.truffleruby.extra.TruffleRubyNodes;
import org.truffleruby.extra.ffi.Pointer;
import org.truffleruby.interop.InteropNodes;
import org.truffleruby.interop.TranslateInteropExceptionNode;
import org.truffleruby.language.Nil;
import org.truffleruby.language.RubyConstant;
import org.truffleruby.language.control.RaiseException;
import org.truffleruby.language.dispatch.DispatchNode;
Expand Down Expand Up @@ -434,8 +439,7 @@ public void ensureCExtImplementationLoaded(String feature, RequireNode requireNo
final String rubyLibPath = context.getRubyHome() + "/lib/cext/libtruffleruby" + Platform.LIB_SUFFIX;
final Object library = loadCExtLibRuby(rubyLibPath, feature, requireNode);

final Object initFunction = requireNode
.findFunctionInLibrary(library, "rb_tr_init", rubyLibPath);
final Object initFunction = findFunctionInLibrary(library, "rb_tr_init", rubyLibPath);

final InteropLibrary interop = InteropLibrary.getFactory().getUncached();
try {
Expand Down Expand Up @@ -479,12 +483,63 @@ public Object loadCExtLibrary(String feature, String path, Node currentNode) {
final TruffleFile truffleFile = FileLoader.getSafeTruffleFile(context, path);
FileLoader.ensureReadable(context, truffleFile, currentNode);

final Source source = Source.newBuilder("llvm", truffleFile).build();
return context.getEnv().parseInternal(source).call();
} catch (IOException e) {
throw new RaiseException(context, context.getCoreExceptions().loadError(e, path, currentNode));
final Source source;
try {
source = Source.newBuilder("llvm", truffleFile).build();
} catch (IOException e) {
throw new RaiseException(context, context.getCoreExceptions().loadError(e, path, currentNode));
}

final Object library = context.getEnv().parseInternal(source).call();

final Object embeddedABIVersion = getEmbeddedABIVersion(path, library);
RubyContext.send(context.getCoreLibrary().truffleCExtModule, "check_abi_version", embeddedABIVersion, path);

return library;
} finally {
Metrics.printTime("after-load-cext-" + feature);
}
}

private Object getEmbeddedABIVersion(String expandedPath, Object library) {
if (!InteropLibrary.getFactory().getUncached(library).isMemberReadable(library, "rb_tr_abi_version")) {
return Nil.INSTANCE;
}

final Object abiVersionFunction = findFunctionInLibrary(library, "rb_tr_abi_version", expandedPath);
final InteropLibrary abiFunctionInteropLibrary = InteropLibrary.getFactory().getUncached(abiVersionFunction);
final String abiVersion = (String) InteropNodes.execute(
abiVersionFunction,
ArrayUtils.EMPTY_ARRAY,
abiFunctionInteropLibrary,
TranslateInteropExceptionNode.getUncached());
return StringOperations.createString(
context,
language,
StringOperations.encodeRope(abiVersion, UTF8Encoding.INSTANCE));
}

Object findFunctionInLibrary(Object library, String functionName, String path) {
final Object function;
try {
function = InteropLibrary.getFactory().getUncached(library).readMember(library, functionName);
} catch (UnknownIdentifierException e) {
throw new RaiseException(
context,
context.getCoreExceptions().loadError(String.format("%s() not found", functionName), path, null));
} catch (UnsupportedMessageException e) {
throw TranslateInteropExceptionNode.getUncached().execute(e);
}

if (function == null) {
throw new RaiseException(
context,
context.getCoreExceptions().loadError(
String.format("%s() not found (READ returned null)", functionName),
path,
null));
}

return function;
}
}
30 changes: 2 additions & 28 deletions src/main/java/org/truffleruby/language/loader/RequireNode.java
Expand Up @@ -44,8 +44,6 @@
import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary;
import com.oracle.truffle.api.dsl.Specialization;
import com.oracle.truffle.api.interop.InteropLibrary;
import com.oracle.truffle.api.interop.UnknownIdentifierException;
import com.oracle.truffle.api.interop.UnsupportedMessageException;
import com.oracle.truffle.api.nodes.IndirectCallNode;
import com.oracle.truffle.api.nodes.Node;
import com.oracle.truffle.api.source.SourceSection;
Expand Down Expand Up @@ -251,7 +249,6 @@ private void requireCExtension(String feature, String expandedPath, Node current
final FeatureLoader featureLoader = getContext().getFeatureLoader();

final Object library;

try {
featureLoader.ensureCExtImplementationLoaded(feature, this);

Expand All @@ -267,10 +264,9 @@ private void requireCExtension(String feature, String expandedPath, Node current
}

final String initFunctionName = "Init_" + getBaseName(expandedPath);
final Object initFunction = featureLoader.findFunctionInLibrary(library, initFunctionName, expandedPath);

final Object initFunction = findFunctionInLibrary(library, initFunctionName, expandedPath);

InteropLibrary initFunctionInteropLibrary = InteropLibrary.getFactory().getUncached(initFunction);
final InteropLibrary initFunctionInteropLibrary = InteropLibrary.getFactory().getUncached(initFunction);
if (!initFunctionInteropLibrary.isExecutable(initFunction)) {
throw new RaiseException(
getContext(),
Expand All @@ -290,28 +286,6 @@ private void requireCExtension(String feature, String expandedPath, Node current
}
}

Object findFunctionInLibrary(Object library, String functionName, String path) {
final Object function;
try {
function = InteropLibrary.getFactory().getUncached(library).readMember(library, functionName);
} catch (UnknownIdentifierException e) {
throw new RaiseException(
getContext(),
coreExceptions().loadError(String.format("%s() not found", functionName), path, null));
} catch (UnsupportedMessageException e) {
throw TranslateInteropExceptionNode.getUncached().execute(e);
}

if (function == null) {
throw new RaiseException(
getContext(),
coreExceptions()
.loadError(String.format("%s() not found (READ returned null)", functionName), path, null));
}

return function;
}

@TruffleBoundary
private void handleCExtensionException(String feature, Exception e) {
TranslateExceptionNode.logJavaException(getContext(), this, e);
Expand Down
9 changes: 9 additions & 0 deletions src/main/ruby/truffleruby/core/truffle/gem_util.rb
Expand Up @@ -142,6 +142,15 @@ def self.abi_version
@abi_version ||= "#{RUBY_VERSION}.#{Truffle::Boot.basic_abi_version}".freeze
end

def self.check_abi_version(embedded_abi_version, extension_path)
if embedded_abi_version != abi_version
message = "The native extension at #{extension_path} has a different ABI version: #{embedded_abi_version.inspect} " \
"than the running TruffleRuby: #{abi_version.inspect}"
warn message, uplevel: 1
raise LoadError, message
end
end

def self.expand(path)
if File.directory?(path)
File.realpath(path)
Expand Down
1 change: 1 addition & 0 deletions test/truffle/cexts/minimum/ext/minimum/minimum.c
@@ -1,4 +1,5 @@
#include <stdio.h>
#include <ruby.h>

void Init_minimum() {
printf("Hello!\n");
Expand Down

0 comments on commit d497bae

Please sign in to comment.