Why do we use CEF? In many cases, it is to realize the mutual call between JavaScript and native C + +. That is, when JavaScript in the web page is called, it triggers the execution of local C + + code, such as accessing hardware and other functions that JavaScript cannot complete. The local C + + code can also call back JavaScript. For example, after receiving some notifications from the operating system, the local code will transfer the notification content to the JavaScript code for execution.
In CEF, JavaScript code is running in Renderer, which is very important to understand. The place where native code is written is inappropriate, and JavaScript code cannot work normally when called. CEF uses the V8 JS engine to execute internal JS. Each Frame has its own JS Context in the browser process, and they all run in the Renderer process.
How can we use native C + + code to extend JavaScript execution? Mainly through two methods in CefRenderProcessHandler:
-
virtual void OnWebKitInitialized() {}
After the webkit is initialized. In this function, we can register the "mapping" relationship between JavaScript and C + + code through the cefregistereextension() function, which is officially called extension
-
virtual void OnContextCreated(CefRefPtr<CefBrowser> browser, CefRefPtr<CefFrame> frame, CefRefPtr<CefV8Context> context) {}
After the JavaScript context is created, in this function, we can bind properties and methods for window objects in JavaScript. Officially, this method is called window binding
Either way, it must be done in the rendering process.
The code in this section is completed in the QyRender project, Reference documents
1. Extension
Here is a demonstration of how to extend AES encryption and decryption for JavaScript. This section uses QT-AES This open source library is used to realize AES encryption and decryption.
1.1 prepare QT-AES
Download to GitHub QT-AES Source code, just qaesencryption.cpp and qaesencryption.h These two files can be copied into our project.
1.2 QyAppRenderer class
Qyapprender inherits from CefApp and CefRenderProcessHandler and needs to override the OnWebKitInitialized method:
// QyAppRenderer.h class QyAppRenderer :public CefApp, public CefRenderProcessHandler { public: QyAppRenderer(); // ... ellipsis void OnWebKitInitialized() OVERRIDE; // ... ellipsis }; // QyAppRenderer.cpp void QyAppRenderer::OnWebKitInitialized() { qDebug() << "=====OnWebKitInitialized======="; }
1.3 expansion
In the OnWebKitInitialized method, we need to complete the extension through the following steps:
1.3. 1 define JS text
Define a JavaScript text string in which native function is declared. In this example, three methods are demonstrated:
- app.encrypt(originText) / / encrypt. The parameter is the string to be encrypted
- app.decrypt(encryptText) / / decrypt. The parameter is the ciphertext string to decrypt
- app.sayHello(callback) / / the parameter is a callback function callback =function(data) {...}
var app; if(!app){ app={}; (function(){ app.encrypt=function(originText){ native function encrypt();//Declare local method name return encrypt(originText); //Execute local method } app.decrypt=function(encryptText){ native function decrypt();//Declare local method name return decrypt(encryptText); //Execute local method } app.sayHello=function(callback) { native function sayHello();//Declare local method name return sayHello(callback);//Execute local method } })(); //JavaScript self executing function }
If this JavaScript code is defined as a string and the format is not beautiful, put it into the resource file of QT project and read it from the resource file when using it:
First, create a folder resources under the project QyRender, and then create extension in this folder_ js. JS file, the content is the above JavaScript code:
Then add a QT resource file: extension_ js. qrc
Add extension_ js. JS file is added to the resource file
C++ 11 provides the syntax of multi line strings, which can also be used to directly define JavaScript code into strings.
std::string jsCode = R"( var app; if(!app){ app={}; .... } )";
1.3. 2. Implement CefV8Handler interface
The native code needs to implement the CefV8Handler interface. For convenience, it is written directly in qyapprender Cpp file:
// QyAppRenderer.cpp #pragma execution_character_set("UTF-8") #include "QyAppRenderer.h" #include <QDebug> #include <QProcess> #include "encrypt/qaesencryption.h" #include <QCryptographicHash> #include <QDateTime> namespace { } class AppNativeV8Handler : public CefV8Handler { public: AppNativeV8Handler() {} bool Execute(const CefString& name, //JavaScript function name CefRefPtr<CefV8Value> object, //JavaScript function holder const CefV8ValueList& arguments,//JavaScript parameters CefRefPtr<CefV8Value>& retval, // JavaScript return value CefString& exception) override { // encryption if (name == "encrypt") { std::string originText = arguments.at(0).get()->GetStringValue(); QString rtText = encrypt(QString::fromStdString(originText)); //Return value to retval retval = CefV8Value::CreateString(rtText.toStdString()); return true; } // decrypt if (name == "decrypt") { std::string encryptText = arguments.at(0).get()->GetStringValue(); QString rtText = decrypt(QString::fromStdString(encryptText)); retval = CefV8Value::CreateString(rtText.toStdString()); return true; } // sayHello JavaScript function with callback // The name is sayHello, the number of parameters is 1, and the type is a function if (name == "sayHello" && arguments.size() == 1 && arguments[0]->IsFunction()) { // Get callback function CefRefPtr<CefV8Value> callback = arguments[0]; QDateTime now = QDateTime::currentDateTime();//Get the current system time time_t nowTime = now.toTime_t(); //Convert to CefDate type CefTime cefNow(nowTime); CefRefPtr<CefV8Value> callbackFunctionParam = CefV8Value::CreateDate(cefNow); CefV8ValueList arguments; arguments.push_back(callbackFunctionParam); // Execute the JavaScript callback and pass the parameter to it, which is a CefV8ValueList callback->ExecuteFunction(NULL, arguments); return true; } return false; } private: // aes encryption algorithm key QString _aes_secret_key = "a0123456789"; /* * AES Algorithm string encryption */ QString encrypt(QString originText) { QAESEncryption encryption(QAESEncryption::AES_128, QAESEncryption::ECB, QAESEncryption::ZERO); QByteArray hashKey = QCryptographicHash::hash(_aes_secret_key.toUtf8(), QCryptographicHash::Md5); QByteArray encodedText = encryption.encode(originText.toUtf8(), hashKey); QString str_encode_text = QString::fromLatin1(encodedText.toBase64()); qDebug() << "encrypt:" << originText << " result:" << str_encode_text; return str_encode_text; } /* * AES Algorithm string decryption */ QString decrypt(QString encryptText) { QAESEncryption encryption(QAESEncryption::AES_128, QAESEncryption::ECB, QAESEncryption::ZERO); QByteArray hashKey = QCryptographicHash::hash(_aes_secret_key.toUtf8(), QCryptographicHash::Md5); QByteArray decodedText = encryption.decode(QByteArray::fromBase64(encryptText.toLatin1()), hashKey); qDebug() << "decrypt:" << encryptText << " result:" << QString::fromUtf8(decodedText); return QString::fromUtf8(decodedText); } IMPLEMENT_REFCOUNTING(AppNativeV8Handler); };
The code is relatively long. In fact, there is only one method here. native c + + code is executed here, and JavaScript function names and other information are passed through parameters. In this method, we judge what code should be executed by judging the name of the function. Common JavaScript types have corresponding types in CEF. For details, see Reference documents
bool Execute(const CefString& name, //JavaScript function name CefRefPtr<CefV8Value> object, //JavaScript function holder const CefV8ValueList& arguments,//JavaScript parameters CefRefPtr<CefV8Value>& retval, // JavaScript return value CefString& exception) override
1.3. 3 implementation of onwebkitinitialized function
Implement the OnWebKitInitialized function in the qyapprender class:
// QyAppRenderer.cpp // ... ellipsis void QyAppRenderer::OnWebKitInitialized() { //1. Get the JavaScript code to be extended from the resource file QFile jsFile(":/extention_js.js"); jsFile.open(QIODevice::ReadOnly); QByteArray jsFileData = jsFile.readAll(); //2. JavaScript string QString jsCode(jsFileData); // 3. Register app extension module // When calling a function in JavaScript, you will look in the CefV8Handler list registered through CefRegisterExtension // Find the CefV8HandlerImpl corresponding to "v8/app", and call its Execute method CefRefPtr<CefV8Handler> v8Handler = new AppNativeV8Handler(); CefRegisterExtension("v8/app", jsCode.toStdString(), v8Handler); } // ... ellipsis
1.3. 4 use in web pages
- Enter the string in the first text box and click "Encrypt", and the result will be displayed in the second text box
- Click "decrypt" and the decryption result will be displayed below
- Click the "test app.sayHello" button, and the c + + code will call back the callback function passed in by sayHello
<!-- index.html --> <div> <input id="msg" type="text" /> <button id="btnEncrypt">encryption</button> <br /> Encryption result: <input id="msgEncryptResult" type="text" /> <button id="btnDncrypt">decrypt</button><br /> <button id="btnTestSayHello">test app.sayHello</button><br /> </div> <div id="dncryptInfo"> </div> <script src="js/jquery-3.6.0.min.js"></script> <script src="js/index.js"></script>
// index.js console.log(app); window.onload = () => { $("#btnEncrypt").click(() => { // encryption var result = app.encrypt($("#msg").val()); $("#msgEncryptResult").val(result); }); $("#btnDncrypt").click(() => { // decrypt var result = app.decrypt($("#msgEncryptResult").val()); $("#dncryptInfo").html(" decryption result: "+ result"); }); $("#btnTestSayHello").click(() => { // c + + callback function, data is the time object passed in by c + + app.sayHello((data) => { console.log(data); console.log("Hello" + data); }); }); }
Start the application and press F12 to open the developer tool:
You can see that the "app" object is successfully registered. The app object contains three functions: decrypt, encrypt and sayhello
Click "test app.sayHello", and the console outputs:
1.3. 5 extended summary
- The rendering process CefApp implements the OnWebKitInitialized function in the CefRenderProcessHandler interface
- Define the JavaScript code text in the OnWebKitInitialized function, and declare the native method in the JavaScript code text.
- The C + + code for the specific execution of the native method should be completed by overriding the Execute method in the CefV8Handler class.
- Associate JavaScript code with CefV8Handler through the function CefRegisterExtension.
The values of data types corresponding to JavaScript can be created through CefV8Value:: CreateXXX. The types of values of various data types in JavaScript are CefV8Value
In CefV8Value
- CefRefPtr<CefV8Value> ExecuteFunction(CefRefPtr<CefV8Value> object,const CefV8ValueList& arguments)
- virtual CefRefPtr<CefV8Value> ExecuteFunctionWithContext(CefRefPtr<CefV8Context> context,CefRefPtr<CefV8Value> object,const CefV8ValueList& arguments)
These two methods can execute JavaScript code,
2. Window binding
Window binding can also realize the mutual call between JavaScript and C + + code, and its idea is the same as that of extension.
This section implements binding a read-only global object window for the window object in JavaScript Appenv, which contains:
- Cpunname: CPU name
- cpuSn:cpu serial number
- encrypt: encryption method (AES encryption amplification above)
- decrypt: decryption method (the above AES decryption method)
- hddSn: hard disk serial number
- ipAddr: native IP address
- macAddr: mac address of the local network card
- Memory: native memory
Note: windows 10 operating system is collected here
When the browser JavaScript context is created, Dom has been parsed. In the OnContextCreated function of CefRenderProcessHandler interface, you can bind various JavaScript objects (strings, objects, functions, etc.) to JavaScript window objects.
2.1 OnContextCreated function
void QyAppRenderer::OnContextCreated(CefRefPtr<CefBrowser> browser, CefRefPtr<CefFrame> frame, CefRefPtr<CefV8Context> context) { ... }
- Browser: current browser object
- Frame: the frame object of the current window
- Context: Javascript context
2.2 method of obtaining hardware information
The following code is used to obtain the hardware information. The hardware information is saved in the AppEnv structure. Here, the system hardware information is obtained by calling the wmic command in windows
// QyAppRenderer.cpp namespace { // Get cpu name const QString CPU_NAME_CMD = "wmic cpu get name"; // Query cpu serial number const QString CPU_ID_CMD = "wmic cpu get processorid"; // View hard disk serial number const QString DISK_NUM_CMD = "wmic diskdrive where index=0 get serialnumber"; // Query network card and IP address const QString IP_MAC_ADDRESS_CMD = "wmic PATH Win32_NetworkAdapterConfiguration WHERE \"IPEnabled = True and not MACAddress like '00:%'\" get Ipaddress,MACAddress"; // Memory size const QString MEM_SIZE_CMD = "wmic ComputerSystem get TotalPhysicalMemory"; //IP address regular expression QString IP_REG_PATTERN = "((2[0-4]\\d|25[0-5]|[01]?\\d\\d?)\\.){3}(2[0-4]\\d|25[0-5]|[01]?\\d\\d?)"; //MAC address regular expression QString MAC_REG_PATTERN = "([0-9a-fA-F]{2})(([/\\s:][0-9a-fA-F]{2}){5})"; //Save data to structure typedef struct { QString cpuSn; //cpu serial number QString cpuName; // cpu name QString ipAddr; //IP address QString macAddr; //Network card mac address QString hddSn; // Hard disk serial number qint64 memory; //Memory size in bytes } AppEnv; // Execute the command to get the returned result QString getWMIC(const QString& cmd) { QProcess p; p.start(cmd); p.waitForFinished(); QString result = QString::fromLocal8Bit(p.readAllStandardOutput()); QStringList list = cmd.split(" "); result = result.remove(list.last(), Qt::CaseInsensitive); result = result.replace("\r", ""); result = result.replace("\n", ""); result = result.simplified(); return result; } // Get application running environment static AppEnv getAppEnv() { AppEnv appEnv; appEnv.cpuName = getWMIC(CPU_NAME_CMD); // CPU name appEnv.cpuSn = getWMIC(CPU_ID_CMD); // CPU serial number QString ipMac = getWMIC(IP_MAC_ADDRESS_CMD); //IP address and mac address QRegExp ipRx(IP_REG_PATTERN); //IP regularization if (ipMac.indexOf(ipRx) >= 0) { appEnv.ipAddr = ipRx.cap(0); } QRegExp macRx(MAC_REG_PATTERN);//MAC regularity if (ipMac.indexOf(macRx) >= 0) { appEnv.macAddr = macRx.cap(0); } appEnv.hddSn = getWMIC(DISK_NUM_CMD); //Hard disk appEnv.memory = getWMIC(MEM_SIZE_CMD).toLongLong(); return appEnv; } }
2.3 implementation of oncontextcreated function
In this function, it is mainly to create CefV8Value corresponding to various JavaScript objects. If a JavaScript function is created, CefV8Value::CreateFunction is required to create it. CefV8Handler is required to create the function. CefV8Handler is used to create the C + + code corresponding to the native JavaScript function.
If C + + wants to call JavaScript code, it only needs to call ExecuteFunction or ExecuteFunctionWithContext with CefV8Value
void QyAppRenderer::OnContextCreated(CefRefPtr<CefBrowser> browser, CefRefPtr<CefFrame> frame, CefRefPtr<CefV8Context> context) { // Get the hardware information of app running environment AppEnv appEnv = ::getAppEnv(); Retrieve the context's window object. CefRefPtr<CefV8Value> window = context->GetGlobal(); //Create a JavaScript object with all read-only properties CefRefPtr<CefV8Value> appEnvObject = CefV8Value::CreateObject(NULL, NULL); // Setting property values for JavaScript objects appEnvObject->SetValue("cpuSn", CefV8Value::CreateString(appEnv.cpuSn.toStdString()), V8_PROPERTY_ATTRIBUTE_READONLY); appEnvObject->SetValue("cpuName", CefV8Value::CreateString(appEnv.cpuName.toStdString()), V8_PROPERTY_ATTRIBUTE_READONLY); appEnvObject->SetValue("ipAddr", CefV8Value::CreateString(appEnv.ipAddr.toStdString()), V8_PROPERTY_ATTRIBUTE_READONLY); appEnvObject->SetValue("macAddr", CefV8Value::CreateString(appEnv.macAddr.toStdString()), V8_PROPERTY_ATTRIBUTE_READONLY); appEnvObject->SetValue("hddSn", CefV8Value::CreateString(appEnv.hddSn.toStdString()), V8_PROPERTY_ATTRIBUTE_READONLY); appEnvObject->SetValue("memory", CefV8Value::CreateString(QString::number(appEnv.memory).toStdString()), V8_PROPERTY_ATTRIBUTE_READONLY); // JavaScript functions need to be handled by CefV8Handler. The AppNativeV8Handler defined earlier is used here CefRefPtr<CefV8Handler> handler = new AppNativeV8Handler(); CefRefPtr<CefV8Value> funcEncrypt = CefV8Value::CreateFunction("encrypt", handler); CefRefPtr<CefV8Value> funcDecrypt = CefV8Value::CreateFunction("decrypt", handler); appEnvObject->SetValue("encrypt", funcEncrypt, V8_PROPERTY_ATTRIBUTE_NONE); appEnvObject->SetValue("decrypt", funcDecrypt, V8_PROPERTY_ATTRIBUTE_NONE); //It is bound to the window object and is also a read-only attribute window->SetValue("appEnv", appEnvObject, V8_PROPERTY_ATTRIBUTE_READONLY); }
2.4 test whether the binding is successful
In index JS, print out window Appenv to view
// index.js if (window.appEnv) { console.log(window.appEnv); }
Start the application and open F12:
The next chapter will try how to implement asynchronous calls between JavaScript and C + +.