前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Javac的构建过程及入口函数

Javac的构建过程及入口函数

作者头像
KINGYT
发布2023-03-15 13:57:51
1.3K0
发布2023-03-15 13:57:51
举报

以前就知道javac的逻辑是用java实现的,当时猜测javac应该是个shell脚本,脚本的内容大概就是通过java命令执行对应的java文件来实现javac的逻辑。

但后来偶然一次机会发现,javac并不是shell脚本,而是二进制文件。

但javac不是用java实现的吗?这里怎么是二进制文件呢?

带着这些疑问,花了两天时间,把openjdk构建过程的脚本通读了一遍,这才解开了这些疑问,这里写下来分享下。

下文涉及到的源码所属的OpenJDK版本为

➜ hg id b5f7bb57de2f jdk-12+31

OpenJDK的构建是用Autoconf和GNU Make来实现的,主体的构建脚本都在OpenJDK根目录的make文件夹下。该文件夹下的autoconf目录里的文件是用来实现Autoconf部分,该文件夹下的其他文件是用来实现make部分。

make文件夹下有个launcher目录,该目录下的各种makefile文件就是用来构建jdk里的各种命令的,比如javac、jcmd、jshell等。

我们来看下该目录的内容

代码语言:javascript
复制
➜  tree make/launcher 
make/launcher
├── LauncherCommon.gmk
├── Launcher-java.base.gmk
├── Launcher-java.rmi.gmk
├── Launcher-java.scripting.gmk
├── Launcher-java.security.jgss.gmk
├── Launcher-jdk.accessibility.gmk
├── Launcher-jdk.aot.gmk
├── Launcher-jdk.compiler.gmk
├── Launcher-jdk.hotspot.agent.gmk
├── Launcher-jdk.jartool.gmk
├── Launcher-jdk.javadoc.gmk
├── Launcher-jdk.jcmd.gmk
├── Launcher-jdk.jconsole.gmk
├── Launcher-jdk.jdeps.gmk
├── Launcher-jdk.jdi.gmk
├── Launcher-jdk.jfr.gmk
├── Launcher-jdk.jlink.gmk
├── Launcher-jdk.jshell.gmk
├── Launcher-jdk.jstatd.gmk
├── Launcher-jdk.pack.gmk
├── Launcher-jdk.rmic.gmk
└── Launcher-jdk.scripting.nashorn.shell.gmk

其中Launcher-jdk.compiler.gmk这个makefile就是用来构建javac命令的,我们重点看下这个文件。

首先看下该文件里有关构建javac命令的相关逻辑

代码语言:txt
复制
# make/launcher/Launcher-jdk.compiler.gmk
(eval (call SetupBuildLauncher, javac, \
   MAIN_CLASS := com.sun.tools.javac.Main, \
   JAVA_ARGS := --add-modules ALL-DEFAULT, \
   CFLAGS := -DEXPAND_CLASSPATH_WILDCARDS, \
))

该部分逻辑调用了SetupBuildLauncher方法,传入了一些参数,比如第一个参数javac,就是说要构建的二进制文件名为javac。

我们再来看下SetupBuildLauncher方法

代码语言:javascript
复制
# make/launcher/LauncherCommon.gmk

SetupBuildLauncher = $(NamedParamsMacroTemplate)

define SetupBuildLauncherBody

  ...

  ifneq ($$($1_MAIN_CLASS), )

    $1_LAUNCHER_CLASS := -m $$($1_MAIN_MODULE)/$$($1_MAIN_CLASS)

  endif

  ...

  $1_JAVA_ARGS_STR := '{ $$(strip $$(foreach a, \

      $$(addprefix -J, $$($1_JAVA_ARGS)) $$($1_LAUNCHER_CLASS), "$$a"$(COMMA) )) }'

  $1_CFLAGS += -DJAVA_ARGS=$$($1_JAVA_ARGS_STR)

  ...

  $$(eval $$(call SetupJdkExecutable, BUILD_LAUNCHER_$1, \

      NAME := $1, \

      EXTRA_FILES := $(LAUNCHER_SRC)/main.c, \

      CFLAGS := ...

          -DLAUNCHER_NAME='"$(LAUNCHER_NAME)"' \

          -DPROGNAME='"$1"' \

          $$($1_CFLAGS), \

      ...

  ))

  ...

endef

这个方法稍微复杂些,我们先说下各个变量的值,再大致说下方法逻辑。

$1为上个方法传过来的参数,值为javac

$1_MAIN_CLASS为上个方法传过来的参数,值为com.sun.tools.javac.Main

