Recognise and run BC bundles
Reviewed By: javache Differential Revision: D4067425 fbshipit-source-id: fade9adebfa8a59dc49aeadfd01a782f7b686082
This commit is contained in:
parent
94711bfb06
commit
16290851aa
|
@ -9,7 +9,40 @@
|
||||||
|
|
||||||
#import <UIKit/UIKit.h>
|
#import <UIKit/UIKit.h>
|
||||||
|
|
||||||
extern uint32_t const RCTRAMBundleMagicNumber;
|
#import "RCTDefines.h"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* RCTScriptTag
|
||||||
|
*
|
||||||
|
* Scripts given to the JS Executors to run could be in any of the following
|
||||||
|
* formats. They are tagged so the executor knows how to run them.
|
||||||
|
*/
|
||||||
|
typedef NS_ENUM(NSInteger) {
|
||||||
|
RCTScriptString = 0,
|
||||||
|
RCTScriptRAMBundle,
|
||||||
|
RCTScriptBCBundle,
|
||||||
|
} RCTScriptTag;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* RCTMagicNumber
|
||||||
|
*
|
||||||
|
* RAM bundles and BC bundles begin with magic numbers. For RAM bundles this is
|
||||||
|
* 4 bytes, for BC bundles this is 8 bytes. This structure holds the first 8
|
||||||
|
* bytes from a bundle in a way that gives access to that information.
|
||||||
|
*/
|
||||||
|
typedef union {
|
||||||
|
uint64_t allBytes;
|
||||||
|
uint32_t first4;
|
||||||
|
uint64_t first8;
|
||||||
|
} RCTMagicNumber;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* RCTParseMagicNumber
|
||||||
|
*
|
||||||
|
* Takes the first 8 bytes of a bundle, and returns a tag describing the
|
||||||
|
* bundle's format.
|
||||||
|
*/
|
||||||
|
RCT_EXTERN RCTScriptTag RCTParseMagicNumber(RCTMagicNumber magic);
|
||||||
|
|
||||||
extern NSString *const RCTJavaScriptLoaderErrorDomain;
|
extern NSString *const RCTJavaScriptLoaderErrorDomain;
|
||||||
|
|
||||||
|
@ -42,7 +75,7 @@ typedef void (^RCTSourceLoadBlock)(NSError *error, NSData *source, int64_t sourc
|
||||||
* @experimental
|
* @experimental
|
||||||
* Attempts to synchronously load the script at the given URL. The following two conditions must be met:
|
* Attempts to synchronously load the script at the given URL. The following two conditions must be met:
|
||||||
* 1. It must be a file URL.
|
* 1. It must be a file URL.
|
||||||
* 2. It must point to a RAM bundle.
|
* 2. It must not point to a text/javascript file.
|
||||||
* If the URL does not meet those conditions, this method will return nil and supply an error with the domain
|
* If the URL does not meet those conditions, this method will return nil and supply an error with the domain
|
||||||
* RCTJavaScriptLoaderErrorDomain and the code RCTJavaScriptLoaderErrorCannotBeLoadedSynchronously.
|
* RCTJavaScriptLoaderErrorDomain and the code RCTJavaScriptLoaderErrorCannotBeLoadedSynchronously.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -18,10 +18,24 @@
|
||||||
|
|
||||||
#include <sys/stat.h>
|
#include <sys/stat.h>
|
||||||
|
|
||||||
uint32_t const RCTRAMBundleMagicNumber = 0xFB0BD1E5;
|
static uint32_t const RCTRAMBundleMagicNumber = 0xFB0BD1E5;
|
||||||
|
static uint64_t const RCTBCBundleMagicNumber = 0xFF4865726D657300;
|
||||||
|
|
||||||
NSString *const RCTJavaScriptLoaderErrorDomain = @"RCTJavaScriptLoaderErrorDomain";
|
NSString *const RCTJavaScriptLoaderErrorDomain = @"RCTJavaScriptLoaderErrorDomain";
|
||||||
|
|
||||||
|
RCTScriptTag RCTParseMagicNumber(RCTMagicNumber magic)
|
||||||
|
{
|
||||||
|
if (NSSwapLittleIntToHost(magic.first4) == RCTRAMBundleMagicNumber) {
|
||||||
|
return RCTScriptRAMBundle;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (NSSwapLittleLongLongToHost(magic.first8) == RCTBCBundleMagicNumber) {
|
||||||
|
return RCTScriptBCBundle;
|
||||||
|
}
|
||||||
|
|
||||||
|
return RCTScriptString;
|
||||||
|
}
|
||||||
|
|
||||||
@implementation RCTLoadingProgress
|
@implementation RCTLoadingProgress
|
||||||
|
|
||||||
- (NSString *)description
|
- (NSString *)description
|
||||||
|
@ -112,7 +126,7 @@ RCT_NOT_IMPLEMENTED(- (instancetype)init)
|
||||||
return nil;
|
return nil;
|
||||||
}
|
}
|
||||||
|
|
||||||
uint32_t magicNumber;
|
RCTMagicNumber magicNumber = {.allBytes = 0};
|
||||||
size_t readResult = fread(&magicNumber, sizeof(magicNumber), 1, bundle);
|
size_t readResult = fread(&magicNumber, sizeof(magicNumber), 1, bundle);
|
||||||
fclose(bundle);
|
fclose(bundle);
|
||||||
if (readResult != 1) {
|
if (readResult != 1) {
|
||||||
|
@ -125,13 +139,13 @@ RCT_NOT_IMPLEMENTED(- (instancetype)init)
|
||||||
return nil;
|
return nil;
|
||||||
}
|
}
|
||||||
|
|
||||||
magicNumber = NSSwapLittleIntToHost(magicNumber);
|
RCTScriptTag tag = RCTParseMagicNumber(magicNumber);
|
||||||
if (magicNumber != RCTRAMBundleMagicNumber) {
|
if (tag == RCTScriptString) {
|
||||||
if (error) {
|
if (error) {
|
||||||
*error = [NSError errorWithDomain:RCTJavaScriptLoaderErrorDomain
|
*error = [NSError errorWithDomain:RCTJavaScriptLoaderErrorDomain
|
||||||
code:RCTJavaScriptLoaderErrorCannotBeLoadedSynchronously
|
code:RCTJavaScriptLoaderErrorCannotBeLoadedSynchronously
|
||||||
userInfo:@{NSLocalizedDescriptionKey:
|
userInfo:@{NSLocalizedDescriptionKey:
|
||||||
@"Cannot load non-RAM bundled files synchronously"}];
|
@"Cannot load text/javascript files synchronously"}];
|
||||||
}
|
}
|
||||||
return nil;
|
return nil;
|
||||||
}
|
}
|
||||||
|
|
|
@ -66,6 +66,11 @@ struct RandomAccessBundleStartupCode {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct TaggedScript {
|
||||||
|
const RCTScriptTag tag;
|
||||||
|
const NSData *script;
|
||||||
|
};
|
||||||
|
|
||||||
#if RCT_PROFILE
|
#if RCT_PROFILE
|
||||||
@interface RCTCookieMap : NSObject
|
@interface RCTCookieMap : NSObject
|
||||||
{
|
{
|
||||||
|
@ -280,15 +285,20 @@ static NSThread *newJavaScriptThread(void)
|
||||||
JSContext:(JSContext *)context
|
JSContext:(JSContext *)context
|
||||||
error:(NSError **)error
|
error:(NSError **)error
|
||||||
{
|
{
|
||||||
BOOL isRAMBundle = NO;
|
TaggedScript taggedScript = loadTaggedScript(script, sourceURL, _performanceLogger, _randomAccessBundle, error);
|
||||||
script = loadPossiblyBundledApplicationScript(script, sourceURL, _performanceLogger, isRAMBundle, _randomAccessBundle, error);
|
|
||||||
if (!script) {
|
if (!taggedScript.script) {
|
||||||
return NO;
|
return NO;
|
||||||
}
|
}
|
||||||
if (isRAMBundle) {
|
|
||||||
|
if (taggedScript.tag == RCTScriptRAMBundle) {
|
||||||
registerNativeRequire(context, self);
|
registerNativeRequire(context, self);
|
||||||
}
|
}
|
||||||
NSError *returnedError = executeApplicationScript(script, sourceURL, _jscWrapper, _performanceLogger, _context.context.JSGlobalContextRef);
|
|
||||||
|
NSError *returnedError = executeApplicationScript(taggedScript, sourceURL,
|
||||||
|
_jscWrapper,
|
||||||
|
_performanceLogger,
|
||||||
|
_context.context.JSGlobalContextRef);
|
||||||
if (returnedError) {
|
if (returnedError) {
|
||||||
if (error) {
|
if (error) {
|
||||||
*error = returnedError;
|
*error = returnedError;
|
||||||
|
@ -652,16 +662,16 @@ RCT_EXPORT_METHOD(setContextName:(nonnull NSString *)contextName)
|
||||||
RCTAssertParam(script);
|
RCTAssertParam(script);
|
||||||
RCTAssertParam(sourceURL);
|
RCTAssertParam(sourceURL);
|
||||||
|
|
||||||
BOOL isRAMBundle = NO;
|
NSError *loadError;
|
||||||
{
|
TaggedScript taggedScript = loadTaggedScript(script, sourceURL,
|
||||||
NSError *error;
|
_performanceLogger,
|
||||||
script = loadPossiblyBundledApplicationScript(script, sourceURL, _performanceLogger, isRAMBundle, _randomAccessBundle, &error);
|
_randomAccessBundle,
|
||||||
if (script == nil) {
|
&loadError);
|
||||||
if (onComplete) {
|
if (!taggedScript.script) {
|
||||||
onComplete(error);
|
if (onComplete) {
|
||||||
}
|
onComplete(loadError);
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
RCTProfileBeginFlowEvent();
|
RCTProfileBeginFlowEvent();
|
||||||
|
@ -671,11 +681,13 @@ RCT_EXPORT_METHOD(setContextName:(nonnull NSString *)contextName)
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isRAMBundle) {
|
if (taggedScript.tag == RCTScriptRAMBundle) {
|
||||||
registerNativeRequire(self.context.context, self);
|
registerNativeRequire(self.context.context, self);
|
||||||
}
|
}
|
||||||
|
|
||||||
NSError *error = executeApplicationScript(script, sourceURL, self->_jscWrapper, self->_performanceLogger,
|
NSError *error = executeApplicationScript(taggedScript, sourceURL,
|
||||||
|
self->_jscWrapper,
|
||||||
|
self->_performanceLogger,
|
||||||
self->_context.context.JSGlobalContextRef);
|
self->_context.context.JSGlobalContextRef);
|
||||||
if (onComplete) {
|
if (onComplete) {
|
||||||
onComplete(error);
|
onComplete(error);
|
||||||
|
@ -683,33 +695,42 @@ RCT_EXPORT_METHOD(setContextName:(nonnull NSString *)contextName)
|
||||||
}];
|
}];
|
||||||
}
|
}
|
||||||
|
|
||||||
static NSData *loadPossiblyBundledApplicationScript(NSData *script, NSURL *sourceURL,
|
static TaggedScript loadTaggedScript(NSData *script,
|
||||||
RCTPerformanceLogger *performanceLogger,
|
NSURL *sourceURL,
|
||||||
BOOL &isRAMBundle, RandomAccessBundleData &randomAccessBundle,
|
RCTPerformanceLogger *performanceLogger,
|
||||||
NSError **error)
|
RandomAccessBundleData &randomAccessBundle,
|
||||||
|
NSError **error)
|
||||||
{
|
{
|
||||||
RCT_PROFILE_BEGIN_EVENT(0, @"executeApplicationScript / prepare bundle", nil);
|
RCT_PROFILE_BEGIN_EVENT(0, @"executeApplicationScript / prepare bundle", nil);
|
||||||
|
|
||||||
// The RAM bundle has a magic number in the 4 first bytes `(0xFB0BD1E5)`.
|
RCTMagicNumber magicNumber = {.allBytes = 0};
|
||||||
uint32_t magicNumber = 0;
|
|
||||||
[script getBytes:&magicNumber length:sizeof(magicNumber)];
|
[script getBytes:&magicNumber length:sizeof(magicNumber)];
|
||||||
isRAMBundle = NSSwapLittleIntToHost(magicNumber) == RCTRAMBundleMagicNumber;
|
RCTScriptTag tag = RCTParseMagicNumber(magicNumber);
|
||||||
if (isRAMBundle) {
|
|
||||||
[performanceLogger markStartForTag:RCTPLRAMBundleLoad];
|
NSData *loadedScript = NULL;
|
||||||
script = loadRAMBundle(sourceURL, error, randomAccessBundle);
|
switch (tag) {
|
||||||
[performanceLogger markStopForTag:RCTPLRAMBundleLoad];
|
case RCTScriptRAMBundle:
|
||||||
[performanceLogger setValue:script.length forTag:RCTPLRAMStartupCodeSize];
|
[performanceLogger markStartForTag:RCTPLRAMBundleLoad];
|
||||||
} else {
|
|
||||||
// JSStringCreateWithUTF8CString expects a null terminated C string.
|
loadedScript = loadRAMBundle(sourceURL, error, randomAccessBundle);
|
||||||
// RAM Bundling already provides a null terminated one.
|
|
||||||
NSMutableData *nullTerminatedScript = [NSMutableData dataWithCapacity:script.length + 1];
|
[performanceLogger markStopForTag:RCTPLRAMBundleLoad];
|
||||||
[nullTerminatedScript appendData:script];
|
[performanceLogger setValue:loadedScript.length forTag:RCTPLRAMStartupCodeSize];
|
||||||
[nullTerminatedScript appendBytes:"" length:1];
|
break;
|
||||||
script = nullTerminatedScript;
|
|
||||||
|
case RCTScriptBCBundle:
|
||||||
|
loadedScript = script;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case RCTScriptString: {
|
||||||
|
NSMutableData *nullTerminatedScript = [NSMutableData dataWithData:script];
|
||||||
|
[nullTerminatedScript appendBytes:"" length:1];
|
||||||
|
loadedScript = nullTerminatedScript;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
RCT_PROFILE_END_EVENT(RCTProfileTagAlways, @"");
|
RCT_PROFILE_END_EVENT(RCTProfileTagAlways, @"");
|
||||||
return script;
|
return { .tag = tag, .script = loadedScript };
|
||||||
}
|
}
|
||||||
|
|
||||||
static void registerNativeRequire(JSContext *context, RCTJSCExecutor *executor)
|
static void registerNativeRequire(JSContext *context, RCTJSCExecutor *executor)
|
||||||
|
@ -718,22 +739,46 @@ static void registerNativeRequire(JSContext *context, RCTJSCExecutor *executor)
|
||||||
context[@"nativeRequire"] = ^(NSNumber *moduleID) { [weakExecutor _nativeRequire:moduleID]; };
|
context[@"nativeRequire"] = ^(NSNumber *moduleID) { [weakExecutor _nativeRequire:moduleID]; };
|
||||||
}
|
}
|
||||||
|
|
||||||
static NSError *executeApplicationScript(NSData *script, NSURL *sourceURL, RCTJSCWrapper *jscWrapper,
|
static NSError *executeApplicationScript(TaggedScript taggedScript,
|
||||||
RCTPerformanceLogger *performanceLogger, JSGlobalContextRef ctx)
|
NSURL *sourceURL,
|
||||||
|
RCTJSCWrapper *jscWrapper,
|
||||||
|
RCTPerformanceLogger *performanceLogger,
|
||||||
|
JSGlobalContextRef ctx)
|
||||||
{
|
{
|
||||||
RCT_PROFILE_BEGIN_EVENT(0, @"executeApplicationScript / execute script", (@{
|
RCT_PROFILE_BEGIN_EVENT(0, @"executeApplicationScript / execute script", (@{
|
||||||
@"url": sourceURL.absoluteString, @"size": @(script.length)
|
@"url": sourceURL.absoluteString, @"size": @(taggedScript.script.length)
|
||||||
}));
|
}));
|
||||||
|
|
||||||
[performanceLogger markStartForTag:RCTPLScriptExecution];
|
[performanceLogger markStartForTag:RCTPLScriptExecution];
|
||||||
JSValueRef jsError = NULL;
|
JSValueRef jsError = NULL;
|
||||||
JSStringRef execJSString = jscWrapper->JSStringCreateWithUTF8CString((const char *)script.bytes);
|
|
||||||
JSStringRef bundleURL = jscWrapper->JSStringCreateWithUTF8CString(sourceURL.absoluteString.UTF8String);
|
JSStringRef bundleURL = jscWrapper->JSStringCreateWithUTF8CString(sourceURL.absoluteString.UTF8String);
|
||||||
jscWrapper->JSEvaluateScript(ctx, execJSString, NULL, bundleURL, 0, &jsError);
|
|
||||||
|
switch (taggedScript.tag) {
|
||||||
|
case RCTScriptRAMBundle:
|
||||||
|
/* fallthrough */
|
||||||
|
case RCTScriptString: {
|
||||||
|
JSStringRef execJSString = jscWrapper->JSStringCreateWithUTF8CString((const char *)taggedScript.script.bytes);
|
||||||
|
jscWrapper->JSEvaluateScript(ctx, execJSString, NULL, bundleURL, 0, &jsError);
|
||||||
|
jscWrapper->JSStringRelease(execJSString);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case RCTScriptBCBundle: {
|
||||||
|
file_ptr source(fopen(sourceURL.path.UTF8String, "r"), fclose);
|
||||||
|
int sourceFD = fileno(source.get());
|
||||||
|
|
||||||
|
jscWrapper->JSEvaluateBytecodeBundle(ctx, NULL, sourceFD, bundleURL, &jsError);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
jscWrapper->JSStringRelease(bundleURL);
|
jscWrapper->JSStringRelease(bundleURL);
|
||||||
jscWrapper->JSStringRelease(execJSString);
|
|
||||||
[performanceLogger markStopForTag:RCTPLScriptExecution];
|
[performanceLogger markStopForTag:RCTPLScriptExecution];
|
||||||
|
|
||||||
NSError *error = jsError ? RCTNSErrorFromJSErrorRef(jsError, ctx, jscWrapper) : nil;
|
NSError *error = jsError
|
||||||
|
? RCTNSErrorFromJSErrorRef(jsError, ctx, jscWrapper)
|
||||||
|
: nil;
|
||||||
|
|
||||||
RCT_PROFILE_END_EVENT(0, @"js_call");
|
RCT_PROFILE_END_EVENT(0, @"js_call");
|
||||||
return error;
|
return error;
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,6 +25,7 @@ typedef JSStringRef (*JSValueCreateJSONStringFuncType)(JSContextRef, JSValueRef,
|
||||||
typedef bool (*JSValueIsUndefinedFuncType)(JSContextRef, JSValueRef);
|
typedef bool (*JSValueIsUndefinedFuncType)(JSContextRef, JSValueRef);
|
||||||
typedef bool (*JSValueIsNullFuncType)(JSContextRef, JSValueRef);
|
typedef bool (*JSValueIsNullFuncType)(JSContextRef, JSValueRef);
|
||||||
typedef JSValueRef (*JSEvaluateScriptFuncType)(JSContextRef, JSStringRef, JSObjectRef, JSStringRef, int, JSValueRef *);
|
typedef JSValueRef (*JSEvaluateScriptFuncType)(JSContextRef, JSStringRef, JSObjectRef, JSStringRef, int, JSValueRef *);
|
||||||
|
typedef JSValueRef (*JSEvaluateBytecodeBundleFuncType)(JSContextRef, JSObjectRef, int, JSStringRef, JSValueRef *);
|
||||||
|
|
||||||
typedef struct RCTJSCWrapper {
|
typedef struct RCTJSCWrapper {
|
||||||
JSStringCreateWithCFStringFuncType JSStringCreateWithCFString;
|
JSStringCreateWithCFStringFuncType JSStringCreateWithCFString;
|
||||||
|
@ -41,6 +42,7 @@ typedef struct RCTJSCWrapper {
|
||||||
JSValueIsUndefinedFuncType JSValueIsUndefined;
|
JSValueIsUndefinedFuncType JSValueIsUndefined;
|
||||||
JSValueIsNullFuncType JSValueIsNull;
|
JSValueIsNullFuncType JSValueIsNull;
|
||||||
JSEvaluateScriptFuncType JSEvaluateScript;
|
JSEvaluateScriptFuncType JSEvaluateScript;
|
||||||
|
JSEvaluateBytecodeBundleFuncType JSEvaluateBytecodeBundle;
|
||||||
Class JSContext;
|
Class JSContext;
|
||||||
Class JSValue;
|
Class JSValue;
|
||||||
} RCTJSCWrapper;
|
} RCTJSCWrapper;
|
||||||
|
|
|
@ -26,7 +26,7 @@ static void *RCTCustomLibraryHandler(void)
|
||||||
static dispatch_once_t token;
|
static dispatch_once_t token;
|
||||||
static void *handler;
|
static void *handler;
|
||||||
dispatch_once(&token, ^{
|
dispatch_once(&token, ^{
|
||||||
handler = dlopen("@executable_path/Frameworks/JSC.framework/JSC", RTLD_LAZY | RTLD_LOCAL);
|
handler = dlopen("@loader_path/Frameworks/JSC.framework/JSC", RTLD_LAZY | RTLD_LOCAL);
|
||||||
if (!handler) {
|
if (!handler) {
|
||||||
const char *err = dlerror();
|
const char *err = dlerror();
|
||||||
|
|
||||||
|
@ -57,6 +57,7 @@ static void RCTSetUpSystemLibraryPointers(RCTJSCWrapper *wrapper)
|
||||||
wrapper->JSValueIsUndefined = JSValueIsUndefined;
|
wrapper->JSValueIsUndefined = JSValueIsUndefined;
|
||||||
wrapper->JSValueIsNull = JSValueIsNull;
|
wrapper->JSValueIsNull = JSValueIsNull;
|
||||||
wrapper->JSEvaluateScript = JSEvaluateScript;
|
wrapper->JSEvaluateScript = JSEvaluateScript;
|
||||||
|
wrapper->JSEvaluateBytecodeBundle = NULL;
|
||||||
wrapper->JSContext = [JSContext class];
|
wrapper->JSContext = [JSContext class];
|
||||||
wrapper->JSValue = [JSValue class];
|
wrapper->JSValue = [JSValue class];
|
||||||
}
|
}
|
||||||
|
@ -83,6 +84,7 @@ static void RCTSetUpCustomLibraryPointers(RCTJSCWrapper *wrapper)
|
||||||
wrapper->JSValueIsUndefined = (JSValueIsUndefinedFuncType)dlsym(libraryHandle, "JSValueIsUndefined");
|
wrapper->JSValueIsUndefined = (JSValueIsUndefinedFuncType)dlsym(libraryHandle, "JSValueIsUndefined");
|
||||||
wrapper->JSValueIsNull = (JSValueIsNullFuncType)dlsym(libraryHandle, "JSValueIsNull");
|
wrapper->JSValueIsNull = (JSValueIsNullFuncType)dlsym(libraryHandle, "JSValueIsNull");
|
||||||
wrapper->JSEvaluateScript = (JSEvaluateScriptFuncType)dlsym(libraryHandle, "JSEvaluateScript");
|
wrapper->JSEvaluateScript = (JSEvaluateScriptFuncType)dlsym(libraryHandle, "JSEvaluateScript");
|
||||||
|
wrapper->JSEvaluateBytecodeBundle = (JSEvaluateBytecodeBundleFuncType)dlsym(libraryHandle, "JSEvaluateBytecodeBundle");
|
||||||
wrapper->JSContext = (__bridge Class)dlsym(libraryHandle, "OBJC_CLASS_$_JSContext");
|
wrapper->JSContext = (__bridge Class)dlsym(libraryHandle, "OBJC_CLASS_$_JSContext");
|
||||||
wrapper->JSValue = (__bridge Class)dlsym(libraryHandle, "OBJC_CLASS_$_JSValue");
|
wrapper->JSValue = (__bridge Class)dlsym(libraryHandle, "OBJC_CLASS_$_JSValue");
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue