开发一个支持跨平台的 Kotlin 编译器插件

2023-05-16

前言

前面简单介绍了一下Kotlin编译器插件是什么,以及如何一步一步开发一个Kotlin编译器插件,但是之前开发的编译器插件是通过修改字节码的方式来修改产物的,只支持JVM平台。今天主要在此基础上,介绍一下如何通过修改IR的方式来修改Kotlin编译器的产物,如何开发一个支持跨平台的Kotlin编译器插件

本文主要包括以下内容:

  1. Kotlin IR是什么?
  2. 如何遍历Kotlin IR
  3. 如何创建Kotlin IR元素?
  4. 如何修改Kotlin IR?
  5. 修改Kotlin IR实战

Kotlin IR是什么?

Kotlin IRKotlin编译器中间表示,它从数据结构上来说也是一个抽象语法树。

因为Kotlin是支持跨平台的,因此有着JVMNativeJS三个不同的编译器后端,为了在不同的后端之间共享逻辑,以及简化支持新的语言特性所需的工作,Kotiln编译器引入IR的概念,如下图所示:

Kotlin 编译器插件中主要使用了ClassBuilderInterceptorExtension在生成字节码的时机来修改产物

但是通过这种开发的插件是不支持Kotlin跨平台的,很显然,NativeJS平台并不会生成字节码

这就是修改IR的意义,让我们的编译器插件支持跨平台

正是为了支持跨平台,官方开发的很多插件,比如Compose编译器插件,都是基于IrGenerationExtension

IrElement.dump()使用

在从概念上理解了Kotlin IR是什么样之后,我们再来看下Kotlin IR在代码中到底长什么样?

Kotlin IR 语法树中的每个节点都实现了 IrElement。语法树的元素包括模块、包、文件、类、属性、函数、参数、if语句、函数调用等等,我们可以通过IrElement.dump方法来看下它们在代码中的样子

//  1\. 注册IrGenerationExtension
IrGenerationExtension.registerExtension(project, TemplateIrGenerationExtension(messageCollector))

// 2\. IrGenerationExtension具体实现
class TemplateIrGenerationExtension() : IrGenerationExtension {
    override fun generate(moduleFragment: IrModuleFragment, pluginContext: IrPluginContext) {
        println(moduleFragment.dump())
    }
}

通过以上方式就可以实现IrGenerationExtension的注册与具体实现,如果我们的源代码如下所示:

fun main() {
  println(debug())
}

fun debug(name: String = "World") = "Hello, $name!"

编译器插件将输出以下内容:

MODULE_FRAGMENT name:<main>
  FILE fqName:<root> fileName:/var/folders/dk/9hdq9xms3tv916dk90l98c01p961px/T/Kotlin-Compilation3223338783072974845/sources/main.kt
    // 1
    FUN name:main visibility:public modality:FINAL <> () returnType:kotlin.Unit
      BLOCK_BODY
        CALL 'public final fun println (message: kotlin.Any?): kotlin.Unit [inline] declared in kotlin.io.ConsoleKt' type=kotlin.Unit origin=null
          message: CALL 'public final fun debug (name: kotlin.String): kotlin.String declared in <root>' type=kotlin.String origin=null
    // 2      
    FUN name:debug visibility:public modality:FINAL <> (name:kotlin.String) returnType:kotlin.String
      VALUE_PARAMETER name:name index:0 type:kotlin.String
        EXPRESSION_BODY
          CONST String type=kotlin.String value="World"
      // 3    
      BLOCK_BODY
        RETURN type=kotlin.Nothing from='public final fun debug (name: kotlin.String): kotlin.String declared in <root>'
          STRING_CONCATENATION type=kotlin.String
            CONST String type=kotlin.String value="Hello, "
            GET_VAR 'name: kotlin.String declared in <root>.debug' type=kotlin.String origin=null
            CONST String type=kotlin.String value="!"

这就是Kotlin IR在代码中的样子,可以看出它包括以下内容:

  1. main函数的声明,可见性,可变性,参数与返回值,可以看出这是一个名为publicfinal函数main,它不接受任何参数,并返回Unit
  2. debug函数则有一个参数name,该参数具有类型String并且还返回一个String,参数的默认值通过VALUE_PARAMETER表示
  3. debug函数的函数体通过BLOCK_BODY表示,返回的内容是一个String

