為 Compose MultiPlatform 添加 C/C++ 支持(2):在 jvm 平臺使用 jni 實現桌面端與 C/C++ 互操作

前言

在上篇文章中我們已經介紹了實現 Compose MultiPlatform 對 C/C++ 互操作的基本思路。

并且先介紹了在 kotlin native 平臺使用 cinterop 實現與 C/C++ 的互操作。

今天這篇文章將補充在 jvm 平臺使用 jni。

在 Compose MultiPlatform 中,使用 jvm 平臺的是 Android 端和 Desktop 端,而安卓端可以直接使用安卓官方的 NDK 實現交叉編譯,但是 Desktop 不僅不支持交叉編譯,甚至連使用 Gradle 自動編譯都沒有。

所以本文重點主要在于實現 Desktop 的 jni 編譯以及調用編譯出來的二進制庫。

Android 使用 jni

在介紹 Desktop 使用 jni 之前,我們先回顧一下在 Android 中使用 jni,并復用 Android 端的 C++ 代碼給 Desktop 使用。

感謝谷歌的工作,在安卓中使用 jni 非常簡單,我們只需要在 Android Studio 隨便打開一個已有的項目,然后依次選擇菜單 File - New - New Module - Android Native Library,保持默認參數,點擊 Finish 即可完成創建安卓端的 jni 模塊。

這里我們以 jetBrains 的官方 Compose MultiPlatform 模板 項目作為示例:

1.jpg

創建完成后需要注意,Android studio 會自動修改項目 settings.gradle.kts 在其中添加一個插件 org.jetbrains.kotlin.android ,這會導致編譯錯誤 java.lang.IllegalArgumentException: Cannot provide multiple default versions for the same plugin.,所以需要我們刪掉新添加的這個插件:

2.jpg

然后在 shared 模塊中的 build.gradle.kts 文件的 Android 依賴部分引入 nativelib 模塊:

kotlin {// ……sourceSets {// ……val androidMain by getting {dependencies {// ……api(project(":nativelib"))}}// ……}
}

接著,需要注意 nativelib 模塊的兩個文件 native.cppNativeLib.kt

3.jpg

我們看一下 nativelib 模塊中的 nativelib.cpp 文件的默認內容:

#include <jni.h>
#include <string>extern "C" JNIEXPORT jstring JNICALL
Java_com_equationl_nativelib_NativeLib_stringFromJNI(JNIEnv* env,jobject /* this */) {std::string hello = "C++";return env->NewStringUTF(hello.c_str());
}

代碼很簡單,就是返回一個字符串 “Hello from C++”,我們改成返回 “C++”。

這里需要注意這個函數的名稱: Java_com_equationl_nativelib_NativeLib_stringFromJNI

開頭的 “Java” 是固定字符,后面的 “com_equationl_nativelib_NativeLib” 表示從 java 調用時的類的包名+類名,最后的 “stringFromJNI” 才是這個函數的名稱。

通過 jni 從 java(kt)中調用這個函數時必須確保其包名和類名與其一致才能成功調用。

然后查看 NativeLib.kt 文件:

class NativeLib {external fun stringFromJNI(): Stringcompanion object {init {System.loadLibrary("nativelib")}}
}

其中 external fun stringFromJNI(): String 表示需要調用的 c++ 函數名。

System.loadLibrary("nativelib") 表示加載 C++ 編譯生成的二進制庫,這里我們無需關心具體的編譯過程和編譯產物,只需要直接加載 nativelib 即可,剩下的工作 NDK 已經替我們完成了。

最后,我們來調用一下這個 C++ 函數。

不過在此之前先簡單介紹一下我們用作示例的這個 Compose MultiPlatform 的內容,它的 UI 就是一個按鈕,按鈕默認顯示 “Hello, World!”,當點擊按鈕后會通過一個 expect 函數獲取當前平臺的名稱然后顯示到按鈕上:

@OptIn(ExperimentalResourceApi::class)
@Composable
fun App() {MaterialTheme {var greetingText by remember { mutableStateOf("Hello, World!") }var showImage by remember { mutableStateOf(false) }Column(Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) {Button(onClick = {greetingText = "Hello, ${getPlatformName()}"showImage = !showImage}) {Text(greetingText)}AnimatedVisibility(showImage) {Image(painterResource("compose-multiplatform.xml"),contentDescription = "Compose Multiplatform icon")}}}
}expect fun getPlatformName(): String

