用 Addon 增強 Node.js 和 Electron 應用的原生能力

前言

Node.js Addon 是 Node.js 中為 JavaScript 環境提供 C/C++ 交互能力的機制。其形態十分類似 Java 的 JNI,都是通過提供一套 C/C++ SDK,用于在 C/C++ 中創建函數方法、進行數據轉換,以便 JavaScript / Java 等語言進行調用。這樣編寫的代碼通常叫做 Bindings。

此外還有基于 C ABI Calling Convention (例如 stdcall / System-V 等標準) 直接進行跨語言調用的方案,例如 Rust FFI、Python 的 ctypes、Node.js 的 ffi 包等。這兩者的差別在于 Rust 等原生語言是直接針對平臺來將函數調用編譯為機器碼,而 ctypes 和 ffi 包則是基于 libffi 動態生成機器碼來完成函數調用的。和 Node.js Addon 的差別則在于調用和類型轉換的開銷上。

本文將圍繞 Node.js Addon 進行介紹,即創建一個 Bindings 來增強 Node.js 或 Electron 應用的原生能力,使其可以和系統進行交互,或者使用一些基于 C/C++ 編寫的第三方庫。

Node.js Electron 的關系

Electron 在主進程和渲染進程中都包含了完整的 Node.js 環境,因此本文既適用于 Node.js 程序,也適用于 Electron 程序。

Node.js Addon 的類型

在 Node.js 的 Addon,有三種類型:

54dbdf1a2152fd46607fbefdad021e86.png

本文主要介紹 Node-API 的原理,以及以 node-addon-api 作為例子。

Node-API 基本原理

9ec850ac30e6f4e3b55173fc0446e6f7.png

Node.js 本質上是一個動態鏈接庫(即 Windows 下的 .dll 文件、MacOS 下的 .dylib 文件、Linux 下的 .so 文件),只不過在分發時會將文件的擴展名改為 .node

加載

Node.js Addon 通常通過 CommonJS 的 require 函數進行導入和初始化。require 在被 .node 擴展名路徑作為參數進行調用的情況下,最終會利用 dlopen(Windows 下是 LoadLibrary)方法來動態加載這個以 .node 擴展名的動態鏈接庫:

a7df3f7402cd8a18fe174dba16a5d675.png

初始化

以 https://github.com/nodejs/node-addon-examples/blob/main/1_hello_world/napi/hello.c 作為參考:

static?napi_value?Init(napi_env?env,?napi_value?exports)?{napi_status?status;napi_property_descriptor?desc?=?DECLARE_NAPI_METHOD("hello",?Method);status?=?napi_define_properties(env,?exports,?1,?&desc);assert(status?==?napi_ok);return?exports;
}NAPI_MODULE(NODE_GYP_MODULE_NAME,?Init)

NAPI_MODULE 宏用來綁定一個 C 函數作為初始化函數。這個函數中可以用來給模塊的 exports 對象添加所需要的功能。

例如上述的代碼中,給 exports 添加了一個叫做 hello 的函數。這樣一來,我們在 Node.js 中 require 這個模塊之后,就能獲得到一個包含 hello 函數的 exports 對象:

ea1c84af9017b88cae4f62b4e95bf250.png

調用

以 https://github.com/nodejs/node-addon-examples/blob/main/1_hello_world/napi/hello.c 作為參考:

static?napi_value?Method(napi_env?env,?napi_callback_info?info)?{napi_status?status;napi_value?world;status?=?napi_create_string_utf8(env,?"world",?5,?&world);assert(status?==?napi_ok);return?world;
}

Method 本身是一個 C 函數,接受 napi_env 作為 JavaScript 的上下文信息。napi_callback_info 作為當前函數調用的信息,例如函數參數等。返回一個 napi_value 作為函數的返回結果。

從這個函數的例子中可以看到,在 C 中是可以獲取到函數的調用參數,并且產生一個值作為函數的返回結果。稍后我們會以 node-addon-api 作為例子來具體介紹其編寫方式。

c52925e33badd8e36c14c387e717c02d.png

模塊編寫指南

本節介紹使用 C++ 配合 node-addon-api 開發模塊時常見的一些模式和樣板代碼,僅供參考。

更多用法詳見官方文檔:https://github.com/nodejs/node-addon-api/blob/main/doc/hierarchy.md

模塊初始化