IrElement.dump()Kotlin编译器插件的开发过程中是很实用的,通过它我们可以Dump任意代码并查看其结构

如何遍历Kotlin IR

如前文所说,Kotlin IR是一个抽象语法树,这意味着我们可以利用处理树结构的方式来处理Kotlin IR

我们可以利用访问者模式来遍历Kotlin IR,我们知道,Kotlin IR中的每个节点都实现了IrElement接口,而IrElement接口中正好有 2 个访问者模式相关的函数。

fun <R, D> accept(visitor: IrElementVisitor<R, D>, data: D): R
fun <D> acceptChildren(visitor: IrElementVisitor<Unit, D>, data: D): Unit

接下来我们看一下在IrClass中两个方法的实现:

override fun <R, D> accept(visitor: IrElementVisitor<R, D>, data: D): R =
    visitor.visitClass(this, data)

override fun <D> acceptChildren(visitor: IrElementVisitor<Unit, D>, data: D) {
    thisReceiver?.accept(visitor, data)
    typeParameters.forEach { it.accept(visitor, data) }
    declarations.forEach { it.accept(visitor, data) }
}

可以看出:acceptChildren方法会调用所有childrenaccept方法,而accept方法则是调用对应elementvisit方法,最后都会调用到visitElement方法

因此我们可以通过以下方式遍历IR

// 1\. 注册 visitor
moduleFragment.accept(RecursiveVisitor(), null)

// 2\. visitor 实现
class RecursiveVisitor : IrElementVisitor<Unit, Nothing?> {
  override fun visitElement(element: IrElement, data: Nothing?) {
    element.acceptChildren(this, data)
  }
}

数据输入与输出