所以接下來我們修改安卓平臺的 getPlatformName 函數的 actual 實現,由:

actual fun getPlatformName(): String = "Android"

修改為:

actual fun getPlatformName(): String = NativeLib().stringFromJNI()

這樣,它獲取的名稱就是來自 C++ 代碼的 “C++” 了。

運行代碼,可以看到完美符合預期:

4.gif

Desktop 使用 jni

上一節我們已經完成了在 Android 中使用 jni,本節我們將在 Desktop 中也實現使用 jni,并且復用上節中的 nativelib.cpp 文件。

因為直接使用 Gradle 編譯 C++ 代碼不是很方便,而且還不支持交叉編譯,所以這里我們首先手動編譯,驗證可行后再自己編寫 gradle 腳本實現自動編譯。

有關編寫 gradle 腳本的基礎知識可以閱讀我之前的文章 Compose Desktop 使用中的幾個問題(分平臺加載資源、編寫Gradle 任務下載平臺資源、桌面特有組件、鼠標&鍵盤事件) 了解。

首先,我們可以使用命令 g++ nativelib.cpp -o nativelib.bin -shared -fPIC -I C:\Users\equationl\.jdks\corretto-19.0.2\include -I C:\Users\equationl\.jdks\corretto-19.0.2\include\win32 編譯我們的 C++ 文件為當前平臺可用的二進制文件。

上述命令中 nativelib.cpp 即需要編譯的文件,nativelib.bin 為輸出的二進制文件,C:\Users\equationl\.jdks\corretto-19.0.2\ 為你電腦上安裝的任意的 jdk 目錄。

輸入 “ j d k P a t h / i n c l u d e " 和 " jdkPath/include" 和 " jdkPath/include""jdkPath/include/win32” 是因為這兩個目錄下有我們的 C++ 文件導入所需的頭文件,如 “jni.h” 。

切換到我們的 C++ 文件所在目錄后執行上述命令編譯:

5.jpg

此時我們可以看到在 “./nativelib/src/main/cpp” 目錄下已經生成了 nativelib.bin 文件。

注意:在 macOS 上系統自帶了 g++ 命令,但是一般來說 Windows 系統沒有自帶 g++ 命令,所以需要先自己安裝 g++

然后,我們在 sahred 模塊下的 desktopMain 包中新建一個文件 NativeLib.kt ,注意該文件的包名需要和 C++ 定義的一致:

6.jpg

然后編寫該文件內容為:

package com.equationl.nativelibclass NativeLib {external fun stringFromJNI(): Stringcompanion object {init {System.load("D:\\project\\ideaProject\\compose-multiplatform-c-test\\nativelib\\src\\main\\cpp\\nativelib.bin")}}
}

可以看到在 Desktop 中加載二進制庫和 Android 中略有不同,它使用的是 System.load() 而不是 System.loadLibrary() ,并且加載二進制文件時使用的是絕對路徑。

這是因為我們無法在 Desktop 中像 Android 一樣直接把二進制文件打包到指定的路徑下并且直接使用庫名通過 System.loadLibrary() 加載,所以只能使用絕對路徑加載外部二進制文件。

這里我們把加載的文件路徑寫為了先前生成的 nativelib.bin 的路徑。

接著,依舊是修改 dektop 的 getPlatformName 函數的實現為:

actual fun getPlatformName(): String = NativeLib().stringFromJNI()

然后運行 Desktop 程序:

7.gif

運行結果完美符合預期。

為 Desktop 實現自動編譯 C++

在上一節中我們已經實現了 Desktop 使用 jni 并驗證了可行性,但是目前還是手動編譯代碼,這顯然是不現實的,所以我們本節將講解如何自己編寫腳本實現自動編譯。

另外,上一節中我們說過, Dektop 加載二進制文件使用的是絕對路徑,所以我們需要將編譯生成的二進制文件放到指定位置并打包進 Desktop 程序安裝包中,Desktop 在安裝時會自動將這個文件解壓到指定路徑,關于這個的基礎知識還是可以看我的文章 Compose Desktop 使用中的幾個問題(分平臺加載資源、編寫Gradle 任務下載平臺資源、桌面特有組件、鼠標&鍵盤事件) 了解。

首先,需要指定一下資源文件目錄,在 desktopApp 模塊的 buiuld.gradle.kts 文件中添加以下內容:

compose.desktop {application {// ……nativeDistributions {// ……appResourcesRootDir.set(project.layout.projectDirectory.dir("resources"))}}
}

指定資源目錄為 resources

然后依舊是在這個文件中,添加一個函數 runCommand,用于執行 shell 命令:

fun runCommand(command: String, timeout: Long = 120): Pair<Boolean, String> {val process = ProcessBuilder().command(command.split(" ")).directory(rootProject.projectDir).redirectOutput(ProcessBuilder.Redirect.INHERIT).redirectError(ProcessBuilder.Redirect.INHERIT).start()process.waitFor(timeout, TimeUnit.SECONDS)val result = process.inputStream.bufferedReader().readText()val error = process.errorStream.bufferedReader().readText()return if (error.isBlank()) {Pair(true, result)}else {Pair(false, error)}
}

代碼很簡單,接收一個字符串表示的 shell 命令,返回一個 Pair ,第一個 booean 數據表示是否執行成功;第二個 String 是輸出內容。

接著注冊一個 task:

tasks.register("compileJni") { }

修改原有的 prepareAppResources task,添加上我們剛注冊的 compileJni 為它的依賴:

gradle.projectsEvaluated {tasks.named("prepareAppResources") {dependsOn("compileJni")}
}

這里的修改依賴需要加在 gradle.projectsEvaluated 語句中,因為 prepareAppResources 這個 task 推遲了注冊,如果不在項目配置完成后再修改依賴的話會報 prepareAppResources 不存在。

注:這里的 prepareAppResources 是 task 模塊中用于執行復制和打包資源文件的 task,所以我們把自定義的 compileJni 添加成它的依賴,以保證在它之前執行。

另外,這里必須明確保證 compileJniprepareAppResources 之前執行,否則由于我們的 compileJni 任務的輸出路徑和 prepareAppResources 任務的輸出路徑沖突,會導致編譯失敗,具體后面詳細解釋。

接著,在 compileJni task 中編寫我們的編譯邏輯,我們先看一下完整的代碼,然后再逐一解釋:

tasks.register("compileJni") {description = "compile jni binary file for desktop"val resourcePath = File(rootProject.projectDir, "desktopApp/resources/common/lib/")val binFilePath = File(resourcePath, "nativelib.bin")val cppFileDirectory = File(rootProject.projectDir, "nativelib/src/main/cpp")val cppFilePath = File(cppFileDirectory, "nativelib.cpp")// 指定輸入、輸出文件,用于增量編譯inputs.dir(cppFileDirectory)outputs.file(binFilePath)doLast {project.logger.info("compile jni for desktop running……")val jdkFile = org.gradle.internal.jvm.Jvm.current().javaHomeval systemPrefix: Stringval os: OperatingSystem = DefaultNativePlatform.getCurrentOperatingSystem()if (os.isWindows) {systemPrefix = "win32"}else if (os.isMacOsX) {systemPrefix = "darwin"}else if (os.isLinux) {systemPrefix = "linux"}else {project.logger.error("UnSupport System for compiler cpp, please compiler manual")return@doLast}val includePath1 = jdkFile.resolve("include")val includePath2 = includePath1.resolve(systemPrefix)if (!includePath1.exists() || !includePath2.exists()) {val msg = "ERROR: $includePath2 not found!\nMaybe it's because you are using JetBrain Runtime (Jbr)\nTry change Gradle JDK to another jdk which provide jni support"throw GradleException(msg)}project.logger.info("Check Desktop Resources Path……")if (!resourcePath.exists()) {project.logger.info("${resourcePath.absolutePath} not exists, create……")mkdir(resourcePath)}val runTestResult = runCommand("g++ --version")if (!runTestResult.first) {throw GradleException("Error: Not find command g++, Please install it and add to your system environment path\n${runTestResult.second}")}val command = "g++ ${cppFilePath.absolutePath} -o ${binFilePath.absolutePath} -shared -fPIC -I ${includePath1.absolutePath} -I ${includePath2.absolutePath}"project.logger.info("running command $command……")val compilerResult = runCommand(command)if (!compilerResult.first) {throw GradleException("Command run fail: ${compilerResult.second}")}project.logger.info(compilerResult.second)project.logger.lifecycle("compile jni for desktop all done")}
}

首先,在 task 頂級定義了四個路徑: resourcePathbinFilePathcppFileDirectorycppFilePath,分別表示需要存放二進制文件的資源目錄、二進制文件輸出路徑、C++文件存放目錄和需要編譯的具體 C++ 文件路徑。

rootProject.projectDir 返回的是當前項目的根目錄。

接著,我們通過 inputs.dir() 方法添加了該 task 的輸入路徑。

outputs.file 方法添加了該 task 的輸出文件。

定義輸入路徑和輸出文件與我們這里需要執行的編譯沒有直接關聯,這里定義這個兩個路徑是為了讓 Gradle 實現增量編譯,即只有在上次編譯完成后輸入路徑的中的文件內容發生了變化或輸出文件發生了變化才會繼續執行這個 task,否則會認為這個 task 沒有變化,不會執行,表現在編譯輸出日志則為:

> Task :desktopApp:compileJni UP-TO-DATE

接下來,我們的代碼寫在了 doLast { } 語句中,則表示里面的代碼只有在編譯階段才會執行,在配置階段不會執行。

在其中的 org.gradle.internal.jvm.Jvm.current().javaHome 返回的是當前項目 Gradle 使用的 jdk 根目錄。

然后,我們需要拼接出編譯時需要導入的兩個 jdk 路徑 includePath1includePath2 ,其中的 includePath2 不同的系統名稱不一樣,所以需要判斷一下當前編譯使用的系統并更改該值。 可以通過 DefaultNativePlatform.getCurrentOperatingSystem().isXXX 判斷當前是否是某個系統。

接著,檢查存放二進制文件的目錄是否存在,不存在則創建。

下一步是使用 g++ --version 測試是否安裝了 g++ 。

最后,拼接出編譯命令后執行編譯:

g++ ${cppFilePath.absolutePath} -o ${binFilePath.absolutePath} -shared -fPIC -I ${includePath1.absolutePath} -I ${includePath2.absolutePath}

此時如果編譯成功,那么二進制文件會輸出到我們指定的 dektop 資源目錄下。

我們現在只需要修改 dektop 加載二進制文件的代碼為:

val libFile = File(System.getProperty("compose.application.resources.dir")).resolve("lib").resolve("nativelib.bin")
System.load(libFile.absolutePath)

上述代碼中 System.getProperty("compose.application.resources.dir") 返回的是我們最開始在 Gradle 中定義的資源打包安裝解壓后在系統上的絕對路徑。

至此,我們的自動編譯已經完成!

最后來說一下我們前面提到的為什么我們的 compileJni task 必須在 prepareAppResources 之前執行,我們現在直接把原本的修改 prepareAppResources 依賴于 compileJni 改成 Desktop 模塊執行的第一個 task compileKotlinJvm 依賴 compileJni

tasks.named("compileKotlinJvm") {dependsOn("compileJni")
}

運行后會看到報錯:

A problem was found with the configuration of task ':desktopApp:prepareAppResources' (type 'Sync').- Gradle detected a problem with the following location: '/Users/equationl/AndroidStudioProjects/life-game-compose/desktopApp/resources/common'.Reason: Task ':desktopApp:prepareAppResources' uses this output of task ':desktopApp:compileJni' without declaring an explicit or implicit dependency. This can lead to incorrect results being produced, depending on what order the tasks are executed.Possible solutions:1. Declare task ':desktopApp:compileJni' as an input of ':desktopApp:prepareAppResources'.2. Declare an explicit dependency on ':desktopApp:compileJni' from ':desktopApp:prepareAppResources' using Task#dependsOn.3. Declare an explicit dependency on ':desktopApp:compileJni' from ':desktopApp:prepareAppResources' using Task#mustRunAfter.

簡單說就是 prepareAppResourcescompileJni 都聲明了同一個輸出路徑,除非明確指定它們兩個之間的依賴關系,否則編譯會出現問題。

其實也很好理解,他們的輸出路徑都是一個,如果不明確依賴關系的話增量編譯就永遠不會觸發了,永遠都將是全量編譯。

而在這里我們的需求是首先使用 compileJni 生成二進制文件后,由 prepareAppResources 將其打包,所以自然應該是寫成 prepareAppResources 依賴于 compileJni

最后,還是需要強調一點,Desktop 編譯 C++ 是不支持交叉編譯的,也就是說在 Windows 只能編譯 Windows 的程序,在 macOS 只能 編譯 macOS 的程序。

其實即使 C++ 可以交叉編譯也沒用,因為 Compose Desktop 并不支持交叉編譯,哈哈哈。

參考資料

  1. Native dependency in Kotlin/Multiplatform — part 2: JNI for JVM & Android
  2. Kotlin JNI for Native Code

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

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

相關文章

Kubernetes實戰(十)-升級k8s集群

1 Kubernetes(k8s) 集群升級過程 Kubernetes 使用 kubeadm 工具來管理集群組件的升級。在集群節點層面&#xff0c;升級 Kubernetes(k8s)集群的過程可以分為以下幾個步驟&#xff1a; 1&#xff09;檢查當前環境和配置是否滿足升級要求。 2&#xff09;升級master主節點&…

如何一個例子玩明白GIT

一個例子玩明白GIT GIT的介紹和教程五花八門&#xff0c;但實際需要用的就是建倉、推送、拉取等操作&#xff0c;這兒咱可以通過一個例子熟悉這些操作&#xff0c;一次性搞定GIT的使用方法學習。下面這個例子的內容是內容是建立初始版本庫&#xff0c;然后將數據復制到 "遠…

輕量封裝WebGPU渲染系統示例<45>- 材質組裝流水線(MaterialPipeline)燈光、陰影、霧(源碼)

當前示例源碼github地址: https://github.com/vilyLei/voxwebgpu/blob/feature/material/src/voxgpu/sample/MaterialPipelineFog.ts 當前示例運行效果: 此示例基于此渲染系統實現&#xff0c;當前示例TypeScript源碼如下&#xff1a; export class MaterialPipelineFog {pr…

數組創建方法

數組的創建 1.let a[] 2.let anew Array(5) 3.let anew Array(1,2,3) 4.let a[1,2,3] 創建數組是空還是有值是以上四種寫法。但是如果沒給值的變量是undefined&#xff0c;再a[0]找不到這種變量的。所以當找某一個數需要已經是數組內存。不想給值可以給空數組。只要是數組…

MEMS制造的基本工藝介紹——晶圓鍵合

晶圓鍵合是一種晶圓級封裝技術&#xff0c;用于制造微機電系統 (MEMS)、納米機電系統 (NEMS)、微電子學和光電子學&#xff0c;確保機械穩定和氣密密封。用于 MEMS/NEMS 的晶圓直徑范圍為 100 毫米至 200 毫米&#xff08;4 英寸至 8 英寸&#xff09;&#xff0c;用于生產微電…

【重點】【環鏈表入口】142. 環形鏈表 II

題目 public class Solution {public ListNode detectCycle(ListNode head) {if (head null || head.next null) {return null;}ListNode slow head, fast head;while (fast ! null && fast.next ! null) {slow slow.next;fast fast.next.next;if (slow fast) …

SQL語句---更新數據

介紹 使用sql語句更新數據。 命令 update 表名 set 字段1值1[,字段2值2] [where 條件表達式];[]&#xff08;方括號&#xff09;內的表是表示可選。 例子 將a表id值等于1的數據的名稱改為666 update a set name666 where id1;

2023-12-05 Qt學習總結7

點擊 <C 語言編程核心突破> 快速C語言入門 Qt學習總結 前言二十 QTcpSocket QTcpServer網絡庫服務端代碼:客戶端代碼 二十一 QProcess進程類二十二 QThread線程總結 前言 要解決問題: 學習qt最核心知識, 多一個都不學. 二十 QTcpSocket QTcpServer網絡庫 QTcpSocket和…

持續集成交付CICD:Jenkins流水線實現Nexus制品晉級策略

目錄 一、理論 1.開發測試運維環境 二、實驗 1.Nexus制品晉級策略 一、理論 1.開發測試運維環境 &#xff08;1&#xff09;環境 1&#xff09;持續集成開發環境&#xff08;DEV: Development Environment&#xff09; 直接通過源代碼編譯打包&#xff0c;其會跑單元測試…

python 筆記 :trajectory_distance包(如何可以正確使用)【debug篇】

包的地址&#xff1a;maikol-solis/trajectory_distance (github.com) 1 模塊介紹 用Cython實現的Python模塊&#xff0c;用于計算二維軌跡之間的距離 trajectory_distance包提供了9種軌跡間的距離計算方法&#xff1a; SSPD&#xff08;對稱線段路徑距離&#xff09;OWD&a…

機器學習算法(9)——集成技術(Bagging——隨機森林分類器和回歸)

一、說明 在這篇文章&#xff0c;我將向您解釋集成技術和著名的集成技術之一&#xff0c;它屬于裝袋技術&#xff0c;稱為隨機森林分類器和回歸。 集成技術是機器學習技術&#xff0c;它結合多個基本模塊和模型來創建最佳預測模型。為了更好地理解這個定義&#xff0c;我們需要…

WLAN配置實驗

本文記錄了WLAN配置實踐的過程&#xff0c;該操作在華為HCIA中屬于相對較復雜的實驗&#xff0c;記錄過程備忘。這里不就WLAN原理解釋&#xff0c;僅進行配置實踐&#xff0c;可以作為學習原理時候的參考。本文使用華為ENSP進行仿真。實驗拓撲圖如下&#xff1a; 1.WLAN工作流程…

【electron】外語函數接口 FFI

? 目錄 ? &#x1f6eb; 導讀需求開發環境 1?? FFI概念優點注意事項 2?? 【廢棄】node-ffi3?? node-ffi-napi安裝&#xff08;windows系統下&#xff09;示例&#xff1a;MessageBoxA、NtSuspendProcess 4?? node-win32-api安裝示例&#xff1a;查找窗口并設置窗口標…

UE5數據傳遞-紋理貼圖

期待結果&#xff1a; 流程 1. 通過C寫入數據到紋理貼圖 2. 在材質中通過采樣能正確讀取寫入的數值 踩坑&#xff1a; 1. UE5之后&#xff0c;需要設置采樣類型&#xff0c;才能達到上圖效果&#xff0c;默認采樣類型做了插值計算 FColor中寫入 PF_B8G8R8A8 UTexture2D* Conve…

第四題:憧憬(JavaPythonC++實現)【第六屆傳智杯-新增場次-程序設計挑戰賽解題分析詳解復盤】

本文僅為【2023傳智杯-第二場】第六屆傳智杯程序設計挑戰賽-題目解題分析詳解的解題個人筆記,個人解題分析記錄。 本文包含:第六屆傳智杯程序設計挑戰賽題目、解題思路分析、解題代碼、解題代碼詳解(Java&Python&C++實現) 文章目錄 更新進度記錄第四題:憧憬(Java…

AI 繪畫 | Stable Diffusion 藝術二維碼制作

前言 這篇文章教會你如果用Stable Diffusion WEB UI制作藝術二維碼,什么是藝術二維碼呢?就是普通二維碼和藝術圖片融合后的二維碼圖片,如下圖所示。主要原理還是使用controlNet的control_v1p_sd15_qrcode_monster模型和光影模型control_v1p_sd15_brightness。 教程 準備…

【論文閱讀筆記】NeRF+Mip-NeRF+Instant-NGP

目錄 前言NeRF神經輻射場體渲染連續體渲染體渲染離散化 方法位置編碼分層采樣體渲染推導公式&#xff08;1&#xff09;到公式&#xff08;2&#xff09;部分代碼解讀相機變換&#xff08;重要&#xff01;&#xff09; Mip-NerfTo do Instant-NGPTo do 前言 NeRF是NeRF系列的…

DIP——邊緣提取與分割

1.使用canny算法進行邊緣提取 本實驗比較簡單&#xff0c;基本思路是對原圖像進行一個高斯模糊處理&#xff0c;用于去噪&#xff0c;之后轉換為灰度圖&#xff0c;直接調用cv庫中的canny記性邊緣提取。若想直接得到彩色邊緣&#xff0c;則通過按位與操作&#xff0c;將原始彩色…

SQLMap進階使用

預計更新SQL注入概述 1.1 SQL注入攻擊概述 1.2 SQL注入漏洞分類 1.3 SQL注入攻擊的危害 SQLMap介紹 2.1 SQLMap簡介 2.2 SQLMap安裝與配置 2.3 SQLMap基本用法 SQLMap進階使用 3.1 SQLMap高級用法 3.2 SQLMap配置文件詳解 3.3 SQLMap插件的使用 SQL注入漏洞檢測 4.1 SQL注入…

ingress介紹和ingress通過LoadBalancer暴露服務配置

目錄 一.ingress基本原理介紹 1.將原有用于暴露服務和負載均衡的服務的三四層負載均衡變為一個七層負載均衡 2.controller和ingress 3.通過下面這個圖可能會有更直觀的理解 二.為什么會出現ingress 1.NodePort存在缺點 2.LoadBalancer存在缺點 三.ingress三種暴露服務的…