使用 NODE_API_MODULE 宏綁定一個 C++ 函數進行模塊初始化:

Napi::Object?Init(Napi::Env?env,?Napi::Object?exports)?{exports.Set(Napi::String::New(env,?"hello"),Napi::Function::New(env,?Method));return?exports;
}NODE_API_MODULE(hello,?Init)
  • 其中 Napi::Env 是對 napi_env 的封裝,代表一個 JavaScript 上下文,大部分和 JavaScript 交互的場景都需要這個上下文,可以保存起來以供下次使用(但是不要跨線程使用)。

  • Napi::Object exports 則是這個模塊的 exports 對象,可以把想要給 JavaScript 暴露的值和函數都設置到這個上面。

創建 JavaScript 函數

首先需要創建一個如下函數簽名的 C++ 函數:

Napi::Value?Add(const?Napi::CallbackInfo&?info)?{Napi::Env?env?=?info.Env();double?arg0?=?info[0].As<Napi::Number>().DoubleValue();double?arg1?=?info[1].As<Napi::Number>().DoubleValue();Napi::Number?num?=?Napi::Number::New(env,?arg0?+?arg1);return?num;
}

其中函數的返回值可以是任何派生自 Napi::Value 的類型,也可以是 Napi::Value 本身。

獲取函數參數

通過 Napi::CallbackInfo& 來獲取函數參數,例如 info[0] 代表第一個參數。

info[n] 會獲取一個 Napi::Value 值,我們需要調用它的 As<T> 方法來轉換為具體的值,我們才能將它繼續轉換為 C/C++ 可用的數據類型。例如,我們希望將函數的第一個參數轉換為字符串,我們需要經過兩個步驟:

  1. 將 Napi::Value 轉換為 Napi::String:

Napi::String?js_str?=?info[0].As<Napi::String>();
  1. 將 Napi::String 轉換為 std::string

std::string?cpp_str?=?js_str.Utf8Value();

其他數據類型例如 Napi::NumberNapi::Buffer<T> 均有類似的方法。

返回函數結果

我們可以直接創建一個 JavaScript 值并在 C++ 函數中返回。具體創建值的方法詳見下一小節。

創建 JavaScript 值

我們可以利用各種實例化方法,來從 C/C++ 的數據類型中創建 JavaScript 的值,下面舉幾個常見的例子。

創建字符串
Napi::String::New(env,?"字符串內容")
創建數字
Napi::Number::New(env,?123)
創建 Buffer

創建 Buffer 是一個有風險的操作。Node-API 提供了兩種創建方式:

  • 提供一個指針和數據長度,創建一個數據的拷貝

    • ? 安全,首選這種方法

    • ? v8 會負責這個 Buffer 的垃圾回收

Napi::Buffer::Copy(napi_env?env,?const?T*?data,?size_t?length)
  • 直接基于指針和數據長度創建一個 External Buffer

    • ?? 同一個指針(相同的內存地址)只能創建一個 Buffer,重復創建會引起錯誤

    • ?? v8 / Node.js 不負責這個 Buffer 的內存管理

Napi::Buffer::New(napi_env?env,?const?T*?data,?size_t?length)

異步代碼

異步函數

異步函數通常用于實現一些異步 IO 任務、事件,例如實現一個異步網絡請求庫的綁定。

異步函數通常有兩種實現方式:回調 和 Promise。

同線程回調

同線程回調的使用場景比較少:

  • 使用了 libuv 來運行了一些異步任務,并且這個異步任務會在 libuv 主線程喚醒事件循環來返回結果,這時候可以比較安全地直接進行同線程回調。但是要求事先把 Napi::Env 保存在一個地方。

  • 實現一個函數的時候,在實現中直接同步調用一個 Napi::Function。

獲取函數

通常我們會從函數調用的參數中獲取到 Napi::Function,一般來說我們需要在當次調用就把這個函數給使用掉,避免后續被 v8 GC 回收。

持久化函數

如果我們確實需要在之后的其他時機去使用函數,我們需要將它通過 Napi::Persistent 持久化:

Napi::FunctionReference?func_persist?=?Napi::Persistent(func);

使用時,可以作為一個正常的函數去使用。

調用函數

無論是 Napi::Function 還是 Napi::FunctionReference,我們都可以通過 Call 方法來調用:

Napi::Value?ret_val?=?func_persist.Call({Napi::String::New(env,?"Arg0")
});
跨線程回調

跨線程回調是比較常見使用場景,因為我們通常會想在另外一個線程調用 JavaScript 函數。

使用線程安全函數 (ThreadSafeFunction)

為了在其他線程中調用 JavaScript 函數,我們需要基于 Napi::Function 去創建一個 Napi::ThreadSafeFunction

Napi::ThreadSafeFunction?tsfn?=?Napi::ThreadSafeFunction::New(env,?????????????????????//?Napi::Envinfo[0].As<Function>(),??//?JavaScript?函數"handler",???????????????//?異步函數的名稱,用于調試的識別0,???????????????????????//?隊列最大大小,通常指定為?0?代表沒有限制。如果隊列已滿則可能會導致調用時阻塞。1????????????????????????//?初始線程數量,通常指定為?1。實際上是作為內存管理使用。可參考這篇文檔。
);

接著就可以把 tsfn 保存在任何位置,并且并不需要同時保存一份 Napi::Env

調用線程安全函數

調用線程函數有兩種形式,一種是同步調用,另一種是異步調用。

同步調用

同步調用指的是如果我們限制了 ThreadSafeFunction 的隊列大小,并對其進行了多次調用,從而創建了許多調用任務,則會導致隊列已滿,調用就會被阻塞,直到成功插入隊列后返回結果。

這是進行一次同步調用的例子:

const?char*?value?=?"hello?world";
napi_status?status?=?tsfn.BlockingCall(value,?[](Napi::Env?env,?Napi::Function?callback,?const?char*?value)?{Napi::String?arg0?=?Napi::String::New(env,?value);callback.Call({?arg0?});
});

這樣一來就能順利地在任意線程去調用 JavaScript 函數。

但是我們發現,實際上我們并不能同步地獲取函數調用的返回結果。并且 Node-API 或者 node-addon-api 都沒有提供這么一種機制。但是我們可以借助 libuv 的信號量來達到這個目的。

uv_sem_t?sem;
uv_sem_init(&sem,?0);
const?char*?value?=?"hello?world";
Napi::Value?ret_val;
napi_status?status?=?tsfn.BlockingCall(value,?[&ret_val](Napi::Env?env,?Napi::Function?callback,?const?char*?value)?{Napi::String?arg0?=?Napi::String::New(env,?value);*ret_val?=?callback.Call({?arg0?});uv_sem_post(&sem);
});
uv_sem_wait(&sem);//?直至?JavaScript?運行結束并返回結果,才會走到這里
//?這里就可以直接使用?ret_val?了

異步調用

異步調用則會在隊列已滿時直接返回錯誤狀態而不進行函數調用。除此之外的使用方法同 “同步調用” 完全一致:

const?char*?value?=?"hello?world";
napi_status?status?=?tsfn.NonBlockingCall(value,?[](Napi::Env?env,?Napi::Function?callback,?const?char*?value)?{Napi::String?arg0?=?Napi::String::New(env,?value);callback.Call({?arg0?});
});
Promise
C++ 中創建 Promise 給 JavaScript 使用

我們通常會需要在 C++ 中實現異步函數。除了直接用上面已經介紹的基于回調的方法之外,我們還可以直接在 C++ 中創建一個 Promise。

Promise 只支持同 線程 調用

由于 Promise 并未提供跨線程 Resolve 的方式,因此如果希望在其他線程對 Promise 進行 Resolve 操作,則需要結合 libuv 來實現。此方法比較繁瑣,建議轉而使用跨線程回調函數。如果讀者感興趣,后續本文可以補充相關內容。

我們可以直接創建一個 Promise,并在函數中返回:

Napi::Value?YourFunction(const?Napi::CallbackInfo&?info)?{Napi::Promise::Deferred?deferred?=?Napi::Promise::Deferred::New(info.Env());//?我們可以把?env?和?Napi::Promise::Deferred?保存在任何地方。//?deferred_?會在?Resolve?或者?Reject?之后釋放。env_?=?info.Env();deferred_?=?deferred;return?deferred.Promise();
}

接著我們可以在其他地方調用 Napi::Promise::Deferred 來完成 Promise。注意,這里一定需要在主線程中調用:

//?返回成功結果
deferred_.Resolve(Napi::String::New(info.Env(),?"OK"));
//?返回錯誤
deferred_.Reject(Napi::String::New(info.Env(),?"Error"));
C++ 中使用來自 JavaScript 的 Promise