$1_MAIN_MODULE的值为jdk.compiler

$1_LAUNCHER_CLASS 的值为jdk.compiler/com.sun.tools.javac.Main

1_JAVA_ARGS_STR 的值我们只要知道包含1_LAUNCHER_CLASS的值就好

$(LAUNCHER_SRC)的值为src/java.base/share/native/launcher

$(LAUNCHER_NAME)的值为openjdk

这个方法的大体逻辑就是,经过一系列变量赋值之后,调用SetupJdkExecutable方法,把 (LAUNCHER_SRC)/main.c 文件编译成 javac 命令,编译时参数为 -DLAUNCHER_NAME=’openjdk’ -DPROGNAME=’javac‘ -DJAVA_ARGS=(1_JAVA_ARGS_STR) 等。

该二进制文件在编译时,也以json形式输出了一份完整的命令内容,文件的位置为 ./build/linux-x86_64-server-release/make-support/compile-commands/support_native_jdk.compiler_javac_main.o.json,文件的关键内容为

代码语言:javascript
复制
/usr/bin/gcc ... -DLAUNCHER_NAME='\"openjdk\"' -DPROGNAME='\"javac\"' 
... -DJAVA_ARGS='{ \"-J--add-modules\", \"-JALL-DEFAULT\", \"-J-ms8m\", \"-m\", \"jdk.compiler/com.sun.tools.javac.Main\", }'
 ... -c -o /home/yt/workspace/jdk/build/linux-x86_64-server-release/support/native/jdk.compiler/javac/main.o /home/yt/workspace/jdk/src/java.base/share/native/launcher/main.c

由上可见,该命令内容和我们从代码中分析的一样。

至此我们可以知道,javac命令确实是二进制文件,其对应的c文件为 src/java.base/share/native/launcher/main.c,当我们在执行javac命令时,调用的就是这个c文件中的main方法。

我们再通过一个小实验进一步确定下。

从源码中我们可以知道,在运行src/java.base/share/native/launcher/main.c的main方法时,我们可以加一个环境变量,使其输出程序名及参数等信息。

执行命令如下

代码语言:javascript
复制
➜  _JAVA_LAUNCHER_DEBUG=1 bin/javac --version
----_JAVA_LAUNCHER_DEBUG----
Launcher state:
        ...
  program name:javac
  launcher name:openjdk
        ...
  fullversion:12-internal+0-adhoc.yt.jdk
Java args:
jargv[0] = -J--add-modules
jargv[1] = -JALL-DEFAULT
jargv[2] = -J-ms8m
jargv[3] = -m
jargv[4] = jdk.compiler/com.sun.tools.javac.Main
...

由上我们可以看到,javac命令的 program name为javac,launcher name为openjdk,而 Java args 是个数组,值为上面输出的内容。

那这些输出的数据又是哪来的呢?根据源码我们可以找到以下头文件

代码语言:javascript
复制
// src/java.base/share/native/launcher/defines.h
#ifdef JAVA_ARGS
#ifdef PROGNAME
static const char* const_progname = PROGNAME;
#else
static char* const_progname = NULL;
#endif
static const char* const_jargs[] = JAVA_ARGS;
...
#else  /* !JAVA_ARGS */
...
static const char* const_progname = "java";
static const char** const_jargs = NULL;
...
#endif /* JAVA_ARGS */


#ifdef LAUNCHER_NAME
static const char* const_launcher = LAUNCHER_NAME;
#else  /* LAUNCHER_NAME */
static char* const_launcher = NULL;
#endif /* LAUNCHER_NAME */

该文件中的这些变量就是上面输出的数据,而这些变量的值的来源正是我们在编译时指定的 -DLAUNCHER_NAME、-DPROGNAME、-DJAVA_ARGS参数。

而由于 src/java.base/share/native/launcher/main.c 文件引用了这个头文件,所以在这个文件中,也就能拿到这些变量的值了。

我们再总结下整个过程

javac命令的入口函数为src/java.base/share/native/launcher/main.c文件中的main方法。

在编译该文件时,通过指定 -DPROGNAME=”javac” -DJAVA_ARGS='{ … “-m”, “jdk.compiler/com.sun.tools.javac.Main”, }’ 等参数,使javac命令在编译期就确定了其要执行的包含main方法的java类为 jdk.compiler/com.sun.tools.javac.Main。

在运行javac时,javac获取该java类,调用它的main方法,然后把我们传给javac命令的参数,传给该java类的main方法。

最后,通过该Java类的main方法以及其他相关内容,实现javac命令的总体逻辑。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2019-02-10,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 卯时卯刻 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档