上文当我们使用IrElementVisitor时,忽略了它的两个泛型参数,一个用于定义data(每个visit函数接受的参数类型,另一个用于定义每个visitor函数的返回类型。

输入值data可用于在整个 IR 树中传递上下文信息。例如,可以用于打印出元素细节时使用的当前缩进间距。

class StringIndentVisitor : IrElementVisitor<Unit, String> {
  override fun visitElement(element: IrElement, data: String) {
    println("$data${render(element)} {")
    element.acceptChildren(this, "  $data")
    println("$data}")
  }
}

而输出类型可用于返回调用访问者的结果,在后面的IR转换中非常实用,这点我们后面再介绍。

如何创建Kotlin IR元素?

上文我们已经介绍了如何遍历Kotlin IR,接下来我们来看下如何创建Kotlin IR元素

class TemplateIrGenerationExtension() : IrGenerationExtension {
    override fun generate(moduleFragment: IrModuleFragment, pluginContext: IrPluginContext) {
        println(moduleFragment.dump())
    }
}

当定义IrGenerationExtension时,我们之前使用了moduleFragment参数,接下来我们来看下另一个参数:IrPluginContext,它可以为插件提供有关正在编译的当前模块之外的内容的上下文信息

IrPluginContext实例中,我们可以获得IrFactory的实例。这个工厂类是 Kotlin 编译器插件创建自己的 IR 元素的方式。它包含许多用于构建IrClassIrSimpleFunctionIrProperty等实例的函数。

IrFactory在构建声明时很有用:比如类、函数、属性等,但是在构建语句和表达式时,您将需要IrBuilder的实例。更重要的是,您将需要一个IrBuilderWithScope实例。有了这个构建器实例,IR 表达式可以使用更多的扩展函数。

在了解了这些基础内容后,我们来看一个实例

fun main() {
  println("Hello, World!")
}

这段代码很简单,我们来看下如何实用IR构建如上内容:

override fun generate(moduleFragment: IrModuleFragment, pluginContext: IrPluginContext) {
  // 1\. 通过pluginContext.irBuiltIns获取Kotlin语言中的内置内容,在这里我们获取了any与unit类型
  val typeNullableAny = pluginContext.irBuiltIns.anyNType
  val typeUnit = pluginContext.irBuiltIns.unitType
  // 2\. 如果您需要的不是语言本身内置的,而是来自依赖项(如标准库),您可以使用这些IrPluginContext.reference*()函数来查找所需的IrSymbol
  val funPrintln = pluginContext.referenceFunctions(FqName("kotlin.io.println"))
    .single {
      val parameters = it.owner.valueParameters
      parameters.size == 1 && parameters[0].type == typeNullableAny
    }

  // 3\. 使用irFactory构建一个函数
  val funMain = pluginContext.irFactory.buildFun {
    name = Name.identifier("main")
    visibility = DescriptorVisibilities.PUBLIC // default
    modality = Modality.FINAL // default
    returnType = typeUnit
  }.also { function ->
    // 4\. 设置function.body,构建函数体
    function.body = DeclarationIrBuilder(pluginContext, function.symbol).irBlockBody {
      通过+号将此表达式添加到块中
      +irCall(funPrintln).also { call ->
        call.putValueArgument(0, irString("Hello, World!"))
      }
    }
  }

  println(funMain.dump())
}

以上代码主要做了这么几件事:

  1. 通过pluginContext.irBuiltIns获取Kotlin语言中的内置内容,在这里我们获取了anyunit类型
  2. 如果您需要的不是语言本身内置的,而是来自依赖项(如标准库),您可以使用这些IrPluginContext.reference*()函数来查找所需的IrSymbol,同时由于函数支持重载,我们这里需要通过single方法过滤出具有所需签名的单个函数
  3. 使用irFactory构建一个函数,可以设置各种属性,如名称、可见性、可变性和返回类型等
  4. 通过设置function.body构建函数体,irBlockBody会创建出一个IrBuilderWithScope,在其中可以可以调用各种扩展方法创建IR,比如调用irCall。同时需要通过IrCall上的+运算符将此函数调用添加到块中

如上所示,通过这段代码就可以构建出我们想要的代码了

如何修改Kotlin IR?

在了解了如何遍历与创建IR之后,接下来就是修改了。

与之前遍历IR树类似,IrElement接口也包含两个与变换相关的接口

fun <D> transform(transformer: IrElementTransformer<D>, data: D): IrElement = accept(transformer, data)
fun <D> transformChildren(transformer: IrElementTransformer<D>, data: D): Unit

transform函数默认委托访问者函数accept,子类中的函数覆盖通常只需要将函数的返回类型覆盖为更具体的类型。例如,IrFile中的transform函数如下所示。

override fun <D> transform(transformer: IrElementTransformer<D>, data: D): IrFile =
  accept(transformer, data) as IrFile

transformChildren方法与遍历访问每个元素的所有子元素一样,transformChildren 函数允许对每个子元素进行变换。例如,让我们看看IrClass的实现。

override fun <D> transformChildren(transformer: IrElementTransformer<D>, data: D) {
  thisReceiver = thisReceiver?.transform(transformer, data)
  typeParameters = typeParameters.transformIfNeeded(transformer, data)
  declarations.transformInPlace(transformer, data)
}

总得来说,transformtransformChildren方法最后也会调用到各种visit方法,我们可以在其中修改IR的内容

在了解了这些基础之后,我们可以开始修改Kotlin IR的实战了

修改Kotlin IR实战

目标代码

接下来我们来看一个修改Kotlin IR的实例,比如以下代码

@DebugLog
fun greet(greeting: String = "Hello", name: String = "World"): String {
  return "${'$'}greeting, ${'$'}name!"
}

我们希望添加了@DebugLog注解的方法,在函数的入口与出口都通过println打印信息,在变换后代码如下所示:

@DebugLog
fun greet(greeting: String = "Hello", name: String = "World"): String {
  println("⇢ greet(greeting=$greeting, name=$name)")
  val startTime = TimeSource.Monotonic.markNow()
  try {
    val result = "${'$'}greeting, ${'$'}name!"
    println("⇠ greet [${startTime.elapsedNow()}] = $result")
    return result
  } catch (t: Throwable) {
    println("⇠ greet [${startTime.elapsedNow()}] = $t")
    throw t
  }
}

接下来我们就一步一步来实现这个目标

注册与定义Transformer

// 1\. 注册Transformer
moduleFragment.transform(DebugLogTransformer(pluginContext, debugLogAnnotation, funPrintln), null)

// 2\. 定义Transformer
class DebugLogTransformer(
  private val pluginContext: IrPluginContext,
  private val debugLogAnnotation: IrClassSymbol,
  private val logFunction: IrSimpleFunctionSymbol,
) : IrElementTransformerVoidWithContext() {
  private val typeUnit = pluginContext.irBuiltIns.unitType

  private val classMonotonic =
    pluginContext.referenceClass(FqName("kotlin.time.TimeSource.Monotonic"))!!

  override fun visitFunctionNew(declaration: IrFunction): IrStatement {
    val body = declaration.body
    if (body != null && declaration.hasAnnotation(debugLogAnnotation)) {
      declaration.body = irDebug(declaration, body)
    }
    return super.visitFunctionNew(declaration)
  }

这一步主要做了这么几件事:

  1. 注册Transfomer,传入用于构建 IR 元素的IrPluginContext,需要处理的注解符号IrClassSymbol,用于记录调试消息的函数的IrSimpleFunctionSymbol
  2. 基于IrElementTransformerVoidWithContext类扩展,自定义Transformer,这个Transformer不接受输入数据,并维护一个它访问过的各种 IR 元素的内部堆栈
  3. 定义一些本地属性来引用已知类型、类和函数,比如typeUnitclassMonotonic
  4. 重写visitFunctionNew函数来拦截函数语句的转换,我们需要检查它是否有body并且拥有目标注解@DebugLog

方法进入打点

进入函数时,我们需要调用println显示函数名称和函数入参。

private fun IrBuilderWithScope.irDebugEnter(
  function: IrFunction
): IrCall {
  val concat = irConcat()
  concat.addArgument(irString("⇢ ${function.name}("))
  for ((index, valueParameter) in function.valueParameters.withIndex()) {
    if (index > 0) concat.addArgument(irString(", "))
    concat.addArgument(irString("${valueParameter.name}="))
    concat.addArgument(irGet(valueParameter))
  }
  concat.addArgument(irString(")"))

  return irCall(logFunction).also { call ->
    call.putValueArgument(0, concat)
  }
}

在这里我们主要用到了irConcat来拼接字符串,以及irGet来获取参数值,这些参数经过concat拼接后通过println一起输出

方法结束时打点

方法退出时,我们要记录结果或抛出的异常。如果函数返回 Unit 我们可以跳过显示结果,因为已知它什么都没有。

private fun IrBuilderWithScope.irDebugExit(
  function: IrFunction,
  startTime: IrValueDeclaration,
  result: IrExpression? = null
): IrCall {
  val concat = irConcat()
  concat.addArgument(irString("⇠ ${function.name} ["))
  concat.addArgument(irCall(funElapsedNow).also { call ->
    call.dispatchReceiver = irGet(startTime)
  })
  if (result != null) {
    concat.addArgument(irString("] = "))
    concat.addArgument(result)
  } else {
    concat.addArgument(irString("]"))
  }

  return irCall(logFunction).also { call ->
    call.putValueArgument(0, concat)
  }
}

这里我们需要记录方法执行时间,startTime通过IrValueDeclaration传入,这是一个局部变量,可以通过irGet读取

为了调用kotlin.time.TimeMark.elapsedNow方法,我们可以调用funElapsedNow符号,并将startTime作为dispatcherReceiver,这样就能计算出方法耗时

result参数是可选的表达式,它可以是函数的返回值,或者是抛出的异常,这些参数经过concat拼接后通过println一起输出

组装函数体

private fun irDebug(
  function: IrFunction,
  body: IrBody
): IrBlockBody {
  return DeclarationIrBuilder(pluginContext, function.symbol).irBlockBody {
    +irDebugEnter(function)
    // ...
    val tryBlock = irBlock(resultType = function.returnType) {
      if (function.returnType == typeUnit) +irDebugExit(function, startTime)
    }.transform(DebugLogReturnTransformer(function, startTime), null)

    +IrTryImpl(startOffset, endOffset, tryBlock.type).also { irTry ->
      irTry.tryResult = tryBlock
      irTry.catches += irCatch(throwable, irBlock {
        +irDebugExit(function, startTime, irGet(throwable))
        +irThrow(irGet(throwable))
      })
    }
  }
}

这一部分主要是构建出try,catch块,并在相关部分调用irDebugEnterirDebugExit方法,完整代码就不在这里缀述了

经过这一步,支持跨平台的Kotlin编译器插件就开发完成了,添加了@DebugLog注解的方法在进入与退出时,都会打印相关的日志

总结

本文主要介绍了Kotlin IR是什么,如何对Kotlin IR进行增删改查,如何一步步开发一个支持跨平台的Kotlin编译器插件,希望对你有所帮助~

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

开发一个支持跨平台的 Kotlin 编译器插件 的相关文章

  • 认识MyBatis与Mybatis-plus及两者的区别

    一 认识Mybatis MyBatis 是持久层框架 xff0c 它支持定制化 SQL 存储过程以及高级映射 MyBatis 避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集 MyBatis 可以使用简单的 XML 或注解来配置
  • 线程

    创建线程 xff1a 方法一 xff09 继承Thread类 实现步骤 xff1a 继承Thread类 覆盖run 方法 提供并发运程的过程 创建这个类的实例 使用start 方法启动线程 xff08 方法二 xff09 实现Runnabl
  • Linux中实现cp命令

    一 main函数的原型 main int argc char argv argc是所有参数的总数量 argv为参数 如该程序所示 xff1a 程序运行结果如图所示 xff1a 二 实现cp指令的思路 1 打开src c 2 通过lseek函
  • Linux系统编程—进程

    一 相关概念 1 进程与程序 xff1a 程序是一个静态的概念 如gcc xxx c o pro xff0c 磁盘中生成的Pro文件 xff0c 叫做程序 进程就是程序运行起来 xff0c 系统中就多了一个进程 2 查看进程的方法 xff1
  • Linux系统编程——进程间通信

    目录 一 无名管道 相关概念 相关函数介 相关说明 实战 二 有名管道 有名管道和无名管道的区别 有名管道和无名管道的相同点 有名管道的相关函数 实战 三 消息队列 消息队列的介绍 消息队列的特点 相关函数 实战 四 共享内存 共享内存优缺
  • 树莓派——初级编程

    一 没有屏幕的树莓派如何使用 通过串口连接树莓派 通过官方给的相关文件和软件 xff0c 对树莓派进行刷系统 设备破解 默认情况 xff0c 树莓派的串口和蓝牙连接 想办法断开蓝牙连接 xff0c 把串口用来数据通信 默认账号 xff1a
  • 树莓派gpio驱动编写——不使用wiringPi

    一 相关概念 总线地址 总线地址 xff1a cpu能够访问内存的范围 可以通过cat proc meminfo 来查看内存条大小 物理地址 物理地址 xff1a 硬件的实际地址或绝对地址 虚拟地址 虚拟地址 xff1a 逻辑 xff08
  • 初识网络七层模型

    OSI 模型 Open System Interconnection model 是一个由国际标准化组织 x10fc01 提出的概念模型 试图 x10fc01 供一个使各种不同的计算机和网络在世界范围内实现互联的标准框架 它将计算机网络体系
  • http的简单实现

    一 http协议的特性 http协议是建立在TCP IP协议之上应用层协议 xff0c 默认端口为80 8080 http协议的的特点是无状态 xff0c 无连接 二 http协议的请求 利用抓包工具httpwatch可以获取报文 http
  • Selenium之css怎么实现元素定位?

    世界上最远的距离大概就是明明看到一个页面元素站在那里 xff0c 但是我却定位不到 xff01 xff01 Selenium定位元素的方法有很多种 xff0c 像是通过id name class name tag name link tex
  • 零基础如何学习计算机语言

    一 认识电脑键盘 1 对于一个还没有接触过计算机的人来说认识键盘是重要的 2 通用的几个常见快捷键 xff1a CTRL 43 C 复制 CTRL 43 V 粘贴 CTRL 43 A 全选 CTRL 43 X 剪切 CTRL 43 S 保存
  • 如何用python连接Linux服务器

    1 安装paramiko库 pip install paramiko 2 使用paramiko库连接linux 导入库 import paramiko 创建一个sshclient对象 ssh 61 paramiko SSHClient 允许
  • 包装类

    注意点 包装类是final的类 包装类对象是不变的 与字符串类似 不变模式 共有8种包装类 int Integer long Long byte Byte short Short float Float double Double bool
  • 数据库和缓存如何保证一致性?

    先更新数据库 xff0c 还是先更新缓存 xff1f 有了这个想法后 xff0c 就准备开始着手优化服务器 xff0c 但是挡在在他前面的是这样的一个问题 由于引入了缓存 xff0c 那么在数据更新时 xff0c 不仅要更新数据库 xff0
  • 一名高级的Javaer,应该了解的 MYSQL 高级知识点

    SQL查询流程 1 通过客户端 服务器通信协议与 MySQL 建立连接2 查询缓存 xff0c 这是 MySQL 的一个可优化查询的地方 xff0c 如果开启了 Query Cache 且在查询缓存过程中查 询到完全相同的 SQL 语句 x
  • Python循环语句

    1 for循环 1 for in range xff08 xff09 简单的for循环代码 xff1a for i in range 5 print i 输出的便是0 4形成一列 xff0c 这里rang xff08 5 xff09 xff
  • 【Android自定义注解】BindView

    注意 xff1a 定义的控件变量必须是public xff0c 否则报错 1 添加类 package com example zhujie import java lang annotation Retention import java
  • 数据可视化——(提示框插件)

    提示框插件有很多 xff0c 不同的框架中选择的也不一样 本文主要分享toastr插件总结它的使用步骤如下 xff1a 使用步骤 xff1a 加载 jqury js toastr css 和 toastr js 文件 全局配置 为方便 xf
  • 文章五:Python 网络爬虫实战:使用 Beautiful Soup 和 Requests 抓取网页数据

    一 简介 本篇文章将介绍如何使用 Python 编写一个简单的网络爬虫 xff0c 从网页中提取有用的数据 我们将通过以下几个部分展开本文的内容 xff1a 网络爬虫的基本概念Beautiful Soup 和 Requests 库简介选择一
  • Android Studio编译无错,但在模拟器上无法运行App

    今天在学习郭霖老师的 第一行代码 第三版时碰到一个问题 xff0c 明明运行无错 xff0c 却无法在模拟器上正常运行 一开始提示说 Waiting for all target devices to come online xff0c 在

随机推荐

  • 突破CloudFlare五秒盾付费版

    使用的第三方库 cloudscraper 可以绕过免费版的五秒盾 但遇到付费版就无能为力了 最近在爬币圈的网站 xff0c 其中有一个网站叫做 xff1a Codebase 1 使用的就是付费版的 CloudFlare 五秒盾 当我们使用
  • CentOS-7 配置 SSH 远程登录

    CentOS 7 配置 SSH 远程登录 CentOS 配置 SSH 远程登录一 环境二 配置网络1 检查网络状态2 下载 net tools 工具3 检查主机与虚拟机是否正常连通 三 SSH 配置1 检查 CentOS 系统是否已经安装了
  • Linux中如何使用systemctl进行服务的管理?

    一 运行级别的分类 runlevel 运行级别0 xff1a 系统停机状态 运行级别1 xff1a 单用户工作状态 xff0c root权限 xff0c 用于系统维护 xff0c 禁止远程登陆 运行级别2 xff1a 多用户状态 没有NFS
  • Hibernate的配置

    Hibernate 的配置 1 创建工程 xff1b 2 导入 Jar 包 xff1b 3 写 Hibernate 配置文件 xff08 hibernate cfg xml xff09 a 数据库连接信息 b 映射文件包含配置 4 创建表和
  • vue3 axios

    Axios 是一个基于 promise 网络请求库 xff0c 作用于node js 和浏览器中 简介 Axios 是一个用于浏览器和Node的基于承诺的简单HTTP客户端 它提供了一个易于使用的库 xff0c 占地面积小 它还有一个可扩展
  • Linux系统四种安软方式

    Linux软件安装方式 本地安装 把需要的软件压缩包下载到linux主机 xff0c 在主机上直接解压启动即可完成本地安装 xff0c 即绿色版安装 把需要的rpm软件包下载到linux主机 xff0c 在主机上使用rpm命令完成本地安装
  • 关于vscode安装扩展插件提示:获取扩展失败,XHR error

    在我们安装vscode扩展插件时 xff0c 出现报错 xff1a error while fetching extensions XHR error 搜了很多网友的解决方案 xff0c 比如修改网络代理设置 xff0c 修改hosts文件
  • simulink模块,提供xpctarget下驱动源码

    simulink模块 xff0c 提供xpctarget下驱动源码 77999632700099250风中的蜗牛
  • SQL SERVER创建字段注释

    第一种方法是用SQL SERVER的管理工具 表设计中的列属性自带说明 xff0c 填写会自动生成注释 第二种方法 如果在navicat等工具上无法可视化创建注释的 xff0c 需要执行语句 EXEC sys sp addextendedp
  • Android平台上如何让应用程序获得系统权限以及如何使用platform密钥给apk签名

    您好 xff0c 欢迎关注我的专栏 xff0c 本篇文章是关于 Flutter 的系列文 xff0c 从简单的 Flutter 介绍开始 xff0c 一步步带你了解进入 Flutter 的世界 你最好有一定的移动开发经验 xff0c 如果没
  • Android 11 Settings源码入门,我就不信你还听不明白了

    前言 曾听过很多人说Android学习很简单 xff0c 做个App就上手了 xff0c 工作机会多 xff0c 毕业后也比较容易找工作 这种观点可能是很多Android开发者最开始入行的原因之一 在工作初期 xff0c 工作主要是按照业务
  • Linux破解密码

    1 重启虚拟机 xff0c 在引导界面按 e xff08 按鼠标左键 xff0c 用键盘控制上下 xff09 xff0c 进入类界面 xff0c 把中间的ro改为 rw rd break 2 按住Ctrl 43 x xff0c 进入紧急界面
  • “移除”虚拟机和“从磁盘中删除”虚拟机的区别

    1 二者的区别 xff1a 移除 虚拟机操作只是在虚拟机上删除了 xff0c 并没有在Windows系统中删除相关文件 xff0c 是部分删除 xff1b 而 从磁盘中删除 是既在虚拟机上删除了 xff0c 也删除了Windows系统中的相
  • Linux常用命令

    一条命令的结构 xff1a 用户名 64 主机名 工作目录 提示符 lt 命令 gt 选项 参数1 参数2 一 文件操作类命令 1 touch命令 xff1a 用于建立文件或更新文件的修改日期 1 语法格式 xff1a touch 参数 文
  • 内部类

    链接 xff1a https www nowcoder com questionTerminal 48524c47dd924887be6684b17175fa40 1 为什么使用内部类 使用内部类最吸引人的原因是 xff1a 每个内部类都能
  • CentOS 8本地离线YUM源的配置

    1 准备好CentOS 8相同版本号的系统镜像文件 2 添加光驱硬件 xff0c 在光驱中调用iso镜像文件 xff08 具体操作 xff1a 先打开设置里面的CD DVD xff0c 再点击使用ISO镜像文件 xff0c 选择浏览会跳转到
  • Linux操作系统:管理用户和组

    任务一 xff1a Linux用户类型和组群 1 Linux系统下的用户账户分为三种 超级用户 xff08 root xff09 xff1a 拥有系统的最高权限 xff0c 可以不受限制的对任何文件和命令进行操作 xff0c 对系统具有绝对
  • 在CentOS_8中添加新的硬盘

    添加新硬盘的具体步骤 xff1a 第一步 xff1a 第二步 xff1a 第三步 xff1a xff08 注意 xff1a 这里选择 SATA A xff0c 其优点是可随时使用 xff0c 无需重启 xff1b 而 SCSI S 需要重启
  • Linux下生产者消费者模型

    Linux下生产者消费者模型 一 什么是生产者消费者模型二 代码实现三 运行结果与修改 一 什么是生产者消费者模型 生产者消费者模型就是通过一个容器来解决生产者和消费者的强耦合问题 生产者和消费者彼此之间不直接通讯 xff0c 而通过阻塞队
  • 开发一个支持跨平台的 Kotlin 编译器插件

    前言 前面简单介绍了一下Kotlin编译器插件是什么 xff0c 以及如何一步一步开发一个Kotlin编译器插件 xff0c 但是之前开发的编译器插件是通过修改字节码的方式来修改产物的 xff0c 只支持JVM平台 今天主要在此基础上 xf