由于 Node-API 或者 node-addon-api 均沒有提供使用 Promise 的封裝,因此我們需要像在 JavaScript 中通過 .then 手動使用 Promise 的方式,在 C++ 中使用 Promise。

//?首先需要定義兩個函數,用來接受?Promise?成功和失敗
Napi::Value?ThenCallback(const?Napi::CallbackInfo?&info)?{?Napi::Value?result?=?info[0];//?result?是?Promise?的返回結果return?info.Env().Undefined();
}
Napi::Value?CatchCallback(const?Napi::CallbackInfo?&info)?{?Napi::Value?error?=?info[0];//?error?是?Promise?的錯誤信息return?info.Env().Undefined();
}Napi::Promise?promise?=?async_function.Call({}).As<Napi::Promise>()
Napi::Value?then_func?=?promise.Get("then").As<Napi::Function>();
then_func.Call(promise,?{?Napi::Function::New(env,?ThenCallback,?"then_callback")?});
Napi::Value?catch_func?=?promise.Get("catch").As<Napi::Function>();
catch_func.Call(promise,?{?Napi::Function::New(env,?CatchCallback,?"catch_callback")?});

顯然這種使用方式是比較繁瑣的,我們也可以通過一些辦法使其可以將 C++ Lambda 作為回調函數來使用,但是本文暫時不涉及這部分內容。

異步任務

異步任務通常是利用 libuv 提供的線程池來運行一些 CPU 密集型的工作。而對于一些跨線程異步回調的 Bindings 實現則直接使用 ThreadSafeFunction 即可。

具體使用可以參考:https://github.com/nodejs/node-addon-api/blob/main/doc/async_worker.md

Node-API 的構建

基本構建配置

Node.js Addon 通常使用 node-gyp 構建,這是一個基于 Google 的 gyp 構建系統實現的構建工具。至于為何是 gyp,因為 Node.js 是基于 gyp 構建的。

我們來看一個 node-addon-api 項目的構建配置,以 bindings.gyp 命名:

{"targets":?[{"target_name":?"hello","cflags!":?[?"-fno-exceptions"?],"cflags_cc!":?[?"-fno-exceptions"?],"sources":?[?"hello.cc"?],"include_dirs":?["<!@(node?-p?"require('node-addon-api').include")"],'defines':?[?'NAPI_DISABLE_CPP_EXCEPTIONS'?],}]
}

具體配置可以參考官方使用文檔:https://gyp.gsrc.io/docs/UserDocumentation.md

一些常識:

  • "sources" 中需要包含所有 C/C++ 代碼文件,不需要包含頭文件

  • "<!@(node -p "require('node-addon-api').include")" 在使用 Node-API 還是 node-addon-api 的情況下是不同的。

  • "target_name" 通常需要修改為你希望使用的擴展名稱,它會影響編譯產物的名稱。

常用構建命令

  • node-gyp rebuild 重新構建,會清理掉已有的構建緩存,推薦每次都使用這個命令來構建產物,避免出現奇怪的問題

    • 可以添加 --arch <ARCH> 參數來指定構建的目標架構,例如希望構建一個 32 位的產物,則可以使用 --arch ia32 來構建。

  • node-gyp clean 清理構建緩存。如果希望使用 node-gyp build 來進行構建的話,需要善用 clean 功能。

實用構建配置

添加頭文件目錄
'include_dirs':?['win32/x64/include'
]
在 Windows 下進行動態鏈接 / 靜態鏈接
'libraries':?['some_library.lib'
]
  • 對于動態鏈接,需要指定 .dll 對應的 .lib 文件,并在分發的時候將 .dll 放在 .node 相同的目錄下。

  • 對于靜態鏈接,則直接指定 .lib 文件即可。但是在 Node.js Addon 中進行靜態鏈接是一個比較費勁的事情,因為通常涉及到對其他靜態依賴的管理,需要謹慎選擇此方案。

在 Windows 下設置 C++ 版本
'msvs_settings':?{'VCCLCompilerTool':?{'AdditionalOptions':?['/std:c++20']}
}
在 Windows MSVC 下構建支持代碼文件中的 UTF-8 字符(中文注釋等)

本質上是給 MSVC 的編譯器添加一個 /utf-8 參數

'msvs_settings':?{'VCCLCompilerTool':?{"AdditionalOptions":?['/utf-8']}
}
在 MacOS 下進行動態鏈接 / 靜態鏈接
'link_settings':?{'libraries':?['-L<動態庫或靜態庫所在的文件夾>','-l<動態庫名稱>']
}
在 MacOS 下引入系統 Framework 依賴
'libraries':?['-framework?MediaPlayer','-framework?Cocoa',
]
在 MacOS 下設置 C++ 版本
"cflags_cc":?["-std=c++20"
]
在 MacOS Xcode 的 Release 構建下生成 .dSYM 調試文件
'xcode_settings':?{'DEBUG_INFORMATION_FORMAT':?'dwarf-with-dsym'
}
使 MacOS 下的 addon 能夠使用同目錄下的動態庫 / Framework
'link_settings':?{'libraries':?['-Wl,-rpath,@loader_path',##?此外,還可以設置到任何相對于?.node?文件的其他目錄下'-Wl,-rpath,@loader_path/../../darwin/arm64',]
},

但是這也要求 .dylib 文件支持該功能,可以通過 otool -D <你的動態鏈接庫位置>.dylib 的返回結果來檢查:

<你的動態鏈接庫>.dylib:
@rpath/<鏈接庫名稱>.dylib

如果文件名往前的開頭是 @rpath,則意味著支持該功能。如果不是,則可以使用 install_name_tool 來修改動態鏈接庫使其支持:

install_name_tool?-id?"@rpath/<鏈接庫名稱>.dylib"?<你的動態鏈接庫位置>.dylib
在 MacOS 下支持 Objective-C 和 C++ 混編
'xcode_settings':?{'OTHER_CFLAGS':?['-ObjC++']
}

開發&分發&使用

項目文件組織

通常來說,我們可以用下面的文件夾結構來扁平地組織我們的 addon 文件:

.
├──?node_modules???????????????????##?npm?依賴
├──?build??????????????????????????##?構建路徑
│???├──?Release???????????????????##?Release?產物路徑
│???????├──?myaddon.node??????????##?addon?產物
│???????├──?myaddon.node.dSYM?????##?addon?的符號文件
├──?binding.gyp????????????????????##?構建配置
├──?addon.cc???????????????????????##?Addon?的?C++?源碼
├──?index.js???????????????????????##?Addon?的?JavaScript?源碼
├──?index.d.ts?????????????????????##?Addon?的?TypeScript?類型(下方會介紹)
└──?package.json???????????????????##?Addon?的?package.json?文件

當然我們也可以把 JavaScript 源碼和 C++ 源碼分別放入不同的文件夾,只需要修改對應的構建配置和 package.json 即可。

編寫 index.js - 使用 bindings 包

一般來說我們會直接在 C++ 中實現大部分邏輯,JavaScript 文件只用來引入 .node 文件。由于 Node.js Addon 存在各種不同的方案、構建配置,因此 .node 文件產物的位置可能也會因此不同,所以我們需要借助一個第三方 npm 包來自動為我們尋找 .node 文件的位置:

https://github.com/TooTallNate/node-bindings

通過 bindings,我們的 index.js 僅需一行代碼就能自動獲取并導出 .node 模塊:

module.exports?=?require('bindings')('binding.node')

同時保證 package.json 的 main 配置為我們的 index.js:

{//?..."main":?"index.js"//?...
}

為 Addon 添加 TypeScript 類型

添加 TypeScript 類型,最簡單的方式只需要創建一個 index.d.ts 文件,并在其中聲明在 C++ 代碼中創建的函數們即可:

export?interface?FooOptions?{bar:?string
}export?function?foo(options:?FooOptions)

并在 package.json 添加一行參數用于指向類型文件:

{//?..."types":?"index.d.ts"//?...
}

大部分情況下,這個方法就可以給你的 Node.js Addon 聲明類型。

分發形式

安裝時編譯

一種方式是在使用者進行 npm install 時,使用用戶設備進行 Addon 的編譯。這時候我們可以使用 install 鉤子來實現,我們僅需在 package.json 文件中添加如下內容:

{//?..."scripts":?{//?..."install":?"prebuild-install?||?node-gyp?rebuild?--release"//?...}//?...
}

保險起見,確保 node-gyp 在你的 devDependencies 之中,這樣就能在用戶通過 npm 安裝你的 Addon 時,自動編譯當前系統架構所對應的產物。

預編譯

如果希望更近一步,節約用戶安裝 Addon 的時間,或者是為了讓用戶無需具備編譯環境即可安裝 Addon,可以使用預編譯方案。即在集成環境中提前編譯常見的操作系統、架構對應的 .node 文件,并隨著 npm 包進行分發,再通過 bindings 或者其他一些庫來自動匹配尋找系統所需要的對應 .node 文件。

由于預編譯方案涉及到更多的細節,本文不再做介紹,大家可以參考該項目:

https://github.com/mapbox/node-pre-gyp

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/168026.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/168026.shtml
英文地址,請注明出處:http://en.pswp.cn/news/168026.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

Spring - Mybatis-設計模式總結

Mybatis-設計模式總結 1、Builder模式 2、工廠模式 3、單例模式 4、代理模式 5、組合模式 6、模板方法模式 7、適配器模式 8、裝飾者模式 9、迭代器模式 雖然我們都知道有26個設計模式&#xff0c;但是大多停留在概念層面&#xff0c;真實開發中很少遇到&#xff0c;…

【數據結構】時間和空間復雜度

馬上就要進入到數據結構的學習了 &#xff0c;我們先來了解一下時間和空間復雜度&#xff0c;這也可以判斷我們的算法是否好壞&#xff1b; 如何衡量一個算法的好壞&#xff1f; 就是看它的算法效率 算法效率 算法效率分析分為兩種&#xff1a;第一種是時間效率&#xff0c;第…

C++ Qt QVariant類型使用介紹與代碼演示

作者:令狐掌門 技術交流QQ群:675120140 csdn博客:https://mingshiqiang.blog.csdn.net/ 文章目錄 一、QVariant基本用法二、自定義類型使用QVariant三、其它用法賦值修改和替換值使用`QVariant::setValue()`設置值復制構造函數和賦值操作比較使用`QVariant::swap()`交換值使…

CVE-2023-22515:Atlassian Confluence權限提升漏洞復現 [附POC]

文章目錄 Atlassian Confluence權限提升(CVE-2023-22515)漏洞復現 [附POC]0x01 前言0x02 漏洞描述0x03 影響版本0x04 漏洞環境0x05 漏洞復現1.訪問漏洞環境2.構造POC3.復現 0x06 修復建議 Atlassian Confluence權限提升(CVE-2023-22515)漏洞復現 [附POC] 0x01 前言 免責聲明&…

vue中下載文件后無法打開的坑

今天在項目開發的時候臨時要添加個導出功能我就寫了一份請求加導出得代碼&#xff0c; 代碼&#xff1a; //導出按鈕放開exportDutySummarizing (dataRangeInfo) {const params {departmentName: dataRangeInfo.name,departmentQode: dataRangeInfo.qode}//拼接所需得urlcons…

UserRole

Qt::UserRole 是 Qt::ItemDataRole 枚舉中的一個成員&#xff0c;用于表示自定義數據角色&#xff08;Data Role&#xff09;的起始值。 在 Qt 中&#xff0c;Qt::ItemDataRole 枚舉用于標識項&#xff08;Item&#xff09;中不同類型的數據。這些數據角色包括 Qt::DisplayRol…

目標檢測YOLO系列從入門到精通技術詳解100篇-【目標檢測】紅外熱成像

目錄 前言 知識儲備 紅外熱成像儀基礎知識 算法原理 紅外熱成像探測距離 紅外圖像增強

第一百七十八回 介紹一個三方包組件:SlideSwitch

文章目錄 1. 概念介紹2. 使用方法3. 代碼與效果3.1 示例代碼3.2 運行效果 4. 內容總結 我們在上一章回中介紹了"如何創建垂直方向的Switch"相關的內容&#xff0c;本章回中將 介紹SlideSwitch組件.閑話休提&#xff0c;讓我們一起Talk Flutter吧。 1. 概念介紹 我們…

多功能智能燈桿主要功能有哪些?

多功能智能燈桿這個詞相信大家都不陌生&#xff0c;最近幾年多功能智能燈桿行業發展迅速&#xff0c;迅速取代了傳統路燈&#xff0c;那么多功能智能燈桿相比傳統照明路燈好在哪里呢&#xff0c;為什么大家都選擇使用叁仟智慧多功能智能燈桿呢&#xff1f;所謂多功能智能燈桿著…

【libGDX】Mesh紋理貼圖

1 前言 紋理貼圖的本質是將圖片的紋理坐標與模型的頂點坐標建立一一映射關系。紋理坐標的 x、y 軸正方向分別朝右和朝下&#xff0c;如下。 2 紋理貼圖 本節將使用 Mesh、ShaderProgram、Shader 實現紋理貼圖&#xff0c;OpenGL ES 的實現見博客 → 紋理貼圖。 DesktopLauncher…

超級應用平臺(HAP)起航

各位明道云用戶和伙伴&#xff0c; 今天&#xff0c;我們正式發布明道云10.0版本。從這個版本開始&#xff0c;我們將產品名稱正式命名為超級應用平臺&#xff08;Hyper Application Platform, 簡稱HAP&#xff09;。我們用“超級”二字表達產品在綜合能力方面的突破&#xff…

清華系下一代 LCM

LCM LoRA模型是一種創新的深度學習模型&#xff0c;它通過特殊的技術手段&#xff0c;顯著提高了圖像生成的效率。這種模型特別適用于需要快速生成高質量圖像的場景&#xff0c;如藝術創作、實時圖像處理等。 GitHub - luosiallen/latent-consistency-model: Latent Consistenc…

視頻監控中的智能算法與計算機視覺技術

智能視頻監控是一種基于人工智能技術的監控系統&#xff0c;它能夠通過對圖像和視頻數據進行分析&#xff0c;自動識別目標物體、判斷其行為以及進行異常檢測等功能&#xff0c;從而實現對場景的智能化監管。以下是常見的一些用于智能視頻監控的算法&#xff1a; 1、人臉識別技…

RabbitMQ簡易安裝

一般來說安裝 RabbitMQ 之前要安裝 Erlang &#xff0c;可以去Erlang官網下載。接著去RabbitMQ官網下載安裝包&#xff0c;之后解壓縮即可。 Erlang官方下載地址&#xff1a;Downloads - Erlang/OTP RabbitMQ官方下載地址&#xff1a;Downloading and Installing RabbitMQ —…

org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder

密碼&#xff0c;加密&#xff0c;解密 spring-security-crypto-5.7.3.jar /** Copyright 2002-2011 the original author or authors.** Licensed under the Apache License, Version 2.0 (the "License");* you may not use this file except in compliance with t…

Kafka(一)

一&#xff1a;簡介 解決高吞吐量項目的需求 是一款為大數據而生的消息中間件&#xff0c;具有百億級tps的吞吐量&#xff0c;在數據采集、傳輸、存儲的過程中發揮著作用 二&#xff1a;為什么要使用消息隊列 一個普通訪問量的接口和一個大并發的接口&#xff0c;它們背后的…

C/C++---------------LeetCode第1512. 好數對的數目

好數對的數目 題目及要求暴力算法哈希算法在main內使用 題目及要求 給你一個整數數組 nums 。 如果一組數字 (i,j) 滿足 nums[i] nums[j] 且 i < j &#xff0c;就可以認為這是一組 好數對 。 返回好數對的數目。 示例 1&#xff1a; 輸入&#xff1a;nums [1,2,3,1,…

376.擺動序列

原題鏈接&#xff1a;376.擺動序列 全代碼&#xff1a; class Solution { public:int wiggleMaxLength(vector<int>& nums) {if (nums.size() < 1) return nums.size();int curDiff 0; // 當前一對差值int preDiff 0; // 前一對差值int result 1; // 記錄峰…

Android骨架圖

用法&#xff1a;在圖片上實現動畫效果 <FrameLayoutandroid:id"id/image_container"android:layout_width"match_parent"android:layout_height"wrap_content"><ImageViewandroid:id"id/ivBlank"android:layout_width"…

PostgreSQL Patroni 3.0 新功能規劃 2023年 紐約PG 大會 (音譯)

開頭還是介紹一下群&#xff0c;如果感興趣PolarDB ,MongoDB ,MySQL ,PostgreSQL ,Redis, Oceanbase, Sql Server等有問題&#xff0c;有需求都可以加群群內有各大數據庫行業大咖&#xff0c;CTO&#xff0c;可以解決你的問題。加群請聯系 liuaustin3 &#xff0c;&#xff08;…